SSH mit Zwei-Faktor-Authentifizierung (TOTP) absichern
SSH-Login mit Schlüssel plus Einmalcode absichern: libpam-google-authenticator einrichten, PAM und sshd korrekt konfigurieren – ohne sich dabei auszusperren.
Inhaltsverzeichnis
Ein SSH-Schlüssel ist stark – solange niemand deinen Laptop hat. Ein zweiter Faktor schließt genau diese Lücke: Wer sich anmeldet, braucht den Schlüssel und einen sechsstelligen Code aus der Authenticator-App. Wir bauen das schrittweise ein – und legen von Anfang an ein Netz, damit du dich nicht selbst aussperrst.
Was bauen wir?
Am Ende verlangt dein Server für die SSH-Anmeldung zwei unabhängige Faktoren: den privaten Schlüssel und einen zeitbasierten Einmalcode (TOTP) aus einer App wie Aegis, FreeOTP oder Google Authenticator. Der Code wird von PAM geprüft, nicht von SSH selbst – deshalb müssen zwei Konfigurationen zusammenpassen.
Aufgebaut und geprüft auf Debian 13 mit OpenSSH 10.0p2 und
libpam-google-authenticator 20191231-2.1. Wir gehen dabei in zwei Stufen vor: erst ein einzelner
Testbenutzer, dann alle. Und wir prüfen jeden Schritt, bevor die bestehende Anmeldung wegfällt.
Die Regel für diesen Artikel: zweite Sitzung offen lassen
Voraussetzungen
- Ein Server mit Debian 13, auf dem der SSH-Zugang schon abgesichert ist: Anmeldung per Schlüssel, kein Root-Login mit Passwort
- Eine Authenticator-App auf dem Handy. Empfehlung: Aegis (Android) oder FreeOTP – beide offen und ohne Cloud-Zwang
- Zugriff auf eine Notfall-Konsole beim Anbieter (VNC im netcup SCP), falls doch etwas schiefgeht
- Etwa 40 Minuten Zeit, in Ruhe – das ist kein Schritt für „zwischendurch"
VPS 1000 G12.5
4 vCore · 8 GB RAM · 128 GB SSD
ab 14,50 €/Monat
Läuft auf jedem Server – die VNC-Konsole im SCP ist hier das eigentliche Sicherheitsnetz.
💶 1 Monat gratis:
Einmal einlösbar, gilt für VPS 1000 G12.5. Im leeren Warenkorb einlösen – der Code legt den Server selbst hinein. Im Warenkorb einlösen →
💶 5 € Gutschein für netcup-Neukunden: immer gültig · nicht für Domains und VPS Lite
Schritt für Schritt
1. PAM-Modul installieren
Den TOTP-Code prüft ein PAM-Modul, nicht der SSH-Server:
sudo apt update && sudo apt install libpam-google-authenticatorDer Name ist irreführend: Das Modul spricht das offene TOTP-Verfahren (RFC 6238) und braucht weder ein Google-Konto noch Internet. Jede Standard-Authenticator-App funktioniert.
Praktisch ist außerdem pamtester – damit prüfst du die Anmeldung, ohne dich per SSH neu
anmelden zu müssen:
sudo apt install pamtester2. Geheimnis für einen Benutzer erzeugen
Melde dich als der Benutzer an, der 2FA bekommen soll (nicht als root – das Geheimnis gehört in sein Home-Verzeichnis) und starte:
google-authenticator -t -d -f -r 3 -R 30 -WDie Schalter sind bewusst gesetzt und ersparen dir fünf Rückfragen. Eine bleibt, und das ist gut so – dazu gleich mehr:
-tzeitbasierte Codes (TOTP) statt zählerbasierter-dverbietet die Wiederverwendung eines Codes – gegen Mitlesen-fschreibt die Datei, ohne vorher nachzufragen, ob sie angelegt werden soll-r 3 -R 30Ratenbegrenzung: höchstens 3 Versuche pro 30 Sekunden-Wengeres Zeitfenster (weniger Toleranz, dafür sicherer)
Das Werkzeug zeigt einen QR-Code und das Geheimnis im Klartext. Scanne den QR-Code mit der App –
dann fragt es Enter code from app (-1 to skip):. Tipp den Code ein, den die App jetzt anzeigt:
Das ist der erste Test, ob App und Server dasselbe rechnen, und überspringen solltest du ihn nicht.
Erst danach schreibt das Werkzeug die Datei und zeigt die Notfallcodes. Schreib sie auf
Papier – sie sind der Weg zurück, wenn das Handy verloren geht.
Die Rückfrage ist auch der Grund, warum sich der Befehl nicht unbeaufsichtigt in einem Skript
ausführen lässt: Ohne Terminal bricht er mit getline(): Inappropriate ioctl for device ab und
legt keine Datei an.
Danach liegt die Konfiguration im Home-Verzeichnis:
ls -l ~/.google_authenticator && grep '^"' ~/.google_authenticator-r-------- 1 koch koch 135 Jul 25 09:14 /home/koch/.google_authenticator
" RATE_LIMIT 3 30
" WINDOW_SIZE 3
" DISALLOW_REUSE
" TOTP_AUTHRechte 0400 und Eigentümer der Benutzer selbst – so soll es sein. Wer die Datei lesen kann, kann
Codes erzeugen.
3. PAM konfigurieren – mit nullok als Netz
Öffne /etc/pam.d/sshd und setze als erste auth-Zeile ein:
auth required pam_google_authenticator.so nullokDas nullok ist der wichtigste Teil dieses Artikels: Es bedeutet „Benutzer ohne
~/.google_authenticator dürfen weiterhin ohne Code herein". Damit sperrst du niemanden aus,
während du 2FA ausrollst – und du selbst bleibst drin, solange du kein Geheimnis angelegt hast.
nullok gehört am Ende wieder weg
nullok ist eine Rampe, kein Zustand: Solange es dort steht, kann jeder Benutzer den zweiten
Faktor umgehen, indem er einfach kein Geheimnis anlegt. Wenn alle Konten eingerichtet sind, entferne
das Wort – aber erst dann, und prüfe vorher mit pamtester (Schritt 6) für jeden Benutzer, dass
die Anmeldung funktioniert.4. sshd: Tastatur-interaktive Anmeldung erlauben
SSH fragt den Code über den Mechanismus „keyboard-interactive" ab, und der ist auf Debian 13 standardmäßig aus. Statt die Hauptdatei zu editieren, legst du eine Ergänzung an – so bleibt sie bei Paket-Updates unangetastet:
echo "KbdInteractiveAuthentication yes" | sudo tee /etc/ssh/sshd_config.d/60-totp.confKonfiguration prüfen, bevor du neu lädst – ein Syntaxfehler kostet dich sonst den Zugang:
sudo sshd -t && sudo systemctl reload sshKontrolle, was jetzt wirklich gilt:
sudo sshd -T | grep -E "kbdinteractive|usepam"usepam yes
kbdinteractiveauthentication yes5. Zwei Faktoren erzwingen – zuerst für einen Benutzer
Jetzt kommt der eigentliche Schalter: AuthenticationMethods verlangt mehrere Verfahren
hintereinander. Damit ein Fehler nicht alle Konten trifft, gilt die Regel zunächst nur für einen
Testbenutzer:
# /etc/ssh/sshd_config.d/61-totp-user.conf
Match User DEIN_TESTBENUTZER
AuthenticationMethods publickey,keyboard-interactivesudo sshd -t && sudo systemctl reload sshWas jetzt für wen gilt, verrät sshd -T mit Benutzerkontext – ein Kommando, das dir viel Rätselraten
erspart:
sudo sshd -T -C user=DEIN_TESTBENUTZER,host=localhost,addr=127.0.0.1 | grep authenticationmethods
sudo sshd -T -C user=root,host=localhost,addr=127.0.0.1 | grep authenticationmethodsauthenticationmethods publickey,keyboard-interactive
authenticationmethods anyGenau das wollten wir: Der Testbenutzer braucht beides, alle anderen sind unberührt.
6. Prüfen, ohne sich auszusperren
Bevor du eine neue SSH-Sitzung wagst, teste den PAM-Stack direkt. pamtester fragt genau das, was
SSH auch fragen würde:
sudo pamtester -v sshd DEIN_TESTBENUTZER authenticatepamtester: invoking pam_start(sshd, koch2, ...)
pamtester: performing operation - authenticate
Verification code: Password: pamtester: successfully authenticatedZwei Beobachtungen, die gleich wichtig werden: Es wird zuerst der Code, dann das Passwort verlangt – die Reihenfolge ergibt sich aus der Position im PAM-Stack. Und mit falschem Code antwortet dasselbe Kommando:
Verification code: Password: pamtester: Authentication failureErst wenn das sitzt, öffnest du eine neue Sitzung (die alte offen lassen!) und meldest dich als Testbenutzer an. Der Ablauf: Schlüssel wird automatisch akzeptiert, dann fragt der Server nach dem Code.
7. Der Stolperstein: Code und Passwort?
In der Standardkonfiguration verlangt die Anmeldung nach dem Schlüssel beides – Einmalcode und
Passwort. Das liegt nicht an SSH, sondern daran, dass keyboard-interactive mit UsePAM yes den
gesamten PAM-Stack durchläuft: erst dein TOTP-Modul, dann über @include common-auth auch die
Passwortprüfung.
Im Test war das eindeutig: Schlüssel + richtiger Code + richtiges Passwort → Anmeldung erfolgreich;
Schlüssel + richtiger Code + falsches Passwort → Permission denied (keyboard-interactive).
Willst du Schlüssel + Code ohne Passwort (die üblichere Variante, weil das Passwort neben dem
Schlüssel wenig beiträgt), kommentiere in /etc/pam.d/sshd die Passwortprüfung aus:
auth required pam_google_authenticator.so nullok
#@include common-authDanach genügt Schlüssel + Code – im Test bestätigt, mit falschem Code weiterhin abgelehnt.
Ohne common-auth gibt es keine Passwort-Anmeldung mehr – für niemanden
8. Auf alle Benutzer ausweiten
Wenn der Testbenutzer sauber läuft und jedes Konto sein Geheimnis hat, machst du die Regel
allgemein: Ersetze den Match-Block durch eine globale Zeile und nimm nullok heraus.
# /etc/ssh/sshd_config.d/60-totp.conf
KbdInteractiveAuthentication yes
AuthenticationMethods publickey,keyboard-interactive# /etc/pam.d/sshd – ohne nullok, damit niemand den Faktor umgehen kann
auth required pam_google_authenticator.soUnd wieder: sudo sshd -t, neu laden, mit pamtester prüfen, dann eine neue Sitzung testen –
mit der alten weiterhin offen.
Automatisierung ausnehmen
Backup-Jobs, CI-Runner oder Deployments können keinen Code eintippen. Nimm sie gezielt aus, statt 2FA ganz zu lassen:
Match User deploy
AuthenticationMethods publickeyEin solcher Zugang sollte dann aber einen eigenen, eingeschränkten Schlüssel bekommen (siehe
command=-Option in authorized_keys) – nicht dieselben Rechte wie dein persönliches Konto.
Wenn es nicht funktioniert
Es kommt gar keine Code-Abfrage, die Anmeldung läuft wie vorher durch. Dann ist
KbdInteractiveAuthentication noch no – auf Debian 13 der Standard. Prüfe mit
sudo sshd -T | grep kbdinteractive, was wirklich gilt, und beachte: Dateien in
/etc/ssh/sshd_config.d/ werden in alphabetischer Reihenfolge gelesen, die erste Festlegung
gewinnt.
Nach dem Schlüssel wird zusätzlich das Passwort verlangt. Kein Fehler, sondern die Folge von
UsePAM yes: keyboard-interactive durchläuft den ganzen PAM-Stack, inklusive @include common-auth. Wer nur Schlüssel + Code will, kommentiert diese Zeile aus – mit der Konsequenz aus
der Warnung in Schritt 7.
Der Code wird abgelehnt, obwohl die App ihn gerade anzeigt. Meist läuft die Serveruhr
auseinander. TOTP toleriert nur wenige Sekunden. Prüfe timedatectl – System clock synchronized: yes muss dort stehen; sonst NTP aktivieren. Zweite Möglichkeit: Du hast denselben Code kurz zuvor
benutzt, und DISALLOW_REUSE verbietet das. Das gilt auch, wenn der vorige Versuch erst am
Passwort scheiterte – der Code war richtig und ist damit verbraucht. Im Test wurde derselbe
Code direkt danach mit korrektem Passwort abgelehnt, der nächste ging durch. Also einfach den
nächsten abwarten.
Nach drei Fehlversuchen geht gar nichts mehr. Das ist die Ratenbegrenzung aus -r 3 -R 30: drei
Versuche pro 30 Sekunden. Dabei zählt jede Runde: Nach einem falschen Passwort fragt der
SSH-Client bis zu dreimal neu, und schon das schöpft das Limit aus. Kurz warten, statt weiter zu
tippen.
Ein Benutzer kommt ohne Code rein. Entweder fehlt seine ~/.google_authenticator und nullok
lässt ihn passieren, oder für ihn greift ein Match-Block mit anderen AuthenticationMethods.
sudo sshd -T -C user=NAME,host=localhost,addr=127.0.0.1 | grep authenticationmethods zeigt, was für
ihn tatsächlich gilt.
Handy verloren, keine Notfallcodes. Dann hilft nur ein Zugang, der nicht über SSH läuft: die
VNC-Konsole beim Anbieter. Dort anmelden und die Datei des Benutzers löschen
(rm ~/.google_authenticator) – mit nullok kommt er dann wieder ohne Code herein, ohne es
entsteht ein Konto ohne zweiten Faktor, das du neu einrichten musst.
Dein Problem ist nicht dabei? Alle Fehlermeldungen durchsuchen →
Wartung & Backups
Notfallcodes gehören ins Backup – aber nicht auf den Server. Die fünf Codes aus Schritt 2 sind gleichwertig zum zweiten Faktor. Auf demselben Server gespeichert wären sie sinnlos; ins Passwortmanager-Tresor oder auf Papier in die Schublade gehören sie schon.
Die Geheimnisse selbst sichern – oder bewusst nicht. ~/.google_authenticator ist Teil des
Home-Verzeichnisses. Sicherst du Homes mit Restic, liegt der
zweite Faktor im Backup: bequem beim Wiederherstellen, aber ein Angreifer mit Backup-Zugriff hat
damit beide Faktoren. Für die meisten Selbsthoster ist das der akzeptable Kompromiss – die Frage
sollte man aber einmal bewusst entschieden haben, statt sie zu übersehen.
Nach jedem OpenSSH-Update kurz gegenprüfen. Optionen werden umbenannt (aus
ChallengeResponseAuthentication wurde KbdInteractiveAuthentication), und alte Namen verschwinden
irgendwann. Ein sudo sshd -T | grep -E "kbdinteractive|authenticationmethods" nach dem Update
zeigt in zwei Sekunden, ob deine Härtung noch greift oder stillschweigend unwirksam wurde. Wie du
Updates automatisiert einspielst, steht in
automatische Updates.
Was 2FA nicht leistet. Der zweite Faktor schützt die Anmeldung. Er hilft nicht gegen eine verwundbare Anwendung hinter deinem Reverse Proxy, gegen ein geleaktes Docker-Socket-Mount oder gegen einen ungepatchten Kernel. Er ist eine Schicht neben Firewall, fail2ban und CrowdSec – und er ersetzt keine davon.
Feedback per E-Mail: feedback@serverkueche.de
Das könnte dir auch schmecken


