Ssh knock

Aus UUGRN

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 Zugangscode 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.

Grundsätzliches[Bearbeiten]

Andere Lösungen mit SSH mit Portknocking basieren in aller Regel auf der serverseitigen Verwaltung von Firewallregeln für den SSH-Daemon in Abhängigkeit von zusätzlichen Mechanismen, die das jeweilige Portknocking implementieren.

Der hier beschriebene Ansatz basiert darauf, dass nicht ein permanenter SSH-Daemon läuft sondern dass für jede Client-Verbindung ein neuer (eigener) SSH-Daemon Prozess via xinetd gestartet wird. Ein so gestarteter SSH-Daemon überlässt die TCP/IP Kommunikation dem xinetd. Wird die Verbindung zum Client beendet, so beendet sich auch der jeweilige SSH-Daemon wieder.

Der initiale Verbindungsaufbau – also noch bevor das eigentliche SSH-Protokoll beginnt – wird hier einerseits durch die Konfiguration des jeweiligen xinetd-Services und zusätzlich noch durch ein Wrapperscript kontrolliert.

Server[Bearbeiten]

Pakete auf dem Server[Bearbeiten]

Neben dem OpenSSH-Server (/usr/sbin/sshd) wird noch benötigt:

security/xinetd
Replacement for inetd with better control and logging

xinetd Konfiguration[Bearbeiten]

/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 4711/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
}

Anstelle von server = /usr/local/sbin/sshd_knock.sh könnte man auch server = /usr/sbin/sshd und als server_args = -i verwenden, um per xinetd einen eigenen SSH-Daemon pro Client-Session im sogenannten inetd mode zu starten. Das wäre dann nur mit xinetd-Mitteln reglementiert.

Das Anwendungsbeispiel hier geht allerdings einen Schritt weiter und verwendet für /usr/sbin/sshd nochmal ein wrapperscript, hier /usr/local/sbin/sshd_knock.sh

Service-Wrapper Script[Bearbeiten]

/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[Bearbeiten]

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[Bearbeiten]

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[Bearbeiten]

Pakete[Bearbeiten]

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[Bearbeiten]

/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[Bearbeiten]

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[Bearbeiten]

$ 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. Auch aus Sicht eines Angreifers oder Mithörers in der Leitung existiert durch die Verwendung von ControlMaster nur genau eine tcp-Session zwischen Client und Server. Bricht diese allerdings dann ab, werden alle anderen SSH-Session ebenfalls beendet.
Spuren im Logfile auf einem Proxy-Server
Will man eine SSH-Session über einen HTTPS-Proxy aufbauen jedoch aber verschleiern, so muss der xinetd auf dem Server auf Port 443 (oder einem anderen erlaubten Port für CONNECT auf dem Proxy) laufen.
Durch die Verwendung des TOKENs kann ein Proxy-Admin, der später das Logfile analysiert nur die Verbindung als solche sehen, nicht allerdings das verwendete TOKEN, denn das wird erst nach dem CONNECT mit dem Zielserver übertragen und landet daher nicht im regulären Logfile de Proxys.
Ein TCP-Mitschnitt (tcpdump, wireshark) könnte das TOKEN und den SSH-Server-Banner allerdings sichtbar machen, denn es wird noch im Klartext übermittelt. Da das TOKEN nur für eine begrenzte Dauer gültig ist, kann es dennoch nicht ohne weiteres missbraucht werden.
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[Bearbeiten]

  • 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.
  • Vermeidung von Bots-Probes und allgemein DDoS auf den sshd (via xinetd), sofern dieses Konstrukt dazu verwendet wird, um auf Port 22 zu lauschen. Per xinetd(8) lassen sich durch weitere Access-Policies der Zugriff auf den Service insgesamt sehr feingranular regelmentieren (XINETD.CONF(5): instances, nice, only_from, no_access, access_times, per_source, cps, max_load, rlimit_*, deny_time).