Zum Inhalt springen
Serverküche
Suche

Die Suche wird geladen … (nur in der veröffentlichten Seite verfügbar).

Sicherheit Schwierigkeit: Fortgeschritten

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.

· 10 Min. Lesezeit ·Dauer: ca. 40 Minuten
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

Alles, was hier verändert wird, kann dich von deinem Server aussperren. Halte deshalb während der gesamten Einrichtung eine zweite SSH-Sitzung offen, in der du bereits angemeldet bist – eine bestehende Verbindung bleibt bestehen, auch wenn du die Anmeldung kaputt konfigurierst. Damit kannst du jede Änderung zurücknehmen. Wer zusätzlich eine Konsole beim Anbieter hat (bei netcup die VNC-Konsole im SCP), ist auf der sicheren Seite.

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"
🍳 Empfehlung Anzeige

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.

Zu netcup →

💶 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

Alle netcup-Gutscheine →

Schritt für Schritt

1. PAM-Modul installieren

Den TOTP-Code prüft ein PAM-Modul, nicht der SSH-Server:

Terminal
sudo apt update && sudo apt install libpam-google-authenticator

Der 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:

Terminal
sudo apt install pamtester

2. 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:

Terminal
google-authenticator -t -d -f -r 3 -R 30 -W

Die Schalter sind bewusst gesetzt und ersparen dir fünf Rückfragen. Eine bleibt, und das ist gut so – dazu gleich mehr:

  • -t zeitbasierte Codes (TOTP) statt zählerbasierter
  • -d verbietet die Wiederverwendung eines Codes – gegen Mitlesen
  • -f schreibt die Datei, ohne vorher nachzufragen, ob sie angelegt werden soll
  • -r 3 -R 30 Ratenbegrenzung: höchstens 3 Versuche pro 30 Sekunden
  • -W engeres 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:

Terminal
ls -l ~/.google_authenticator && grep '^"' ~/.google_authenticator
Ausgabe
-r-------- 1 koch koch 135 Jul 25 09:14 /home/koch/.google_authenticator
" RATE_LIMIT 3 30
" WINDOW_SIZE 3
" DISALLOW_REUSE
" TOTP_AUTH

Rechte 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:

INI
auth required pam_google_authenticator.so nullok

Das 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:

Terminal
echo "KbdInteractiveAuthentication yes" | sudo tee /etc/ssh/sshd_config.d/60-totp.conf

Konfiguration prüfen, bevor du neu lädst – ein Syntaxfehler kostet dich sonst den Zugang:

Terminal
sudo sshd -t && sudo systemctl reload ssh

Kontrolle, was jetzt wirklich gilt:

Terminal
sudo sshd -T | grep -E "kbdinteractive|usepam"
Ausgabe
usepam yes
kbdinteractiveauthentication yes

5. 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:

INI
# /etc/ssh/sshd_config.d/61-totp-user.conf
Match User DEIN_TESTBENUTZER
    AuthenticationMethods publickey,keyboard-interactive
Terminal
sudo sshd -t && sudo systemctl reload ssh

Was jetzt für wen gilt, verrät sshd -T mit Benutzerkontext – ein Kommando, das dir viel Rätselraten erspart:

Terminal
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 authenticationmethods
Ausgabe
authenticationmethods publickey,keyboard-interactive
authenticationmethods any

Genau 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:

Terminal
sudo pamtester -v sshd DEIN_TESTBENUTZER authenticate
Ausgabe
pamtester: invoking pam_start(sshd, koch2, ...)
pamtester: performing operation - authenticate
Verification code: Password: pamtester: successfully authenticated

Zwei 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:

Ausgabe
Verification code: Password: pamtester: Authentication failure

Erst 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:

INI
auth required pam_google_authenticator.so nullok
#@include common-auth

Danach 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

Diese eine Zeile wirkt auf alle SSH-Anmeldungen. Nachgeprüft: Nach dem Auskommentieren wurde sogar eine Anmeldung mit korrektem Passwort abgewiesen. Wer sich bisher irgendwo mit Passwort anmeldet – ein Kollege, ein Deployment-Skript, dein Handy-Client – kommt ab sofort nicht mehr rein. Mach diesen Schritt nur, wenn alle Zugänge auf Schlüssel umgestellt sind, und mit offener zweiter Sitzung.

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.

INI
# /etc/ssh/sshd_config.d/60-totp.conf
KbdInteractiveAuthentication yes
AuthenticationMethods publickey,keyboard-interactive
INI
# /etc/pam.d/sshd – ohne nullok, damit niemand den Faktor umgehen kann
auth required pam_google_authenticator.so

Und 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:

INI
Match User deploy
    AuthenticationMethods publickey

Ein 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