QVINTVS · SCRIBET

Mit SSH Löcher in Firewalls stanzen

SSH ist ja so etwas wie eine unerschöpfliche Quelle interessanter Erfahrungen. Ständig entdeckt man neue Features. Heute: Wie man per SSH rückwärts durch eine Firewall bricht.

Ausgangssituation

Ein Rechner steht hinter einer Firewall, und auf diesem laufen einige Dienste, die man gern von außerhalb der Firewall erreichen würde. Grundsätzlich besitzt man SSH-Zugriff auf den Rechner, allerdings wird von der Firewall jeglicher eingehender Verkehr auf den Rechner geblockt, sodass von außerhalb der Firewall keine Verbindungsaufnahme möglich ist. Umgekehrt kann man jedoch von hinter der Firewall Verbindungen aufbauen.

Lösungsweg

Da es nicht möglich ist, von außen durch die Firewall zu brechen, man selber aber auch nicht direkt hinter ihr sitzt, ist die Idee grundsätzlich, den Zielrechner, bevor er hinter die Firewall gestellt wird, so zu präparieren, dass er selbst ene Verbindung zu einem Rechner „auf der anderen Seite“ öffnet und anschließend über diese Verbindung Befehle entgegennimmt. Bei etwaigen Verbindungsabbrüchen soll diese Verbindung so schnell wie möglich wieder aufgebaut werden.

Befolgen des Lösungswegs

Erst dachte ich, ich müsste mühsam einen eigenen Dienst schreiben, der diese Aufgabe erfüllt und Befehle von mir entegegennimmt, aber erfreulicherweise verfügt SSH über ein Feature namens „Remote Port Forwarding“, mit dem man einen beliebigen Port am lokalen Rechner auf einen anderen (fast) beliebigen Port an einem entfernten Rechner (im Folgenden beispielhaft remote.example.com) weiterleiten kann1. Ich stelle mir das gelegentlich als „Reverse-Forwarding“ bzw. „Backwarding“ vor, denn effektiv ermögliche ich einem entfernten Rechner rückwärts über die SSH-Verbindung auf meinen Rechner zuzugreifen.

Obwohl es natürlich möglich und aus Sicherheitsgründen empfehlenswert ist, über diese Technik nur den/die Dienst/e zu tunneln, auf die man zugreifen möchte, ist es natürlich genauso möglich, den lokalen SSH-Dienst auf einen entfernten Rechner zu tunneln, denn aus Sicht von SSH ist SSH auch nur ein lokaler Dienst von vielen. Grundsätzlich funktioniert das ganze mithilfe der -R-Option:

$ ssh user@remote.example.com -R remoteport:host:localport

Dabei sind die Werte für user und remote.example.com natürlich entsprechend zu ersetzen. Hinter dem -R folgt die Spezifikation, welcher lokale Port (localport) an welchen entfernten Port (remoteport) weitergeleitet werden soll. host gibt an, an welchen Rechner man den Port binden möchte (aus Sicht von remote.example.com!), was man in 98% der Fälle auf localhost setzen möchte.

Konkret kann man den SSH-Dienst des lokalen Rechners, sofern er auf dem Standardport 22 läuft, wie folgt weiterleiten:

$ ssh user@remote.example.com -R 1234:localhost:22

Danach könnt ihr euch von einem beliebigen Rechner auf remote.example.com anmelden und danach dort mithilfe von

$ ssh otheruser@localhost -p 1234

auf den weiterleitenden Port 1234 zugreifen, auf dem der SSH-Dienst des ursprünglichen Rechners läuft. Obwohl ich in der SSH-Manpage nichts entsprechendes finden konnte, hat sich die -R-Option auch als recht robust gegenüber Verbindungsabbrüchen bewiesen (getestet durch einfaches Ziehen des LAN-Kabels am weiterleitenden Rechner und späterem Wiedereinstecken). Während die Verbindung unterbrochen ist, sind selbstverständlich keine Anmeldungen von remote.example.com möglich, aber SSH stellt die Weiterleitung recht schnell wieder her, nachdem die Verbindung wieder aufgebaut werden kann. Das SSH-Kommando beendet sich nicht einmal, sondern führt den Reconnect vollkommen transparent durch.

Soweit das grundsätzliche Vorgehen. Das ganze lässt sich noch etwas weiter ausschmücken und automatisieren, um den Prozess zu vereinfachen. Zunächst ist es empfehlenswert, auf dem Rechner hinter der Firewall ein SSH-Schlüsselpaar ohne Passwort zu erstellen und den öffentlichen Schlüssel auf remote.example.com zu hinterlegen, sodass der Verbindungsaufbau ohne Passwortabfrage geschehen kann (Sicherheitshinweis: Jeder, der Zugriff auf diesen Rechner hat, hat damit auch automatisch Zugriff auf remote.example.com!2). Danach kann man den eigentlichen SSH-Befehl noch etwas verfeinern:

ssh user@remote.example.com -R 1234:localhost:22 -N -o ExitOnForwardFailure=yes -f

-N weist SSH an, kein Kommando auf dem entfernten Rechner auszuführen, was praktisch ist, weil wir ohnehin nur Ports weiterleiten wollen. -o ExitOnForwardFailure=yes aktiviert die ExitOnForwardFailure-Option, die SSH anweist, mit einem Errorstatus zu beenden, wenn die Portweiterleitung nicht aufgebaut werden kann (standardmäßig wird nur eine Warnung auf der Standardfehlerausgabe ausgegeben und der Befehl läuft weiter, was wegen -N ziemlich sinnlos ist). -f schließlich instruiert SSH zu *f*orken, sprich, sich in den Hintergrund zurückzuziehen, wenn der Verbindungsaufbau erfolgreich war (anderenfalls endet der Befehl sofort mit einem Errorstatus). Somit ist es nicht erforderlich, eine ständige Verbindung zu dem weiterleitenden Rechner aufrecht zu erhalten. Alternativ kann man ExitOnForwardFailure auch über die Konfigurationsdatei ~/.ssh/config festlegen.

Ziel erreicht

Das ganze könnte man nun in einen Systemd-Service (oder etwas Ähnliches für Upstart, SysV-Init, etc.) wrappen und automatisch beim Booten starten lassen, nachdem das Netzwerk hochgefahren und verbunden ist. Ist dies getan, so kann man den Rechner getrost hinter die Firewall stellen und hat trotzdem noch Zugriff auf den Rechner, indem man sich einfach auf remote.example.com verbindet und von dort über Port 1234 weiter auf den Zielrechner.

Valete.

1 Es gelten die üblichen Verdächtigen: Für Ports < 1024 muss man sich auf remote.example.com als root anmelden, was man nach Möglichkeit vermeiden sollte; außerdem darf der gewünschte Port, in diesem Posting beispielhaft 1234, natürlich nicht schon von einem Dienst belegt sein.

2 Obwohl ich es nicht getestet habe, sollte es möglich sein, den Nutzer des weiterleitenden Rechners bei der SSH-Anmeldung auf remote.example.com in ein Chroot zu stecken, um die Auswirkungen eines Fremdzugriffs auf den weiterleitenden Rechner zu minimieren. Schließlich möchte man nicht gleich einen Server, den man für diesen Anwendungsfall höchstwahrscheinlich benutzen wird, schutzlos dem Shellzugriff unbekannter Dritter überlassen, selbst wenn es nicht Root-Zugriff ist…