2018 habe ich meinen ersten eigenen Server gemietet. Ein Ubuntu-VPS, ein Root-Passwort, keine weiteren Gedanken. Zwei Wochen später zeigte fail2ban die Bilanz: 2.847 fehlgeschlagene SSH-Logins in 14 Tagen, die meisten aus IPs in China und Russland. Kein einziger wäre durchgekommen: Das Passwort war stark und lang. Trotzdem hat mich die Zahl wach gehalten. Passwörter sind nicht das schwächste Glied, weil sie zu kurz sind, sondern weil wir sie überall wiederverwenden. Dasselbe Passwort, das damals meinen Server schützte, hatte ich Jahre zuvor schon in ein Forum gekippt. Heute läuft auf jedem Dienst, den ich selbst betreibe, ein zweiter Faktor. Dieser Artikel zeigt, was TOTP wirklich tut, wo es wehtut und wie du es einführst, ohne dich auszusperren.

TOTP steht für Time-based One-Time Password, und die Grundidee passt in einen Satz: Server und Authenticator-App teilen sich ein Geheimnis, rechnen aus demselben Zeitwert denselben sechsstelligen Code aus, und wer den Code kennt, beweist, dass er das Geheimnis kennt. Der Clou ist die Zeit. Der Code gilt nur 30 Sekunden, dann rechnen beide Seiten einen neuen aus. Ein abgefangener Code ist wertlos, bevor du ihn fertig abgetippt hast.

Unter der Haube: HMAC-SHA1, Truncation und das Zeitfenster

Der Standard heißt RFC 6238 und baut auf HMAC auf, einem Verfahren, das aus einem Schlüssel und einer Nachricht einen Prüfwert berechnet. Bei TOTP ist die Nachricht ein Zähler, der aus der Uhrzeit entsteht: die Unix-Zeit in Sekunden, geteilt durch 30, abgerundet. Gerade eben war dieser Zähler bei mir bei 1.723.456.789 oder so, die genaue Zahl ist egal. Wichtig ist nur, dass er sich alle 30 Sekunden ändert und dass er sich für Server und App exakt gleichzeitig ändert.

Das Geheimnis, der TOTP-Secret, besteht aus 20 zufälligen Bytes, also 160 Bit Entropie. In der Praxis siehst du ihn als 32 Zeichen langen Base32-String, so etwas wie JBSWY3DPEHPK3PXP. Die App speichert ihn, der Server speichert ihn, und keiner der beiden schickt ihn je wieder über die Leitung. Ich erzeuge Secrets mit openssl rand -base64 20, wer lieber klickt, nimmt den bitcalc Passwort-Generator: 32 Zeichen aus dem Base32-Alphabet sind exakt ein 160-Bit-Secret. Wenn ein Secret in einer Datei landet, bilde ich mit dem bitcalc Hash-Generator die SHA-256-Summe und lege sie separat ab. So sehe ich sofort, wenn an der Datei manipuliert wurde, bevor ich sie irgendwo einspiele.

Aus Secret und Zähler rechnet HMAC-SHA1 einen 20 Byte langen Hash. SHA1 klingt nach 1995 und ist hier trotzdem richtig: Der Hash wird nie vollständig übertragen, und die bekannten Schwächen von SHA1 betreffen Kollisionen, nicht die Vorhersagbarkeit, die einem Angreifer hier helfen würde. Aus dem Hash werden am Ende sechs Ziffern. Dafür liest das Verfahren die letzten vier Bits als Offset: eine Zahl zwischen 0 und 19, die festlegt, ab welcher Stelle vier Bytes extrahiert werden. Aus diesen vier Bytes entsteht eine 31-Bit-Zahl, und der Code ist der Rest bei Division durch 10^6. Sechs Ziffern sind also keine Willkür, sondern der bequemste Ausschnitt aus einer Million möglicher Codes.

Genau dort greift die Sicherheit. Ein Angreifer, der einen Code abgefangen hat, hat 30 Sekunden, um ihn zu benutzen. Ein Angreifer, der rät, bekommt pro Fenster nur ein paar Versuche, bevor der Dienst sperrt. Bei drei Versuchen pro Fenster braucht er für alle eine Million Kombinationen rechnerisch rund 116 Tage. Das Fenster von 30 Sekunden ist der Kompromiss zwischen beiden Welten: kurz genug, um Codes nutzlos zu machen, lang genug, um sie in Ruhe abzutippen.

2FA für SSH: pam_oath und die Alternativen

Der klassische Weg für SSH ist pam_oath, ein PAM-Modul, das TOTP direkt in den Login einbaut. Die Einrichtung dauert zehn Minuten und sieht so aus:

# 1. Modul installieren
apt install libpam-oath

# 2. Secret erzeugen (hex!) und eintragen
openssl rand -hex 20
echo "HOTP/T30 admin 5c1f2a9b8e7d6c4f3a2b1e0d9c8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f" \
     >> /etc/security/users.oath

# 3. PAM aktivieren, Datei: /etc/pam.d/sshd
auth required pam_oath.so usersfile=/etc/security/users.oath window=30

# 4. SSH auf zwei Faktoren stellen, Datei: /etc/ssh/sshd_config
AuthenticationMethods publickey,keyboard-interactive

Zwei Details entscheiden über den Erfolg. Erstens die Reihenfolge: publickey,keyboard-interactive mit Komma bedeutet, dass einer der beiden Faktoren reicht. Wer beide will, schreibt die Methoden durch ein Leerzeichen getrennt, dann müssen SSH-Key und TOTP-Code in jeder Session kommen. Ich fahre die Komma-Variante, damit ich mich im Notfall mit Key allein retten kann, wenn der OATH-Dienst mal klemmt. Zweitens der window-Parameter: Er akzeptiert Codes aus dem vorherigen und dem nächsten Fenster, damit kleine Uhr-Abweichungen und schnelles Abtippen nicht zu Fehlschlägen führen. Zwei Schritte Toleranz sind genug, mehr öffnet nur das Tor für abgefangene Codes.

Die Alternativen zu pam_oath sind zwei: YubiKeys über pam_u2f und die ed25519-SK-Variante von OpenSSH. Beide machen den Login sogar phishing-resistent, weil der Key den Hostnamen in die Signatur einbezieht. Ein YubiKey ist für Leute gedacht, die keinen Code abtippen wollen. Meine Mutter bekommt keinen Authenticator-String hin, aber sie steckt einen YubiKey in den USB-Port. Auf meinem Pi und den VPS läuft trotzdem pam_oath, weil es keine Hardware braucht und in fünf Minuten eingerichtet ist. Was viele vergessen: Ein zweiter Faktor schützt den Login, nicht die Session. Wer deinen SSH-Key klaut, braucht keinen TOTP-Code. Keys gehören auf Hardware oder mindestens passwortgeschützt in den Agent.

Web-Apps und Mail: TOTP dort, wo es hingehört

Für Web-Apps ist TOTP der Standard, weil es überall gleich funktioniert. Nextcloud, Grafana, Proxmox, Cockpit, alle können es, die meisten out of the box. Wer viele Dienste hat, hängt einen zentralen Login wie Authelia oder authentik davor und stellt 2FA genau einmal ein. Ich betreibe Authelia vor Grafana, Portainer und zwei Kundendashboards. Ein Dienst ist bewusst ausgenommen: der Prometheus-Endpoint. Der wird maschinell abgefragt, und ein sechsstelliger Code, der alle 30 Sekunden wechselt, macht dort nur kaputt, was vorher funktioniert hat. Maschinelle Zugriffe gehören nicht in TOTP, die gehören in API-Keys mit eingeschränkten Rechten.

Mail ist der kniffligste Fall, weil SMTP kein TOTP kennt. Dein Mailserver muss sich bei fremden Servern mit Benutzername und Passwort melden, sonst landet alles im Spam. Absichern kannst du den Zugang zum Postfach: Webmail mit TOTP, IMAP mit App-Passwörtern statt des Klartext-Passworts und ein Master-Passwort, das nur auf dem Server liegt. Mein Mailclient benutzt ein App-Passwort, das ich einmal erzeugt und im Passwort-Manager vergraben habe. Das eigentliche Postfach-Passwort kennt niemand, es steckt nur im TOTP-Setup des Webmailers. So kommt selbst ein gestohlenes Laptop nicht direkt an die Mails.

Backup-Codes sind wichtiger als die App auf deinem Handy

Hier ist die Geschichte, die diesen Abschnitt erklärt. Im Frühjahr 2024 bin ich umgezogen. Neues Handy, neue SIM, alles wie gehabt. Am Abend wollte ich mich im Nextcloud-Admin einloggen, und der Login verlangte den TOTP-Code. Mein altes Handy mit dem Authenticator lag in einer Umzugskiste, die ich an diesem Abend nicht finden konnte, und das neue war noch nicht eingerichtet. Ohne Backup-Codes wäre ich aus meiner eigenen Cloud ausgesperrt gewesen, und der einzige Weg zurück hätte über einen Admin-Reset geführt, der fremde Sessions killt und Logs verliert. Gerettet hat mich ein Zettel mit zehn Codes, den ich vor zwei Jahren ausgedruckt und in den Aktenschrank gelegt hatte. Zehn Minuten später war ich drin und hatte neue Codes gedruckt.

Deshalb meine Reihenfolge: Backup-Codes zuerst, App danach. Die App ist ersetzbar, ein Handy geht kaputt, wird gestohlen oder liegt in einer Umzugskiste. Die Codes sind das einzige Artefakt, das dich durch genau diese Situation trägt. Zehn Codes pro Dienst sind Standard und genug, du verbrauchst im Jahr vielleicht einen. Ein Code ist genau einmal nutzbar, danach streichst du ihn durch.

Wohin damit? An zwei Orte. Ein Zettel in einem Umschlag im Aktenschrank oder Tresor und eine Kopie im Passwort-Manager. Was ich niemals mache: die Codes in der Cloud desselben Dienstes ablegen, zu dem sie gehören. Ein Einbruch in deinen Nextcloud-Account darf nicht die Codes für genau diese Nextcloud mitliefern. Wer die Liste digital ablegt, hash sie vorher mit dem bitcalc Hash-Generator und lege den Hash separat ab, dann erkennst du jede Manipulation.

Und die letzte Regel: verbrauchte Codes sofort ersetzen. Ich drucke eine frische Liste, sobald zehn Codes weg sind. Der Tag, an dem du deinen letzten Code benutzt, ist garantiert der Tag, an dem dein Handy verloren geht.

Backup-Codes sicher ablegen: Zehn Codes pro Dienst, an zwei Orten: ein Zettel im Aktenschrank oder Tresor, eine Kopie im Passwort-Manager. Niemals in der Cloud des Dienstes, den die Codes schützen. Verbrauchte Codes streichen und ersetzen, bevor die Liste leer ist.

Das Zeitproblem: Drift, falsche Uhren und der Server ohne NTP

TOTP steht und fällt mit der Uhr. Server und App rechnen aus demselben Sekundenwert denselben Code, und wenn eine Seite fünf Minuten danebenliegt, sind zehn Fenster vergangen und kein einziger Code stimmt. Das Symptom ist immer dasselbe: Der Code wird abgelehnt, obwohl du ihn gerade eben aus der App abgetippt hast.

Die meisten Authenticator-Apps tolerieren ein Fenster nach vorn und eins nach hinten, manche zwei. Danach hilft nur die Uhr. Auf Servern heißt die Lösung NTP, konkret systemd-timesyncd oder chrony. Ich hatte einmal einen KVM-Server, dessen BIOS-Uhr bei jedem Reboot zwei Minuten nachging, weil die RTC-Batterie leer war. Zwei Monate habe ich geglaubt, mein TOTP-Setup sei kaputt, bis ich hwclock -s ausführte und der nächste Code auf Anhieb funktionierte. Der eigentliche Fehler: Der Server holte nach dem Reboot nie die Netzwerkzeit. Seitdem schaue ich nach jedem Reboot in timedatectl und will die Zeile System clock synchronized: yes sehen.

Auf dem Client ist die Uhr selten das Problem, aber zwei Fälle gibt es: Dual-Boot-Rechner, bei denen Windows die Hardware-Uhr auf Lokalzeit stellt und Linux auf UTC, und Container mit eingefrorener Uhr. Docker-Container erben die Host-Uhr, also reicht es, den Host zu richten. Und wenn Uhr und Code stimmen und trotzdem nichts klappt, ist es fast immer das Secret: ein abgetippter Base32-String mit einem falschen Zeichen. Prüfen, neu einlesen, fertig.

Die Einführung in fünf Schritten

So habe ich den letzten Dienst umgestellt, ohne dass irgendjemand draußen stand:

  1. Uhr prüfen: timedatectl zeigt System clock synchronized: yes, sonst chrony oder timesyncd einrichten.
  2. Secret erzeugen: openssl rand -base64 20, den Base32-String mit dem Passwort-Generator gegenprüfen, falls du ihn abtippen musst.
  3. Zweiten Faktor einrichten: App oder Hardware-Key, QR-Code scannen, einen Code testen.
  4. Backup-Codes drucken und ablegen: zehn Codes, zwei Orte, siehe Kasten oben.
  5. Zweiten Faktor erzwingen: erst nach ein paar Tagen Testbetrieb, damit sich niemand aussperrt.

Die Reihenfolge ist der Punkt. Wer mit Schritt 5 anfängt, sperrt sich garantiert selbst aus. Wer Schritt 4 überspringt, verliert beim ersten kaputten Handy den Zugang. Wer Schritt 1 überspringt, rennt zwei Monate lang einem Zeitproblem hinterher, das gar keins war. Ich weiß das alles aus eigener Erfahrung.

Fazit

TOTP ist das beste Verhältnis aus Sicherheit und Aufwand, das ich für selbst betriebene Dienste kenne. Kein Zertifikats-Wildwuchs, kein zweiter Server, keine laufenden Kosten. Ein Secret, eine Uhr, sechs Ziffern. Die Mathematik dahinter ist simpel genug, um sie zu verstehen, und robust genug, um seit 2011 unverändert zu funktionieren. RFC 6238 ist einer der wenigen Standards, die ich auswendig erklären kann, ohne nachzusehen.

Was ich dir mitgeben will: Fang klein an. Ein Dienst, ein Secret, zehn Backup-Codes. SSH mit pam_oath, dann Nextcloud, dann der Rest. Die 116 Tage Brute-Force-Widerstand sind ein schönes Gefühl, aber der echte Gewinn zeigt sich im fail2ban-Log: kein einziger Treffer mehr, denn Passwort allein reicht nicht mehr. Und wenn du irgendwann vor einem Login stehst und die App auf dem alten Handy in einer Umzugskiste liegt, dann sind die zehn Zettel-Codes dein bester Freund.