<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Pam – Serverküche</title><link>https://serverkueche.de/tags/pam/</link><description>Pam – Neueste Beiträge von Serverküche</description><generator>Hugo</generator><language>de-DE</language><managingEditor>feedback@serverkueche.de (Serverküche)</managingEditor><webMaster>feedback@serverkueche.de (Serverküche)</webMaster><copyright>2026 Serverküche</copyright><lastBuildDate>Tue, 13 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://serverkueche.de/tags/pam/index.xml" rel="self" type="application/rss+xml"/><item><title>SSH mit Zwei-Faktor-Authentifizierung (TOTP) absichern</title><link>https://serverkueche.de/tutorials/ssh-2fa-totp/</link><pubDate>Tue, 13 Oct 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/tutorials/ssh-2fa-totp/</guid><description>SSH-Login mit Schlüssel plus Einmalcode absichern: libpam-google-authenticator einrichten, PAM und sshd korrekt konfigurieren – ohne sich dabei auszusperren.</description><content:encoded><![CDATA[<p>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 <strong>und</strong> 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.</p>
<h2 id="was-bauen-wir">Was bauen wir?</h2>
<p>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 <strong>PAM</strong> geprüft, nicht von SSH selbst – deshalb müssen zwei
Konfigurationen zusammenpassen.</p>
<p>Aufgebaut und geprüft auf <strong>Debian 13 mit OpenSSH 10.0p2</strong> und
<code>libpam-google-authenticator 20191231-2.1</code>. Wir gehen dabei in zwei Stufen vor: erst ein einzelner
Testbenutzer, dann alle. Und wir prüfen jeden Schritt, <strong>bevor</strong> die bestehende Anmeldung wegfällt.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-paprika-400 bg-paprika-50 dark:border-paprika-700 dark:bg-paprika-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🔥</span>Die Regel für diesen Artikel: zweite Sitzung offen lassen
  </p>
  <div class="prose-kitchen text-sm">Alles, was hier verändert wird, kann dich von deinem Server aussperren. Halte deshalb während der
gesamten Einrichtung <strong>eine zweite SSH-Sitzung offen</strong>, 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.</div>
</div>
<h2 id="voraussetzungen">Voraussetzungen</h2>
<ul>
<li>Ein Server mit Debian 13, auf dem der
<a href="/tutorials/ssh-absichern/">SSH-Zugang schon abgesichert</a> ist: Anmeldung per Schlüssel,
kein Root-Login mit Passwort</li>
<li>Eine Authenticator-App auf dem Handy. Empfehlung: <strong>Aegis</strong> (Android) oder <strong>FreeOTP</strong> – beide
offen und ohne Cloud-Zwang</li>
<li>Zugriff auf eine Notfall-Konsole beim Anbieter (VNC im netcup SCP), falls doch etwas schiefgeht</li>
<li>Etwa 40 Minuten Zeit, in Ruhe – das ist kein Schritt für „zwischendurch&quot;</li>
</ul>
<div class="not-prose my-6 overflow-hidden rounded-xl border border-paprika-200 bg-paprika-50 dark:border-paprika-800 dark:bg-paprika-900/20"
     data-track-content data-content-name="Affiliate-Box · /tutorials/ssh-2fa-totp/" data-content-piece="VPS 1000 G12.5">
  <div class="flex items-center justify-between border-b border-paprika-200 bg-paprika-100 px-4 py-1.5 text-xs font-semibold uppercase tracking-wide text-paprika-700 dark:border-paprika-800 dark:bg-paprika-900/40 dark:text-paprika-300">
    <span>🍳 Empfehlung</span>
    <span title="Mit * markierte Links sind Affiliate-Links.">Anzeige</span>
  </div>
  <div class="flex flex-col gap-4 p-4 sm:flex-row sm:items-center sm:justify-between">
    <div>
      <p class="text-lg font-bold text-slate-900 dark:text-white">VPS 1000 G12.5</p>
      <p class="mt-1 text-sm text-slate-600 dark:text-slate-300">4 vCore · 8 GB RAM · 128 GB SSD</p>
      <p class="mt-1 text-sm font-semibold text-paprika-700 dark:text-paprika-400">ab 14,50 €/Monat</p>
      <p class="mt-2 text-sm text-slate-600 dark:text-slate-400">Läuft auf jedem Server – die VNC-Konsole im SCP ist hier das eigentliche Sicherheitsnetz.</p>
    </div>
    <a href="https://www.netcup.com/de/server/vps/vps-1000-g12.5-24m-eu?ref=44083#vps-1000-g12.5-12m-eu" rel="sponsored noopener" target="_blank"
   data-track-event="Affiliate|netcup: Affiliate-Box|VPS 1000 G12.5 · {page}"
   class="inline-flex shrink-0 items-center justify-center rounded-lg bg-paprika-600 px-5 py-2.5 font-semibold text-white transition-colors hover:bg-paprika-700">
  Zu netcup →
</a>

  </div><div class="px-4 pb-4"><div class="not-prose my-3 rounded-lg border border-herb-500/40 bg-herb-50 px-3 py-2 text-sm text-slate-700 dark:bg-herb-900/20 dark:text-slate-200"
     data-voucher-api="vps-1000" data-voucher-msg-empty="Gerade ist für diesen Tarif kein Code frei – nimm den 5-€-Code." data-voucher-msg-limit="Für heute sind keine weiteren Codes drin – nimm den 5-€-Code oder versuch es morgen." data-voucher-msg-error="Der Code ließ sich gerade nicht abrufen – nimm den 5-€-Code.">
  <p class="flex flex-wrap items-center gap-x-2 gap-y-1"><span>💶 <span class="font-semibold">1 Monat gratis</span>:</span>
    <button type="button" data-voucher-reveal data-copy-title="Zum Kopieren klicken"
            class="cursor-pointer rounded bg-white px-2 py-0.5 font-mono text-sm font-semibold text-herb-800 hover:ring-2 hover:ring-herb-500/40 dark:bg-slate-800 dark:text-herb-400">Code anzeigen</button>
    <button type="button" data-voucher-next hidden
            class="rounded border border-herb-500/40 px-2 py-0.5 text-xs text-herb-800 hover:bg-white dark:text-herb-400 dark:hover:bg-slate-800">anderer Code</button>
  </p>
  <p class="mt-1 text-xs text-slate-500 dark:text-slate-400">
    Einmal einlösbar, gilt für VPS 1000 G12.5. Im leeren Warenkorb einlösen – der Code legt den Server selbst hinein.
    <a href="https://www.netcup.com/de/checkout/warenkorb?ref=44083" rel="sponsored noopener" target="_blank"
       data-track-event="Affiliate|netcup: Gutschein einlösen|VPS 1000 G12.5 · {page}"
       class="font-medium text-herb-800 underline underline-offset-2 hover:text-herb-900 dark:text-herb-400">Im Warenkorb einlösen →</a>
  </p><p class="mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 border-t border-herb-500/30 pt-2">
    <span>💶 <span class="font-semibold">5 € Gutschein</span> für netcup-Neukunden:</span>
    <button type="button" data-voucher-code data-track-voucher="36nc17844976032"
            title="Zum Kopieren klicken" class="cursor-pointer rounded bg-white px-2 py-0.5 font-mono text-sm font-semibold text-herb-800 hover:ring-2 hover:ring-herb-500/40 dark:bg-slate-800 dark:text-herb-400">36nc17844976032</button>
    <span class="text-xs text-slate-500 dark:text-slate-400">immer gültig · nicht für Domains und VPS Lite</span>
  </p><p class="mt-1 text-xs"><a href="/netcup-gutschein/" class="font-medium text-herb-800 underline underline-offset-2 hover:text-herb-900 dark:text-herb-400">Alle netcup-Gutscheine →</a></p>
</div></div>
</div>

<h2 id="schritt-für-schritt">Schritt für Schritt</h2>
<h3 id="1-pam-modul-installieren">1. PAM-Modul installieren</h3>
<p>Den TOTP-Code prüft ein PAM-Modul, nicht der SSH-Server:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt update <span class="o">&amp;&amp;</span> sudo apt install libpam-google-authenticator</span></span></code></pre></div>
</div>
<p>Der Name ist irreführend: Das Modul spricht das offene <strong>TOTP</strong>-Verfahren (RFC 6238) und braucht
weder ein Google-Konto noch Internet. Jede Standard-Authenticator-App funktioniert.</p>
<p>Praktisch ist außerdem <code>pamtester</code> – damit prüfst du die Anmeldung, <strong>ohne</strong> dich per SSH neu
anmelden zu müssen:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt install pamtester</span></span></code></pre></div>
</div>
<h3 id="2-geheimnis-für-einen-benutzer-erzeugen">2. Geheimnis für einen Benutzer erzeugen</h3>
<p>Melde dich als der Benutzer an, der 2FA bekommen soll (nicht als root – das Geheimnis gehört in
sein Home-Verzeichnis) und starte:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">google-authenticator -t -d -f -r <span class="m">3</span> -R <span class="m">30</span> -W</span></span></code></pre></div>
</div>
<p>Die Schalter sind bewusst gesetzt und ersparen dir fünf Rückfragen. Eine bleibt, und das ist gut so – dazu gleich mehr:</p>
<ul>
<li><strong><code>-t</code></strong> zeitbasierte Codes (TOTP) statt zählerbasierter</li>
<li><strong><code>-d</code></strong> verbietet die Wiederverwendung eines Codes – gegen Mitlesen</li>
<li><strong><code>-f</code></strong> schreibt die Datei, ohne vorher nachzufragen, ob sie angelegt werden soll</li>
<li><strong><code>-r 3 -R 30</code></strong> Ratenbegrenzung: höchstens 3 Versuche pro 30 Sekunden</li>
<li><strong><code>-W</code></strong> engeres Zeitfenster (weniger Toleranz, dafür sicherer)</li>
</ul>
<p>Das Werkzeug zeigt einen QR-Code und das Geheimnis im Klartext. Scanne den QR-Code mit der App –
dann fragt es <code>Enter code from app (-1 to skip):</code>. 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 <strong>Notfallcodes</strong>. <strong>Schreib sie auf
Papier</strong> – sie sind der Weg zurück, wenn das Handy verloren geht.</p>
<p>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 <code>getline(): Inappropriate ioctl for device</code> ab und
legt <strong>keine</strong> Datei an.</p>
<p>Danach liegt die Konfiguration im Home-Verzeichnis:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ls -l ~/.google_authenticator <span class="o">&amp;&amp;</span> grep <span class="s1">&#39;^&#34;&#39;</span> ~/.google_authenticator</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">-r-------- 1 koch koch 135 Jul 25 09:14 /home/koch/.google_authenticator
</span></span><span class="line"><span class="cl">&#34; RATE_LIMIT 3 30
</span></span><span class="line"><span class="cl">&#34; WINDOW_SIZE 3
</span></span><span class="line"><span class="cl">&#34; DISALLOW_REUSE
</span></span><span class="line"><span class="cl">&#34; TOTP_AUTH</span></span></code></pre></div>
</div>
<p>Rechte <code>0400</code> und Eigentümer der Benutzer selbst – so soll es sein. Wer die Datei lesen kann, kann
Codes erzeugen.</p>
<h3 id="3-pam-konfigurieren--mit-nullok-als-netz">3. PAM konfigurieren – mit <code>nullok</code> als Netz</h3>
<p>Öffne <code>/etc/pam.d/sshd</code> und setze <strong>als erste <code>auth</code>-Zeile</strong> ein:</p>
<div class="sk-code">
  <span class="sk-code-head">INI</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="na">auth required pam_google_authenticator.so nullok</span></span></span></code></pre></div>
</div>
<p>Das <code>nullok</code> ist der wichtigste Teil dieses Artikels: Es bedeutet „Benutzer <strong>ohne</strong>
<code>~/.google_authenticator</code> dürfen weiterhin ohne Code herein&quot;. Damit sperrst du niemanden aus,
während du 2FA ausrollst – und du selbst bleibst drin, solange du kein Geheimnis angelegt hast.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-amber-400 bg-amber-50 dark:border-amber-700 dark:bg-amber-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">⚠️</span>nullok gehört am Ende wieder weg
  </p>
  <div class="prose-kitchen text-sm"><code>nullok</code> 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 <strong>prüfe vorher</strong> mit <code>pamtester</code> (Schritt 6) für jeden Benutzer, dass
die Anmeldung funktioniert.</div>
</div>
<h3 id="4-sshd-tastatur-interaktive-anmeldung-erlauben">4. sshd: Tastatur-interaktive Anmeldung erlauben</h3>
<p>SSH fragt den Code über den Mechanismus „keyboard-interactive&quot; ab, und der ist auf Debian 13
standardmäßig <strong>aus</strong>. Statt die Hauptdatei zu editieren, legst du eine Ergänzung an – so bleibt sie
bei Paket-Updates unangetastet:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;KbdInteractiveAuthentication yes&#34;</span> <span class="p">|</span> sudo tee /etc/ssh/sshd_config.d/60-totp.conf</span></span></code></pre></div>
</div>
<p>Konfiguration prüfen, <strong>bevor</strong> du neu lädst – ein Syntaxfehler kostet dich sonst den Zugang:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo sshd -t <span class="o">&amp;&amp;</span> sudo systemctl reload ssh</span></span></code></pre></div>
</div>
<p>Kontrolle, was jetzt wirklich gilt:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo sshd -T <span class="p">|</span> grep -E <span class="s2">&#34;kbdinteractive|usepam&#34;</span></span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">usepam yes
</span></span><span class="line"><span class="cl">kbdinteractiveauthentication yes</span></span></code></pre></div>
</div>
<h3 id="5-zwei-faktoren-erzwingen--zuerst-für-einen-benutzer">5. Zwei Faktoren erzwingen – zuerst für einen Benutzer</h3>
<p>Jetzt kommt der eigentliche Schalter: <code>AuthenticationMethods</code> verlangt <strong>mehrere</strong> Verfahren
hintereinander. Damit ein Fehler nicht alle Konten trifft, gilt die Regel zunächst nur für einen
Testbenutzer:</p>
<div class="sk-code">
  <span class="sk-code-head">INI</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># /etc/ssh/sshd_config.d/61-totp-user.conf</span>
</span></span><span class="line"><span class="cl"><span class="na">Match User DEIN_TESTBENUTZER</span>
</span></span><span class="line"><span class="cl">    <span class="na">AuthenticationMethods publickey,keyboard-interactive</span></span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo sshd -t <span class="o">&amp;&amp;</span> sudo systemctl reload ssh</span></span></code></pre></div>
</div>
<p>Was jetzt für wen gilt, verrät <code>sshd -T</code> mit Benutzerkontext – ein Kommando, das dir viel Rätselraten
erspart:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo sshd -T -C <span class="nv">user</span><span class="o">=</span>DEIN_TESTBENUTZER,host<span class="o">=</span>localhost,addr<span class="o">=</span>127.0.0.1 <span class="p">|</span> grep authenticationmethods
</span></span><span class="line"><span class="cl">sudo sshd -T -C <span class="nv">user</span><span class="o">=</span>root,host<span class="o">=</span>localhost,addr<span class="o">=</span>127.0.0.1 <span class="p">|</span> grep authenticationmethods</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">authenticationmethods publickey,keyboard-interactive
</span></span><span class="line"><span class="cl">authenticationmethods any</span></span></code></pre></div>
</div>
<p>Genau das wollten wir: Der Testbenutzer braucht beides, alle anderen sind unberührt.</p>
<h3 id="6-prüfen-ohne-sich-auszusperren">6. Prüfen, ohne sich auszusperren</h3>
<p>Bevor du eine neue SSH-Sitzung wagst, teste den PAM-Stack direkt. <code>pamtester</code> fragt genau das, was
SSH auch fragen würde:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo pamtester -v sshd DEIN_TESTBENUTZER authenticate</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">pamtester: invoking pam_start(sshd, koch2, ...)
</span></span><span class="line"><span class="cl">pamtester: performing operation - authenticate
</span></span><span class="line"><span class="cl">Verification code: Password: pamtester: successfully authenticated</span></span></code></pre></div>
</div>
<p>Zwei Beobachtungen, die gleich wichtig werden: Es wird <strong>zuerst der Code</strong>, dann das <strong>Passwort</strong>
verlangt – die Reihenfolge ergibt sich aus der Position im PAM-Stack. Und mit falschem Code
antwortet dasselbe Kommando:</p>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Verification code: Password: pamtester: Authentication failure</span></span></code></pre></div>
</div>
<p>Erst wenn das sitzt, öffnest du eine <strong>neue</strong> 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.</p>
<h3 id="7-der-stolperstein-code-und-passwort">7. Der Stolperstein: Code <em>und</em> Passwort?</h3>
<p>In der Standardkonfiguration verlangt die Anmeldung nach dem Schlüssel <strong>beides</strong> – Einmalcode und
Passwort. Das liegt nicht an SSH, sondern daran, dass <code>keyboard-interactive</code> mit <code>UsePAM yes</code> den
<strong>gesamten</strong> PAM-Stack durchläuft: erst dein TOTP-Modul, dann über <code>@include common-auth</code> auch die
Passwortprüfung.</p>
<p>Im Test war das eindeutig: Schlüssel + richtiger Code + richtiges Passwort → Anmeldung erfolgreich;
Schlüssel + richtiger Code + <strong>falsches</strong> Passwort → <code>Permission denied (keyboard-interactive)</code>.</p>
<p>Willst du <strong>Schlüssel + Code ohne Passwort</strong> (die üblichere Variante, weil das Passwort neben dem
Schlüssel wenig beiträgt), kommentiere in <code>/etc/pam.d/sshd</code> die Passwortprüfung aus:</p>
<div class="sk-code">
  <span class="sk-code-head">INI</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="na">auth required pam_google_authenticator.so nullok</span>
</span></span><span class="line"><span class="cl"><span class="c1">#@include common-auth</span></span></span></code></pre></div>
</div>
<p>Danach genügt Schlüssel + Code – im Test bestätigt, mit falschem Code weiterhin abgelehnt.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-paprika-400 bg-paprika-50 dark:border-paprika-700 dark:bg-paprika-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🔥</span>Ohne common-auth gibt es keine Passwort-Anmeldung mehr – für niemanden
  </p>
  <div class="prose-kitchen text-sm">Diese eine Zeile wirkt auf <strong>alle</strong> SSH-Anmeldungen. Nachgeprüft: Nach dem Auskommentieren wurde
sogar eine Anmeldung mit <strong>korrektem</strong> 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 <strong>alle</strong> Zugänge auf Schlüssel umgestellt sind, und mit offener
zweiter Sitzung.</div>
</div>
<h3 id="8-auf-alle-benutzer-ausweiten">8. Auf alle Benutzer ausweiten</h3>
<p>Wenn der Testbenutzer sauber läuft und jedes Konto sein Geheimnis hat, machst du die Regel
allgemein: Ersetze den <code>Match</code>-Block durch eine globale Zeile und nimm <code>nullok</code> heraus.</p>
<div class="sk-code">
  <span class="sk-code-head">INI</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># /etc/ssh/sshd_config.d/60-totp.conf</span>
</span></span><span class="line"><span class="cl"><span class="na">KbdInteractiveAuthentication yes</span>
</span></span><span class="line"><span class="cl"><span class="na">AuthenticationMethods publickey,keyboard-interactive</span></span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">INI</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># /etc/pam.d/sshd – ohne nullok, damit niemand den Faktor umgehen kann</span>
</span></span><span class="line"><span class="cl"><span class="na">auth required pam_google_authenticator.so</span></span></span></code></pre></div>
</div>
<p>Und wieder: <code>sudo sshd -t</code>, neu laden, mit <code>pamtester</code> prüfen, <strong>dann</strong> eine neue Sitzung testen –
mit der alten weiterhin offen.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-herb-400 bg-herb-50 dark:border-herb-700 dark:bg-herb-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🧑‍🍳</span>Automatisierung ausnehmen
  </p>
  <div class="prose-kitchen text-sm"><p>Backup-Jobs, CI-Runner oder Deployments können keinen Code eintippen. Nimm sie gezielt aus, statt
2FA ganz zu lassen:</p>
<div class="sk-code">
  <span class="sk-code-head">INI</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="na">Match User deploy</span>
</span></span><span class="line"><span class="cl">    <span class="na">AuthenticationMethods publickey</span></span></span></code></pre></div>
</div>
<p>Ein solcher Zugang sollte dann aber einen eigenen, eingeschränkten Schlüssel bekommen (siehe
<code>command=</code>-Option in <code>authorized_keys</code>) – nicht dieselben Rechte wie dein persönliches Konto.</p>
</div>
</div>
<h2 id="wenn-es-nicht-funktioniert">Wenn es nicht funktioniert</h2>
<div class="troubleshoot not-prose">
<p><strong>Es kommt gar keine Code-Abfrage, die Anmeldung läuft wie vorher durch.</strong> Dann ist
<code>KbdInteractiveAuthentication</code> noch <code>no</code> – auf Debian 13 der Standard. Prüfe mit
<code>sudo sshd -T | grep kbdinteractive</code>, was wirklich gilt, und beachte: Dateien in
<code>/etc/ssh/sshd_config.d/</code> werden in alphabetischer Reihenfolge gelesen, die <strong>erste</strong> Festlegung
gewinnt.</p>
<p><strong>Nach dem Schlüssel wird zusätzlich das Passwort verlangt.</strong> Kein Fehler, sondern die Folge von
<code>UsePAM yes</code>: <code>keyboard-interactive</code> durchläuft den ganzen PAM-Stack, inklusive <code>@include common-auth</code>. Wer nur Schlüssel + Code will, kommentiert diese Zeile aus – mit der Konsequenz aus
der Warnung in Schritt 7.</p>
<p><strong>Der Code wird abgelehnt, obwohl die App ihn gerade anzeigt.</strong> Meist läuft die Serveruhr
auseinander. TOTP toleriert nur wenige Sekunden. Prüfe <code>timedatectl</code> – <code>System clock synchronized: yes</code> muss dort stehen; sonst NTP aktivieren. Zweite Möglichkeit: Du hast denselben Code kurz zuvor
benutzt, und <code>DISALLOW_REUSE</code> verbietet das. Das gilt auch, wenn der vorige Versuch erst am
<strong>Passwort</strong> 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.</p>
<p><strong>Nach drei Fehlversuchen geht gar nichts mehr.</strong> Das ist die Ratenbegrenzung aus <code>-r 3 -R 30</code>: 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.</p>
<p><strong>Ein Benutzer kommt ohne Code rein.</strong> Entweder fehlt seine <code>~/.google_authenticator</code> und <code>nullok</code>
lässt ihn passieren, oder für ihn greift ein <code>Match</code>-Block mit anderen <code>AuthenticationMethods</code>.
<code>sudo sshd -T -C user=NAME,host=localhost,addr=127.0.0.1 | grep authenticationmethods</code> zeigt, was für
ihn tatsächlich gilt.</p>
<p><strong>Handy verloren, keine Notfallcodes.</strong> 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
(<code>rm ~/.google_authenticator</code>) – mit <code>nullok</code> kommt er dann wieder ohne Code herein, ohne es
entsteht ein Konto ohne zweiten Faktor, das du neu einrichten musst.</p>

</div>
<p class="mt-3 text-sm text-slate-500 dark:text-slate-400">
  Dein Problem ist nicht dabei?
  <a href="/fehler/" class="font-medium text-paprika-700 hover:underline dark:text-paprika-400">Alle Fehlermeldungen durchsuchen →</a>
</p>

<h2 id="wartung--backups">Wartung &amp; Backups</h2>
<p><strong>Notfallcodes gehören ins Backup – aber nicht auf den Server.</strong> Die fünf Codes aus Schritt 2 sind
gleichwertig zum zweiten Faktor. Auf demselben Server gespeichert wären sie sinnlos; ins
<a href="/tutorials/vaultwarden-passwortmanager/">Passwortmanager-Tresor</a> oder auf Papier in die Schublade
gehören sie schon.</p>
<p><strong>Die Geheimnisse selbst sichern – oder bewusst nicht.</strong> <code>~/.google_authenticator</code> ist Teil des
Home-Verzeichnisses. Sicherst du Homes mit <a href="/tutorials/backups-mit-restic/">Restic</a>, 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.</p>
<p><strong>Nach jedem OpenSSH-Update kurz gegenprüfen.</strong> Optionen werden umbenannt (aus
<code>ChallengeResponseAuthentication</code> wurde <code>KbdInteractiveAuthentication</code>), und alte Namen verschwinden
irgendwann. Ein <code>sudo sshd -T | grep -E &quot;kbdinteractive|authenticationmethods&quot;</code> nach dem Update
zeigt in zwei Sekunden, ob deine Härtung noch greift oder stillschweigend unwirksam wurde. Wie du
Updates automatisiert einspielst, steht in
<a href="/tutorials/automatische-updates-unattended-upgrades/">automatische Updates</a>.</p>
<p><strong>Was 2FA nicht leistet.</strong> Der zweite Faktor schützt die <em>Anmeldung</em>. 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
<a href="/tutorials/firewall-ufw-einrichten/">Firewall</a>,
<a href="/tutorials/fail2ban-einrichten/">fail2ban</a> und
<a href="/tutorials/crowdsec-einrichten/">CrowdSec</a> – und er ersetzt keine davon.</p>
]]></content:encoded></item></channel></rss>