Skip to content

Login als technischer User mit ssh forced commands

Oftmals hat man die Aufgabe, einen maschinenübergreifenden Zugriff automatisiert stattfinden zu lassen, ohne dass man der einen Seite gleich "vollwertige" Credentials für einen interaktiven Login auf der anderen Seite geben möchte. Wenn der Zugriff automatisiert stattfinden soll, fällt auch noch die Sicherheitsstufe eines ssh passphrases weg, da niemand da ist um diesen einzugeben.

ssh forced commands bieten eine Möglichkeit, einen solchen Zugriff zu erlauben, ohne gleich die Büchse der Pandora zu öffnen. Selbst ein automatischer Zugriff mit root-Rechten ist auf diese Weise "reasonably secure" realisierbar.

Die Werkzeuge

Aus man sshd:

command="command":

Specifies that the command is executed whenever this key is used for authentication. The command supplied by the user (if any) is ignored. (…) This option might be useful to restrict certain public keys to perform just a specific operation. An example might be a key that permits remote backups but nothing else. The command originally supplied by the client is available in the `SSH_ORIGINAL_COMMAND` environment variable. (…) Also note that this command may be superseded by either a sshd_config(5) ForceCommand directive or a command embedded in a certificate.

from="pattern-list": Specifies that in addition to public key authentication, either the canonical name of the remote host or its IP address must be present in the comma-separated list of patterns. (…) The purpose of this option is to optionally increase security: public key authentication by itself does not trust the network or name servers or anything (but the key); however, if somebody somehow steals the key, the key permits an intruder to log in from anywhere in the world. This additional option makes using a stolen key more difficult (name servers and/or routers would have to be compromised in addition to just the key).

Aus man sshd_config:

PermitRootLogin: Specifies whether root can log in using ssh(1). The argument must be “yes”, “prohibit-password”, “without-password”, “forced-commands-only”, or “no”. The default is “prohibit-password”. If this option is set to “prohibit-password” or “without-password”, password and keyboard-interactive authentication are disabled for root.If this option is set to “forced-commands-only”, root login with public key authentication will be allowed, but only if the command option has been specified (which may be useful for taking remote backups even if root login is normally not allowed). All other authentication methods are disabled for root. If this option is set to “no”, root is not allowed to log in.

Wie und warum?

Mit diesen Mechanismen ist der automatisierte Login mit (optional passphraselosen) ssh-Keys "reasonably secure". Die from-Option macht einen gestohlenen Key nutzlos, weil er nur von den dort eingetragenen Hostnamen bzw. IP-Adressen überhaupt benutzt werden kann. Die command-Option sorg dafür, dass ein Angreifer, der den ssh-Client übernommen hat, nur die auf dem ssh-Server hinterlegten Aktionen ausführen kann, und das sind diejenigen, die für den Regelbetrieb unbedingt benötigt werden und schon in diesem (hoffentlich) ungefährlich sind.

PermitRootLogin forced-commands-only in der Konfiguration des sshd schränkt den root-Login per ssh so ein, dass nur Keys mit command-Option akzeptiert werden. Auf diese Weise kann man den root-Login für Aktionen wie Backup etc erlauben, ohne einen interaktiven root-Login zuzulassen. Damit sind hochkomplexe und fehleranfällige Konstrukte wie "ssh funktionsuser@remotehost sudo irgendwas" inklusive der nur auf die erlaubten Kommandos beschränkten Konfiguration in sudoers.d nicht mehr notwendig, weil in den meisten Fällen direkt "ssh root@remotehost" erlaubt werden kann, ohne inakzeptable Sicherheitsrisiken zu erzeugen.

Und all diese Konfiguration erfolgt serverseitig, so dass sie nur von jemandem beeinflusst werden können, der den Server bereits hat.

Ja, aber?

ja, man muss bei diesem Setup höllisch aufpassen, weil es etliche Fallstricke gibt:

  • Man muss die komplette Kommandozeile des aufzurufenden Kommandos in die authorized_keys schreiben, also inklusive aller Parameter und eventueller Dateinamen. Das macht den naiven Ansatz unflexibel.
  • Wird das System der Clientseite aktualisiert, ändern sich eventuell die Parameter, mit denen das Kommando der Serverseite aufgerufen wird (hallo, rsync!). Das führt dann dazu, dass die Serverseite trotzdem mit dem alten, in der authorized_keys festgenageltem Parametersatz aufgerufen wird, während die Clientseite sich im Glauben wähnt, den neuen Parametersatz verwendet zu haben, was im hübschesten Fall zu bizarren Trümmern führt.
  • Ja, man braucht für jedes einzelne Kommando einen eigenen Key.

Und so ist es dann beherrschbar

Ich verwende deswegen eine Zwischenstufe mit einem Shellscript, das in der authorized_keys aufgerufen wird:

 
mh@torres:/usr/local/stow/ssh-forced-command/bin$ cat ssh-forced-command

#!/bin/bash

KEY="$1"
PATH="/usr/sbin:/usr/bin:/sbin:/bin"
me=$(basename "$0")

cmd()
{
  # write command to log and execute the command
  logger -p user.info -t "$me $KEY" -- "$@"
  "$@"
}

execute-all() {
  # this is mainly an "allow-all" debugging aid
  # when starting with a new setup, associate this function with a key to allow everything
  # and to log the exact command line. Then try everything that needs doing, use the log
  # to write a key-specific dispatcher function and associate the key with the new function
  # This function imprints the string "execute-all" into the log entries which can be used
  # to raise an alarm that execute-all, which is a debugging aid, is still in place for
  # a certain key.
  # using execute-all is a sure security risk, don't do it in production.
  KEY="$KEY execute-all"
  case "$SSH_ORIGINAL_COMMAND" in
    \*) cmd "$SSH_ORIGINAL_COMMAND";;
  esac
}

illegal-command() {
    # helper function to generate and log an error message
    echo "$me@$(hostname): illegal command $SSH_ORIGINAL_COMMAND"
    logger -p user.err -t "$me $KEY" -- "ignoring illegal command $SSH_ORIGINAL_COMMAND"
    exit 1
}

# dispatcher functions that parse $SSH_ORIGINAL_COMMAND, decide whether the client requested
# a valid command, and call the command. This is a point where one should consider
# a rewrite in perl or another language when we begin to pull parameters from $S_O_C.
# Please note that we avoid using contents originating on the remote side in the
# actually called command. If this is unavoidable in some function, careful sanitizing
# of the remote content is advised, this is a shell script after all.

mh-hostmaster-torres-ppl() {
  if echo "$SSH_ORIGINAL_COMMAND" | grep --quiet "^$(hostname --fqdn) rsync --server -v\\?logDtpre\\.iLs\\?f\\?x\\? --delete . /etc/bind/\\?"; then
    cmd rsync --server -logDtpre.iLsf --delete . /etc/bind/
  elif [ "$SSH_ORIGINAL_COMMAND" = "mkbinddirs" ]; then
    cmd /etc/bind/scripts/mkbinddirs
  elif [ "$SSH_ORIGINAL_COMMAND" = "rndc reload" ]; then
    cmd /usr/sbin/rndc reload
  elif echo "$SSH_ORIGINAL_COMMAND" | grep --quiet "^svnserve"; then
    cmd svnserve -t --tunnel-user=mh --root /home/mh-hostmaster/svn/dns
  elif [ "$SSH_ORIGINAL_COMMAND" = "distribute" ]; then
    cmd /home/mh-hostmaster/dns/scripts/distribute
  else
    illegal-command
  fi
}

mh-hostmaster-ivanova-ppl() {
  if echo "$SSH_ORIGINAL_COMMAND" | grep --quiet "^svnserve"; then
    cmd svnserve -t --tunnel-user=mh --root /home/mh-hostmaster/svn/dns
  elif [ "$SSH_ORIGINAL_COMMAND" = "distribute" ]; then
    cmd /home/mh-hostmaster/dns/scripts/distribute
  else
    illegal-command
  fi
}

# this case in the main shell program associates a given key-ID with a dispatcher function
# it is a coincidence that the key ID is identical to the function name, this is not a
# must.
# The catch-all deliberately generates an error message.
# It might be feasible to have a catch-all which simply invokes a dispatcher function
# with a name identically to the key ID and bombs out if none has been found.
case $KEY in
  mh-hostmaster-torres-ppl) mh-hostmaster-torres-ppl ;;
  mh-hostmaster-ivanova-ppl) mh-hostmaster-ivanova-ppl ;;
  *) echo "$me@$(hostname): illegal key $KEY presented"
     logger -p user.err -t "$me" -- "ignoring illegal key $KEY"
     exit 1
     ;;
esac

Eine dazugehörige authorized_keys könnte so aussehen:

mh@torres:~$ cat ~mh-hostmaster/.ssh/authorized_keys
from="85.214.68.41,::ffff:85.214.68.41,85.214.131.164,::ffff:85.214.131.164",command="/usr/local/bin/ssh-forced-command mh-hostmaster-torres-ppl",no-port-forwarding,no-x11-forwarding,no-agent-forwarding,no-pty ssh-rsa  AAAA…8Tw== mh-hostmaster-torres-ppl 2008-05-14
from="81.169.179.176,2a01:238:40b7:9101::1",command="/usr/local/bin/ssh-forced-command mh-hostmaster-ivanova",no-port-forwarding,no-x11-forwarding,no-agent-forwarding,no-pty  ssh-rsa  AAAA…qqw== mh-hostmaster-ivanova 2014-12-24

(ssh public keys der Lesbarkeit halber gekürzt)

Zu beachten ist hier insbesondere, dass es nicht möglich ist, die Key-ID des Keys aus dem eigentlichen Key zu nehmen, die muss direkt aus dem Kommando als Parameter übergeben werden. Das ist nebenbei auch sicherer als die Übergabe über die environment=-Option, da der Client eine Environment-Variable u.U. überschreiben könnte.

Die Liste der gesperrten Features (no-port-forwarding, no-agent-forwarding etc pp) muss man aus der für den Server aktuellen sshd(8) manpage übernehmen. Neuere Versionen von OpenSSH (ab 7.2, nicht einmal in CentOS 7 enthalten [Debian ab stretch]) haben eine "restrict"-Option, die einfach alles abschaltet was man abschalten könnte. Die gewünschten Features können dann wieder eingeschaltet werden.

Man kann natürlich für jeden Account ein eigenes Script nehmen, oder alle Dispatcher-Funktionen und Key-IDs in einer einzigen Instanz des Scripts abhandeln.

Dieses Script erfordert höchste Vorsicht, es wird mit den Rechten des Users aufgerufen, dessen .ssh/authorized_keys den Call enthält, also im Zweifelsfall root.

Für die Einführungsphase eines neuen Keys mit neuen Rechten gibt es die Funktion "execute-all", die man dem neuen Key im Zuordungs-case assoziieren kann. Diese Funktion führt jedes Kommando über $SSH_ORIGINAL_COMMAND unbedingt aus und logged den aufruf. Analog zu SELinux' permissive-Mode kann man auf diese Weise die grundsätzliche Funktionstüchtigkeit des Remote-Aufrufs testen und erhält eine Liste der ausgeführten Kommandos, anhand der man dann eine passende Dispatcher-Funktion schreiben kann.

Trackbacks

No Trackbacks

Comments

Display comments as Linear | Threaded

Ulrich on :

Good news, everyone, mit RHEL7.4 bzw. CentOS 1708 wird openssh jetzt sehr bald(tm) auf 7.4 gehoben.

Marc 'Zugschlus' Haber on :

Man kann sich auch an der git-shell versuchen, die mit ~/git-shell-commands ein ähnliches Featureset hat. Ich habe ein paar Wünsche geäußert, die den Missbrauch der git-Shell für unsere Zwecke erleichtert: #873579.

Add Comment

Markdown format allowed
Enclosing asterisks marks text as bold (*word*), underscore are made via _word_.
Standard emoticons like :-) and ;-) are converted to images.
E-Mail addresses will not be displayed and will only be used for E-Mail notifications.
Form options