Ssh knock

Aus UUGRN
Version vom 17. Januar 2014, 17:02 Uhr von Rabe (Diskussion | Beiträge) (SSH-Daemon mit Tarnnetz)
(Unterschied) ← Nächstältere Version | Aktuelle Version (Unterschied) | Nächstjüngere Version → (Unterschied)

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.