SSH - Secure SHell - Teil 2

Einleitung

Grundsätzliches zur Secure SHell wurde bereits in SSH - Secure SHell geschrieben. Im Folgenden möchte ich detaillierter auf die Konfigurationsdatei eingehen und eine kleine Möglichkeit zeigen, wie man Host-Aliase in einer Arbeitsgruppe einfach teilen kann. Damit wird SSH zu einem noch komfortableren Werkzeug.

« zurück (Gesammelte Anleitungen)

Inhalt

Shell Aliase und connect2-Shellskript

Als Netzwerk- und Systemadministrator hat man es oft mit einer Vielzahl von Systemen zu tun, auf die man mit SSH zugreift. Außerdem werden Dateien mit den SSH-Tools SCP oder SFTP auf oder von diesen Systemen kopiert. Jetzt wäre es natürlich schön, wenn man nicht immer die gesammten Parameter wie FQDN, Benutzer, Port, etc. auf der Kommandozeile mit angeben muss.

Eine naheliegende Möglichkeit ist es, für oft genutzte Zugriffe Shell-Aliases zu definieren, z. B.:

$ alias hostabc='ssh username@hostabc -p 10123'
$ hostabc
Password:
user@hostabc$ _

Ich bin sogar noch einen Schritt weiter gegangen und habe ein kleines Skript geschrieben, auf das man einfach einen Link mit dem Hostname setzt und das Skript sich dann, nach einer kurzen DNS und Ping-Probe, mit dem jeweiligen Host verbindet. Ich möchte dieses Skript (download) nicht vorenthalten, obwohl ich es nicht mehr verwende:

+ Skript: connect2
(+) drücken, um diesen Teil einzublenden

#!/bin/bash
# ----------------------------------------------------------------------
# name        : connect2
# description : connect to remote machine $0 if it is registered in DNS
#               or listed in hosts file and machine is running
# usage       : create an symbolic link to this script with the hostname
#               you wish to connect. call this script via the newly
#               created link to connect to the remote machine.
# ----------------------------------------------------------------------
# (c) Kai Hildebrandt <kai.hildebrandt@web.de>
# ----------------------------------------------------------------------
# my real name
me="connect2"

# username to use for connection if not given via link alias
# (e.g. user@host -> connect2)
# examples: "${LOGNAME}" or 'root'
default_username="${LOGNAME}"

# the name I was called
machine=$( basename $0 )
args=$@

# wrong call
if [ "${machine}" = "${me}" ]; then
    cat <<EOF
${me}: wrong call
usage: create an symbolic link to this script with the hostname you wish to
       connect. call this script via the newly created link to connect to
       the remote machine.
EOF
    exit 255
fi

# get probable username from link alias
username=${machine%%@*}
if [ "${username}" = "${machine}" ] ; then
    # no username given => use default
    username=${default_username}
else
    # remove username from machine name
    machine=${machine##*@}
fi

# do not connect to localhost
if [ "${HOSTNAME%%.*}" = "${machine}" ]; then
    echo "${me}: you are already on machine ${machine}. Exiting."
    exit 0
fi

# try to ping the machine
PING=$( which ping )

${PING} -c 1 ${machine} > /dev/null 2>&1
ret=$?

case ${ret} in
    2)
        echo "${me}: machine ${machine} not found in DNS. Exiting."
        exit 2
        ;;

    1)
        echo "${me}: ping timeout for machine ${machine}. Exiting."
        exit 1
        ;;

    0)
        echo "${me}: connecting to ${machine}..."
        exec ssh ${machine} ${args}
        ;;

    *)
        echo "${me}: this message should never appear!"
        echo "Return value of ping was ${ret}. Bug in script?"
        exit ${ret}
        ;;
esac

Man kann natürlich die Shell-Alias Variante mit diesem Skript kombinieren. Der Nachteil dieser Methode ist, dass man für jedes Kommando (ssh und scp) einen eigenen Alias anlegen muss.

Ich verwende aber weder Skript noch Shell-Aliase mehr dafür, denn das alles ist zu umständlich, wenn man weiß, wie man die Möglichkeiten der SSH-Konfigurationsdatei richtig ausschöpfen kann.

« nach oben

Erweiterte SSH-Konfiguration mit Host-Aliasen

Ich hatte das obige Skript und Aliase relativ lange im Einsatz und habe an sich nichts vermisst. Nachteile dieser Methode sind aber die doppelte Konfiguration für scp und dass diese Möglichkeit bereits in SSH enthalten ist.

Hier ein Beispiel einer Host-Definition aus der Datei $HOME/.ssh/config:

[...globale Optionen...]

Host abc
  HostName abc.example.com
  User khildebr
  Port 10123

Damit wird aus

$ ssh -p 10123 khildebr@abc.example.com

ein gleichbedeutendes, aber deutlich kürzeres

$ ssh abc

Wie auch bereits oben erwähnt, bezieht sich dieser Mechanismus auf alle SSH-Tools, also auch auf scp.

Die folgende Zeile kopiert die Datei /usr/local/bin/connect2 von dem Server abc.example.com in das lokale bin-Verzeichnis relativ zum aktuellen Arbeitsverzeichnis:

$ scp abc:/usr/local/bin/connect2 ./bin

Wie geil ist das denn? :-)

Es folgen Beispiele, wie man diesen Mechanismus weiter ausnutzen kann. Ich gehe davon aus, dass der SSH-Server als "Sprungbrett" in ein entferntes Netzwerk dient, über den dann dahinter liegende Dienste oder Server erreicht werden sollen.

« nach oben

Beispiele für die Verwendung

SSH-Konfigfragment: Port Forwarding auf lokalen Mailserver

Host xyz
  HostName xyz.dynamicip.example.net
  CheckHostIP no
  User grafzahl
  Port 22
  LocalForward 10025 localhost:25
  LocalForward 10143 localhost:143
  LocalForward 10993 localhost:993

Der entfernte Server ist über einen Dynamischen DNS-Namen, also einer sich dahinter wechselnden IP-Adresse erreichbar. Somit ist es nicht sinnvoll, diese IP-Adresse mit einer Reverse-DNS-Abfrage zu prüfen (Option: 'CheckHostIP no'). Die Port-Angabe wäre hier nicht notwendig, da der Standard-Port 22 verwendet wird.

Auf dem entfernten System laufen SMTP und IMAP-Dienste, die z. B. durch eine Firewall geschützt sind, oder nur an localhost (127.0.0.1) lauschen. Sobald die SSH-Verbindung aufgebaut ist, kann man auf dem lokalen Rechner über Port 10025 den SMTP-Server und über die Ports 10143 und 10993 den IMAP-Server des entfernten Systems erreichen.

SSH-Konfigfragment: Port Forwarding auf Windows Remotedesktop

Host abc
  HostName abc.example.com
  User khildebr
  Port 10123
  LocalForward 6500 ad01:3389
  LocalForward 6501 filer01:3389
  LocalForward 6502 backup:3389

In diesem Fall dient der SSH-Server als Einwahlserver, über den auf die drei Windows-Systeme ad01, filer01 und backup via RDP zugegriffen wird. Den Server ad01 kann man dann mit einem RDP-Client wie xfreerdp vom lokalen System aus erreichen:

$ xfreerdp -f -a 16 -u username -d domain localhost:6500

Ein weiteres cooles Feature von SSH ist, dass man Kommandos mitgeben kann, die nach einem erfolgreichen Verbindungsaufbau lokal ausgeführt werden. Damit kann man sich den manuellen Aufruf des RDP-Clients auch noch sparen und in die Host-Konfiguration einbauen:

Host abc
  HostName abc.example.com
  User khildebr
  Port 10123
  LocalForward 6500 ad01:3389
  PermitLocalCommand yes
  LocalCommand /usr/bin/xfreerdp -f -a 16 -u username -d domain localhost:6500 >/dev/null 2>&1 &

Der RDP-Client auf den Windowsrechner "ad01" öffnet sich nach einem

$ ssh abc

automatisch.

SSH-Konfigfragment: Zugriff auf Windows Remotedesktop über Komserver

Aus Sicherheitsgründen wird z. B. an Universitäten und Fachhochschulen sogenannte Komserver eingesetzt, auf die man sich vom Internet aus verbinden kann und dann erst noch auf einen weiteren im inneren Netzwerk stehenden Server verbinden muss, um an die geschützten Server zu gelangen.

Die Verbindung erfolgt wie dargestellt über zwei Schritte:

Zugriff via RDP über zwei Server: Zuerst auf einen Komserver und von dort aus über einen internen Server auf das Zielsystem

Und hier die entsprechende SSH-Konfiguration für diesen Aufbau:

Host windows01
  HostName comserver.example.org
  User khildebr
  LocalForward 40021 fileserver:22
  ExitOnForwardFailure yes
  PermitLocalCommand yes
  LocalCommand /usr/bin/ssh -N windows01-step2 &

Host windows01-step2
  HostName localhost
  Port 40021
  LocalForward 3391 windows01:3389
  User khildebr
  PermitLocalCommand yes
  LocalCommand /usr/bin/xfreerdp -f -a 16 -x modem -u khildebr -d windows01 --plugin cliprdr localhost:3391 >/dev/null 2>&1 &

Auch in diesem Fall öffnet sich der RDP-Client auf das Windowssystem mit dem Namen windows01

$ ssh windows01

automatisch.

Die Verbindung erfolgt aber im Gegensatz zu oben über zwei Schritte:

Zuerst wird eine SSH-Verbindung zu dem Komserver aufgebaut. In dieser Konfiguration wird ein Port Forwarding auf den im internen Netz stehenden "fileserver" eingerichtet. Falls die Weiterleitung nicht eingerichtet werden kann, sorgt die Option "ExitOnForwardFailure" dafür, dass die Aktion abgebrochen wird.

Mit der Option "LocalCommand" wird gleich danach wieder SSH aufgerufen:

/usr/bin/ssh -N windows01-step2 &

Damit wird über den eingerichteten Port Forward auf den internen Server zugegriffen und darüber dann der Zugriff auf den Remotedesktop von dem Windowssystem, auch wieder via Port Forward, hergestellt. Schließlich wird über die Option "LocalCommand" der RDP-Client auf dem lokalen System gestartet.

SSH-Konfigfragment: Multiplexen einer SSH-Verbindung und X-Forwarding

Host def
  HostName def.internal.example.org
  ControlMaster auto
  ControlPath /tmp/%r@%h:%p.sock
  ForwardX11 yes

Mit den Control-Optionen wird auf dem lokalen Rechner ein Socket im /tmp-Verzeichnis angelegt, über den die SSH-Verbindung zu dem entfernten Server 'def' gemultiplext wird, d. h. bei der ersten Verbindung wird die Verbindung und der lokale Datei-Socket angelegt und alle weiteren Verbindungen zu dem Server 'def' laufen nun über diese eine SSH-Verbindung.

Ein Vorteil davon ist der deutlich schnellere Verbindungsaufbau aller weiteren Verbindungen zu diesem Server. Ein Nachteil davon ist, dass alle SSH-Verbindungen (es ist ja nur eine) einbrechen, sollte die Leitung "wackeln". Man sollte das also nur im internen LAN verwenden und wenn man einen Server hat, auf den man viele SSH-Verbindungen geöffnet haben muss. Eine andere Alternative, wenn es nur um die Anzahl der geöffneten Shell-Fenster geht, wäre die Verwendung des Programms screen.

« nach oben

Zusammengesetzte SSH-Konfigurationsdatei

Abschließend gibt es noch eine Anregung für das gemeinsame Nutzen eines Teils der SSH-Konfiguration, z. B. für das zentrale Pflegen der Zugangsrechner von Kundensystemen oder der internen Serverlandschaft.

Die Idee: Aufteilen der SSH-Konfigurationsdatei in einen Globalen Teil und die Alias-Definitionen der Remote-Systeme, auf die eine Gruppe von Administratoren zugreift. Kommen neue Systeme hinzu oder werden bestehende Systeme geändert, wird durch ein erneutes Generieren der SSH-Konfigurationsdatei die Änderung bei allen übernommen.

Für die Generierung der SSH-Konfigurationsdatei habe ich mir ein Makefile geschrieben und in $HOME/.ssh gelegt. Durch ein einfaches 'make' in diesem Verzeichnis wird die Konfigurationsdatei aus den Fragmenten neu erzeugt. Hier ist dieses Makefile (download):

+ Skript: Makefile
(+) drücken, um diesen Teil einzublenden

DOTSSH="${HOME}/.ssh"
CONFIGD="${DOTSSH}/config.d"

all:
        @test -d "$(CONFIGD)" || { echo "error: directory '$(CONFIGD)' does not exist." ; exit 1 ; }
        @echo "# ~/.ssh/config"                        > $(DOTSSH)/config.gen ;\
        for conf in $(CONFIGD)/*.config ; do \
        conf_bname=`basename $$conf` ;\
        echo                                           >> $(DOTSSH)/config.gen ;\
        echo "##### BEGIN fragment '$$conf_bname'"     >> $(DOTSSH)/config.gen ;\
        cat "$${conf}"                                 >> $(DOTSSH)/config.gen ;\
        echo                                           >> $(DOTSSH)/config.gen ;\
        echo "##### END fragment '$$conf_bname'"       >> $(DOTSSH)/config.gen ;\
        done
        @if [ ! -r $(DOTSSH)/config.gen ] ; then \
        echo "error: file '$(DOTSSH)/config' was NOT generated" ;\
        exit 2 ;\
        fi
        @mv $(DOTSSH)/config.gen $(DOTSSH)/config
        @echo "success: generated file '$(DOTSSH)/config'"

Warnung: Vor dem ersten Aufruf von 'make' sollte man die Datei $HOME/.ssh/config wegsichern, da diese ungefragt überschrieben wird!

Was bei dem Aufruf von 'make' innerhalb des Verzeichnisses $HOME/.ssh passiert:

Alle SSH-Konfigurationsfragmente in dem Verzeichnis $HOME/.ssh/config.d mit der Dateiendung .config werden der Reihe nach, sortiert nach dem Dateinamen, zu einer einzigen Datei $HOME/.ssh/config zusammengefasst.

Hinweis: Das Makefile ist sehr generisch und kann generell dazu verwendet werden, Dateifragmente zu einer einzigen Datei zu vereinen.

Der Inhalt des Verzeichnisses $HOME/.ssh/config.d sieht bei mir folgendermaßen aus:

$ ls -1 $HOME/.ssh/config.d
000head.config
hosts_kunden.config
hosts_private.config

Bei 'hosts_kunden.config' handelt es sich um einen Link, der auf einen für alle Benutzer lesbar ist, die ihre SSH-Konfiguration auf diese Art und Weise aktualisieren.

Die erste Datei mit dem Namen 000head.config beinhaltet globale Optionen wie den Wildcard-Host (*). Diese Optionen werden für alle Hosts angewendet.

Der Inhalt der Datei 000head.config:

# overwrite settings defined in /etc/ssh/ssh_config
HashKnownHosts no

Host *
  Protocol 2
   ForwardX11 no
   SendEnv TERM

Die Option 'HashKnownHosts yes' verhindert, dass die Namen der Systeme im Klartext vor deren Host-Key in der Datei $HOME/.ssh/known_hosts gespeichert werden. Aus Sicherheitsgründen wurde die Option bei Debian gesetzt. Eine Diskussion über das 'Warum?' gibt es hier: HashKnownHosts (debian-devel)

Ich setze diese Option zurück, weil ich ein Shell-Skript namens 'ramssh' ;-) verwende, dass nicht mehr gültige Schlüssel aus der known_hosts-Datei entfernt.

Warnung: Bekannte Host-Keys sollte man nur aus der known_hosts-Datei entfernen, wenn sich ein Host-Key wissentlich geändert und man ist sich sicher, dass der Host oder Schlüssel nicht kompromittiert worden ist!

Die Warnung von SSH sieht folgendermaßen aus, wenn der übertragene Host-Key nicht dem bekannten Schlüssel in der known_hosts-Datei entspricht:

$ ssh def
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@       WARNING: POSSIBLE DNS SPOOFING DETECTED!          @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
The RSA host key for def.example.com has changed,
and the key for the according IP address 192.168.101.23
is unchanged. This could either mean that
DNS SPOOFING is happening or the IP address for the host
and its host key have changed at the same time.
Offending key for IP in /home/user/.ssh/known_hosts:4
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that the RSA host key has just been changed.
The fingerprint for the RSA key sent by the remote host is
12:34:56:78:9a:bc:de:f0:12:34:45:56:67:78:9a:bc.
Please contact your system administrator.
Add correct host key in /home/user/.ssh/known_hosts to get rid of this message.
Offending key in /home/user/.ssh/known_hosts:11
RSA host key for def.example.com has changed and you have requested strict checking.
Host key verification failed.

Das oben erwähnte Skript 'ramssh', mit dem man den nicht mehr gültigen Eintrag entfernen kann, gibt es hier (download):

+ Skript: ramssh
(+) drücken, um diesen Teil einzublenden

#!/bin/bash
# ----------------------------------------------------------------------
# name        : ramssh
# description : removes old public key for a host in $HOME/.ssh/known_kosts
# usage       : ramssh <hostname>
#               ramssh -n <line>
#               ramssh [-l] [-h]
# ----------------------------------------------------------------------
# (c) Kai Hildebrandt <kai.hildebrandt@web.de>
# ----------------------------------------------------------------------
# my name
me="ramssh"

function usage()
{
cat <<EOF
usage: ${me} hostname [hostname ...]
removes the given hostnames from the file ${known_hosts}.

possible options:
  -l, --list  list hosts in ${known_hosts}
  -n <line>   remove specific line
  -h, --help  display this help text
EOF
}

known_hosts="${HOME}/.ssh/known_hosts"

if [ ! -r "${known_hosts}" ]; then
    echo "${me} error: file ${known_hosts} does not exist or is unreadable"
    exit 1
fi

if [ $# -eq 0 ]; then
    usage
    exit 1
fi

# parse parameters

set -- $( getopt -u 'hln:' "$@" )
hosts2remove=""
line=-1

while [ $# -gt 0 ]
  do
  case "$1" in
      -h)
          usage
          exit 0
          ;;

      -l)
          awk -F" " '{ print $1 }' "${known_hosts}"
          exit 0
          ;;

      -n)
          shift; line=$1; shift;
 	       ;;

       *)
          hosts2remove="${hosts2remove} $1"
          shift
         ;;
  esac
done

if [ ${line} -ne -1 ]; then
    if [ ${line} -lt 1 -o ${line} -gt $( cat "${known_hosts}" | wc -l )  ]; then
        echo "${me} error: illegal value for line: ${line}"
        exit 1
    fi
    cp "${known_hosts}" "${known_hosts}.bak"
    sed -e "${line}d" "${known_hosts}.bak" > "${known_hosts}"
    echo "${me}: removed line ${line} from ${known_hosts}"
    exit 0
fi

if [ ! -z "${hosts2remove}" ]; then
    egrep_pat="("
    for host in ${hosts2remove}; do
        egrep_pat="${egrep_pat}(^|,)${host}[[:blank:],]| 
    done
    egrep_pat="${egrep_pat%|*})"
fi

mv -f "${known_hosts}" "${known_hosts}.bak"

egrep -v "${egrep_pat}" "${known_hosts}.bak" > "${known_hosts}"

lines=$(( $(cat "${known_hosts}.bak" | wc -l) - $(cat "${known_hosts}" | wc -l) ))
echo "${me}: removed ${lines} lines from ${known_hosts}"

Mit diesem Skript kann man mit dem Aufruf von

$ ramssh def.example.com

oder alternativ, bei gesetztem 'HashKnownHosts', über die in der Warnmeldung angegebenen Zeile (rot)

$ ramssh -n 4
$ ramssh -n 11

den gecachten Host-Key für das Remotesystem 'def.example.com' aus der known_hosts-datei löschen. Bei der nächsten Verbindung wird wie bei jeder Erstverbindung zu einem SSH-Server der Fingerabdruck zur Prüfung angezeigt:

$ ssh def
The authenticity of host 'def.example.com (192.168.101.23)' can't be established.
RSA key fingerprint is 12:34:56:78:9a:bc:de:f0:12:34:45:56:67:78:9a:bc.
Are you sure you want to continue connecting (yes/no)?

Mit 'yes' bestätigt man, den Fingerabdruck geprüft und sichergestellt zu haben, dass dieser auch zu dem Remotesystem gehört.

« nach oben

Zusammenfassung

Wenn man sich näher mit SSH und seinen Tools beschäftigt mehr man erst, was für ein mächtiges Werkzeug man hier vor sich hat. Ich hoffe, dass die hier angebotenen Tipps und Skripte hilfreich sind und freue mich jederzeit über Feedback dazu. :-)

Hier nochmal die drei Skripte auf einen Blick:

  • connect2 - Verbinden auf ein Fremdsystem nach DNS- und Ping-Prüfung
  • Makefile zum Erzeugen einer SSH-Konfigurationsdatei $HOME/.ssh/config aus Dateifragmenten. Das Makefile kann auch für andere Konfigurationsdateien angepasst werden.
  • ramssh - Shell-Skript, das nicht mehr gültige Host-Keys aus der known_hosts-Datei entfernt, entweder über den Hostname oder falls dieser mit der Option 'HashKnownHosts yes' nicht lesbar ist, über Angabe der zu löschenden Zeile.

Detaillierte Informationen findet man in den Manpages zu ssh_config(5) für den Client bzw. sshd_config(5) für den Server.

« nach oben

« zurück (SSH Teil 1)

« zurück (Gesammelte Anleitungen)