Ssh knock
Dieser Artikel beschreibt wie man mit Hilfe von xinetd einen sshd verwalten und absichern kann. Als zusätzliches Bonbon existiert ein wrapper-Script, welches vor dem SSH Verbindungsaufbau einen kurzlebigen Code abfragt: Wird nach dem Verbindungsaufbau ein falscher Code übermittelt, so wird der sshd für die entsprechende Session gar nicht erst gestartet.
Der Aufbau wird für FreeBSD-Server und Linux-Clients beschrieben.
Server
Pakete auf dem Server
Neben dem OpenSSH-Server (/usr/sbin/sshd) wird noch benötigt:
- security/xinetd
- Replacement for inetd with better control and logging
xinetd Konfiguration
- /usr/local/etc/xinetd.conf
- Relativ harmlos, entscheidend ist hier lediglich das includedir-Statement
defaults { instances = 25 log_type = FILE /var/log/servicelog log_on_success = HOST PID log_on_failure = HOST per_source = 5 } includedir /usr/local/etc/xinetd.d
- /usr/local/etc/xinetd.d/sshd_knock
- xinetd-Service Definition für unseren Special-SSH-Daemon, zum Beispiel dass der Service nur auf IPv6 39078/tcp lauscht und nur von einer bestimmten (externen) IPv6-Adresse aus erreichbar sein soll (kann man auch weglassen):
service sshd_knock { disable = no id = sshd_knock type = UNLISTED socket_type = stream protocol = tcp port = 4711 wait = no user = root group = wheel flags = IPv6 only_from = 2001:xxx:xxx:xxx:xxx:xxx:xxx:27a5 server = /usr/local/sbin/sshd_knock.sh }
Service-Wrapper Script
- /usr/local/sbin/sshd_knock.sh
#! /bin/sh DENY="ACCESS DENIED" log() { /usr/bin/logger -t sshd_knock.sh "$@" } deny() { echo "${DENY}" >&2 log "${DENY}: $@" exit 1; } test -r /root/.ssh_knock || deny "/root/.ssh_knock doesn' exist" SECRET="" . /root/.ssh_knock || deny "Error loading /root/.ssh_knock" test -n "${SECRET}" || deny "Error loading /root/.ssh_knock: SECRET is empty" test -n "${REMOTE_HOST}" || deny "Cannot run without xinetd" read KNOCK INFO TOKEN="$(/bin/date "+%Y%m%d%H%M-${SECRET}" | /sbin/md5)" LOGLINE="TOKEN=${TOKEN} REMOE_HOST=${REMOTE_HOST} KNOCK=${KNOCK} INFO=${INFO}" test "${KNOCK}" != "${TOKEN}" && deny "FAILED: ${LOGLINE}" log "MATCHED: ${LOGLINE}" exec /usr/sbin/sshd -i
- /root/.ssh_knock
- enthält eine Zeile mit dem shared secret:
SECRET="Ye6Neir5so"
Relevant für dieses Script ist, das ein TOKEN generiert wird, welches auf der md5 checksumme des aktuellen Datum mit Uhrzeit in ganzen Minuten und zusätzlich dem shared secret, hier zum Beispiel Ye6Neir5so, basiert. Das ist deswegen relevant, weil die das Token vom Client im Klartext übermittelt wird und abgehört werden kann, es sollte daher nur kurzzeitig (1 Minute) gültig sein. Der Client benötigt für diesen Vorgang die aktuelle Uhrzeit (syncron zum Server) und das shared secret im Klartext, um daraus den Token bilden zu können.
Sobald man den xinetd (neu) startet, wird dieser auf Port 4711/tcp (hier nur IPv6) lauschen und bei einer neuen Verbindung vom Client das Script /usr/local/sbin/sshd_knock.sh ausführen. Ist das initiale TOKEN vom Client korrekt übermittelt, so wird der SSH-Daemon im inetd Modus gestartet. Die Option -i sorgt dafür, dass der gestarete ssh-daemon kein TCP/IP spricht sondern das gesamte SSH-Protokoll via stdin/stdout abwickelt.
Test
Zum Testen benötigt man daher ein gültiges TOKEN. Dieses kann man in einer Endlosschleife sekundengenau in einem Terminanfenster ausgeben lassen:
- unter Linux
while sleep 1; do date "+%Y%m%d%H%M-Ye6Neir5so" | md5sum; done
In einem zweiten Terminalfenster startet man ein netcat und fügt dort das aktuell gültige Token (aus dem ersten Fenster) ein:
- unter Linux
user@client$ nc -6 knock.example.com 4711 0e97ca1755eeb5d5de973b26f3c23074 SSH-2.0-OpenSSH_5.8p2_hpn13v11 FreeBSD-20110503 ^C
Das Token war korrekt, wir sehen nun den gewohnten Banner von OpenSSH im Klartext. Wir brechen das mit ^C ab.
Wartet man eine Weile ab, so wird das eben noch gültige TOKEN invalid:
user@client$ nc -6 knock.example.com 4711 0e97ca1755eeb5d5de973b26f3c23074 ACCESS DENIED
Variationen
Durch Ändern der Bildungsvorschrift für das TOKEN kann man somit auch die Dauer beeinflussen, in der das TOKEN gültig ist. Sind sowohl der Server als auch der Client jeweils zeitsyncron, könnte man versuchen die Lebensdauer von gültigen TOKENs auf eine Sekunde zu reduzieren. Alternativ könnte man auch den Unix-Timestamp (date +%s) ganzzahlig durch 10 teilen, damit der TOKEN nur innerhalb der jeweiligen 10-Sekunden gültig ist. Wird das TOKEN erst zum Ende der jeweiligen Periode (Minute, Sekunde, 10sekunden) erzeugt, so ist es natürlich nur sehr kurz gültig. Das kann dazu führen, dass der Client mehrere Versuche unternehmen muss um eine akzeptierte verbindung zu bekommen.
Client
Pakete
- netcat-openbsd
- TCP/IP swiss army knife (OpenBSD Variante)
- openssh-client
- secure shell (SSH) client, for secure access to remote machines
Client Wrapper Script für ssh-Client
- /home/user/bin/ssh_knock.sh
- Das Client-Wrapper-Script ist dazu gedacht vom OpenSSH Client via ProxyCommand aufgerufen zu werden:
#! /bin/bash HOST="${1}" PORT="${2}" SECRET="${3:-"defaultinvalid"}" test -n "${HOST}" || { echo "ERROR: \$1=Hostname is not set" >&2 ; exit 1; } test -n "${PORT}" || { echo "ERROR: \$2=Port is not set" >&2 ; exit 1; } test ${PORT} -gt 0 || { echo "ERROR: PORT=${PORT} is not a positive number" >&2 ; exit 1; } /bin/nc "${HOST}" "${PORT}" < <(date "+%Y%m%d%H%M-${SECRET}" | md5sum ; cat )
SSH-Client Konfiguration
Der Servername im DNS lautet real.server.name.example.com, wir verwenden für ssh den Alias knock.example.com, der in dieser Form weder auf dem Server bekannt sein muss noch im DNS stehen muss.
- /home/user/.ssh/config
Host knock.example.com HostName real.server.name.example.com Port 4711 User remoteusername IdentityFile ~/.ssh/id_key_so_use_here ControlMaster auto ControlPath ~/.ssh/master/%r@%h:%p ProxyCommand /home/user/bin/ssh_knock.sh %h %p Ye6Neir5so
Relevant sind hier der echte HostName, der (alternative) Port, auf dem der xinetd läuft und das ProxyCommand.
Ebenso wie der sshd -i sich nicht mehr im TCP/IP selbst kümmert sondern über stdin/stdout läuft, genauso kann man dem ssh-Client abgewöhnen selbst TCP/IP zu sprechen und stattdessen per stdin/stdout mit dem ProxyCommand zu kommunizieren. Das script ssh_knock funktioniert als Wrapper für /bin/nc (netcat-openbsd), wobei stdin zunächst das TOKEN übermittelt wird bevor dann mittels "cat" der restliche Datenstrom übertragen wird. stdout von diesem Script wird hier nicht berührt.
Test
$ ssh knock.example.com
- Randbedingung
- Durch ControlMaster wird nur die erste SSH-Verbindung tatsächlich neu aufgebaut, also mit TOKEN und allem. Startet man eine weitere ssh-Session mit knock.example.com, so wird diese als zusätzlicher Channel durch die bestehende erste ssh-Session mit benutzt. Aus Sicht von xinetd und sshd kommt dabei keine neue SSH-Verbindung zustande.
- ssh -v
- Hier sieht man, wie der SSH-Client anhand der Konfiguration aus ~/.ssh/config für den Host (Alias) knock.example.com das entsprechende ProxyCommand ausführt:
… debug1: Applying options for * debug1: Executing proxy command: exec /home/user/bin/ssh_knock.sh real.server.name.example.com 4711 Ye6Neir5so debug1: permanently_drop_suid: 1000 …
Anwendung
Betrieb eines (versteckten) ssh-Daemons, der nur für bestimmte Quell-Systeme als solcher erkennbar ist und auch nur bei Übermittlung eines (zeitlich) passenden TOKENs für den Quell-Server erkennbar wird.