Home » Netzwerk & Sicherheit » mTLS mit Caddy: Dienste nur für eigene Geräte

mTLS mit Caddy: Dienste nur für eigene Geräte

Nextcloud, Vaultwarden oder Immich hängen bei vielen von euch hinter einem Reverse Proxy und sind damit für das ganze Internet erreichbar — geschützt nur durch ein Anmeldeformular, das jeder Bot der Welt ausprobieren darf. Mit mTLS mit Caddy ändert sich das grundlegend: Der Proxy verlangt schon beim Verbindungsaufbau ein Client-Zertifikat, und wer keines vorzeigt, bekommt die Anmeldeseite gar nicht erst zu sehen.

Titelbild zu mTLS mit Caddy: Links auf dunklem Grund das blaue Caddy-Logo mit dem Schriftzug caddy und dem Hinweis "verlangt ein Zertifikat eurer eigenen CA". Drei Pfeile führen nach rechts zu drei Karten: laptop-mario mit Häkchen, eigene Client-CA, zugelassen; gesperrtes Gerät, in der Sperrregel, Status 403; fremdes Gerät, unknown ca, abgebrochen. Die Aussage: Caddy lässt nur Geräte mit einem Zertifikat der eigenen Zertifizierungsstelle durch.

Diese Anleitung baut direkt auf unserer Einrichtung des Caddy Reverse Proxy mit automatischem HTTPS auf; Installation, Caddyfile und Zertifikate für den Server setzen wir voraus. Hier kommt die andere Richtung dazu: Nicht nur der Server weist sich gegenüber eurem Gerät aus, sondern auch euer Gerät gegenüber dem Server. Dafür legen wir mit OpenSSL eine eigene kleine Zertifizierungsstelle an, stellen jedem Gerät ein Zertifikat aus, bringen Caddy bei, nur noch diese zu akzeptieren, und klären, wie ihr ein verlorenes Gerät wieder aussperrt.

mTLS mit Caddy: was gegenseitiges TLS leistet

Bei gewöhnlichem HTTPS ist die Prüfung einseitig. Der Server zeigt ein Zertifikat vor, euer Browser prüft es, und danach steht eine verschlüsselte Verbindung — zu irgendwem. Bis zum Login hat jeder Besucher Zugriff auf alles, was davor liegt: Anmeldemaske, öffentliche Schnittstellen und jede Sicherheitslücke, die genau dort steckt.

Mutual TLS, kurz mTLS, macht die Prüfung gegenseitig. Während des TLS-Handshakes fordert der Server zusätzlich ein Zertifikat vom Client an und prüft, ob es von einer Stelle unterschrieben ist, der er vertraut. Fehlt es oder stammt es von einer fremden Stelle, endet die Verbindung, bevor eine einzige HTTP-Anfrage ausgewertet wird. Das Anmeldeformular bleibt, steht aber hinter einer zweiten Tür, die sich nur für Geräte öffnet, die ihr selbst ausgestattet habt.

Wichtig ist die Abgrenzung zu einem Verfahren, das wir schon beschrieben haben. Die SSH-Zertifikate aus unserer SSH-CA-Anleitung folgen derselben Idee, benutzen aber ein eigenes OpenSSH-Format ohne jede Verbindung zu X.509. Hier geht es um X.509-Zertifikate, also dasselbe Format, das auch euer Server für HTTPS vorzeigt. Eine SSH-CA lässt sich für mTLS nicht weiterverwenden, und umgekehrt.

Schaubild "Caddy prüft das Zertifikat, der Dienst sieht nur den Namen" mit drei Stationen von links nach rechts. Euer Gerät hält den privaten Schlüssel und das Client-Zertifikat CN=laptop-mario und zeigt das Zertifikat im TLS-Handshake vor. Caddy mit client_auth im Modus require_and_verify prüft die Unterschrift der eigenen Client-CA, die Laufzeit und eine eigene Sperrregel per Fingerabdruck und sieht Subject, Aussteller, Seriennummer und SHA-256-Fingerabdruck. Der Dienst, etwa Nextcloud auf 127.0.0.1:8080, sieht nur, was header_up weitergibt, zum Beispiel X-Client-Subject: CN=laptop-mario, aber weder Zertifikat noch Schlüssel. Ein roter Kasten darunter: Ohne Zertifikat oder mit fremdem bricht Caddy den TLS-Handshake ab, keine HTTP-Anfrage erreicht den Dienst.

Warum es eine eigene Zertifizierungsstelle sein muss

Die naheliegende Frage lautet, ob sich nicht einfach Let's Encrypt einspannen lässt. Die Antwort ist seit diesem Sommer endgültig nein. Let's Encrypt hat im Mai 2025 angekündigt, die Verwendung für Client-Authentifizierung aus seinen Zertifikaten zu streichen: Das Standardprofil verlor sie am 11. Februar 2026, und am 8. Juli 2026 wurde auch das Übergangsprofil tlsclient eingestellt. Als Grund nennt das Projekt die Vorgabe des Chrome-Root-Programms, Server- und Client-Authentifizierung bis Juni 2026 in getrennte Infrastrukturen aufzuteilen. Gleich dazu steht der Hinweis, viele Anwendungsfälle seien mit einer privaten Zertifizierungsstelle ohnehin besser bedient.

Für euren Zweck ist das sogar der bessere Weg. Ein öffentliches Zertifikat beweist nur, dass jemand eine Domain kontrolliert, und das kann jeder. Eine eigene Zertifizierungsstelle beweist, dass ihr dieses Gerät persönlich ausgestattet habt. Genau diese Aussage soll Caddy prüfen. Dass ein Weltkonzern seine Wurzelzertifikate neu sortiert und dabei zufällig das Richtige für Selfhoster herauskommt, ist selten genug, um es einmal festzuhalten.

mTLS oder VPN?

Das gleiche Ziel — nur eigene Geräte kommen an den Dienst — erreicht auch ein VPN, etwa ein eigener Wireguard-Server. Der Unterschied liegt in der Ebene. Ein VPN öffnet einen Tunnel ins Netz, und hinter dem Tunnel ist dann alles erreichbar, was dort freigegeben ist. mTLS gilt pro Dienst und pro Hostname, und auf dem Gerät muss nichts laufen ausser dem Browser oder der App, die ohnehin benutzt wird.

Ein VPN ist die richtige Wahl für Verwaltungsoberflächen, die gar nicht öffentlich sein sollen. mTLS passt dort, wo ein Dienst bewusst im Internet steht, weil Familie oder Freunde ihn ohne VPN-Client nutzen sollen.

Was ihr für die Client-Zertifikate braucht

  • Einen laufenden Caddy in Version 2.11.1 oder neuer. Älteren Fassungen fehlt eine Sicherheitskorrektur, die genau diese Funktion betrifft; mehr dazu in Schritt 3. Die aktuelle Version zeigt caddy version.
  • OpenSSL auf dem Rechner, der die Zertifizierungsstelle verwahrt. Unter Debian und Ubuntu ist es vorinstalliert. Die Befehle unten wurden mit OpenSSL 3 geprüft.
  • Einen sicheren Ort für den privaten Schlüssel der Zertifizierungsstelle. Das ist nicht der Server, auf dem Caddy läuft. Ein verschlüsselter Arbeitsrechner oder USB-Stick genügt.
  • Einen zweiten Zugang zum Server, etwa eine offene SSH-Sitzung. Sonst sperrt euch ein Tippfehler aus.

Schritt 1: Eine eigene Zertifizierungsstelle anlegen

Die Zertifizierungsstelle, kurz CA, ist bei OpenSSL ein Schlüsselpaar und ein selbst unterschriebenes Zertifikat. Erzeugt wird beides auf dem Rechner, auf dem es dauerhaft bleiben soll:

mkdir -p ~/client-ca && chmod 700 ~/client-ca
cd ~/client-ca

openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out client-ca.key
chmod 600 client-ca.key

openssl req -x509 -new -key client-ca.key -sha256 -days 3650 \
  -subj "/CN=Heimnetz Client-CA" \
  -addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
  -addext "keyUsage=critical,keyCertSign,cRLSign" \
  -out client-ca.crt

Der erste Befehl erzeugt einen Schlüssel auf der Kurve P-256, der zweite daraus ein zehn Jahre gültiges Zertifikat. Die Optionen sind in openssl-req(1) beschrieben: -x509 erzeugt direkt ein Zertifikat statt einer Anfrage, und -addext fügt eine Erweiterung hinzu, in derselben Schreibweise wie in einer Konfigurationsdatei.

Die beiden Erweiterungen sind der eigentliche Inhalt. basicConstraints mit CA:TRUE erklärt das Zertifikat zur Zertifizierungsstelle, und pathlen:0 bedeutet laut x509v3_config(5), dass sie nur Endzertifikate unterschreiben darf, keine weiteren Zwischenstellen. keyUsage beschränkt den Schlüssel auf das Signieren von Zertifikaten und Sperrlisten. Prüft das Ergebnis:

openssl x509 -in client-ca.crt -noout -subject -enddate -ext basicConstraints,keyUsage

Die Datei client-ca.crt ist öffentlich und wandert gleich auf den Server. Die Datei client-ca.key ist das Gegenteil: Wer sie besitzt, kann sich selbst ein Zertifikat für jeden eurer Dienste ausstellen. Für sie gilt dasselbe wie für den CA-Schlüssel aus der SSH-CA-Anleitung — nicht auf den Server, nicht in ein Cloud-Verzeichnis, und eine Kopie an einen zweiten Ort, wie es die 3-2-1-Regel für Backups verlangt. Wer zusätzlich eine Passphrase will, hängt beim ersten Befehl -aes256 an.

Schritt 2: Ein Client-Zertifikat pro Gerät ausstellen

Jedes Gerät bekommt ein eigenes Zertifikat, sonst lässt sich später kein einzelnes Gerät aussperren. Legt zuerst einmalig eine kleine Datei mit den Erweiterungen für Client-Zertifikate an:

cat > ~/client-ca/client.ext <<'EOF'
basicConstraints = critical, CA:FALSE
keyUsage = critical, digitalSignature
extendedKeyUsage = clientAuth
EOF

CA:FALSE verhindert, dass das Gerät selbst Zertifikate ausstellen kann. extendedKeyUsage = clientAuth ist der Eintrag, den x509v3_config(5) als "SSL/TLS WWW Client Authentication" beschreibt — genau der, den Let's Encrypt gestrichen hat. Dann das eigentliche Zertifikat, hier für einen Laptop:

cd ~/client-ca
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out laptop-mario.key
openssl req -new -key laptop-mario.key -subj "/CN=laptop-mario" -out laptop-mario.csr
openssl x509 -req -in laptop-mario.csr \
  -CA client-ca.crt -CAkey client-ca.key \
  -days 365 -sha256 -extfile client.ext \
  -out laptop-mario.crt
openssl verify -CAfile client-ca.crt laptop-mario.crt

Der Name hinter CN= ist frei wählbar. Nehmt etwas, das Person und Gerät eindeutig benennt; er taucht später in den Protokollen auf und ist das, was ihr beim Sperren sucht. Zur Seriennummer müsst ihr nichts tun: Laut openssl-x509(1) erzeugt OpenSSL eine zufällige, wenn weder -CAserial noch -CAcreateserial angegeben ist und keine Seriennummerndatei existiert. Ohne -days gälte das Zertifikat übrigens nur 30 Tage; ein Jahr ist ein brauchbarer Mittelweg zwischen Sicherheit und Erneuerungsaufwand. Die letzte Zeile muss mit laptop-mario.crt: OK enden.

Browser und Betriebssysteme wollen Schlüssel und Zertifikat in einer gemeinsamen, passwortgeschützten Datei im Format PKCS#12. Die entsteht so:

openssl pkcs12 -export \
  -in laptop-mario.crt -inkey laptop-mario.key \
  -certfile client-ca.crt \
  -name "laptop-mario" \
  -out laptop-mario.p12

OpenSSL fragt nach einem Exportpasswort. Vergebt ein echtes, denn die Datei reist gleich über USB-Stick, Messenger oder E-Mail auf das Gerät. -name setzt laut openssl-pkcs12(1) den Anzeigenamen, den Browser beim Auswählen des Zertifikats zeigen. Verschlüsselt wird standardmässig mit AES-256-CBC. Ältere Programme kennen das unter Umständen nicht; meldet ein Gerät beim Import ein falsches Passwort, obwohl es stimmt, erzeugt die Datei mit dem zusätzlichen Schalter -legacy neu, der auf die älteren Verfahren zurückfällt.

Haltet zum Schluss den Fingerabdruck fest. Ihr braucht ihn in Schritt 6, und zwar genau in dem Moment, in dem das Gerät nicht mehr greifbar ist:

openssl x509 -in laptop-mario.crt -noout -fingerprint -sha256 \
  | cut -d= -f2 | tr -d ':' | tr 'A-F' 'a-f' \
  | sed 's/^/laptop-mario /' >> ~/client-ca/register.txt

Die Umformung in Kleinbuchstaben ohne Doppelpunkte ist kein Zierrat: In genau dieser Schreibweise gibt Caddy den Fingerabdruck aus, und nur so passt er später in die Sperrregel. Die Dateien .key, .csr und .p12 des Geräts könnt ihr nach dem Import auf dem Gerät löschen; aufbewahrt werden nur das Zertifikat und das Register.

Schritt 3: Caddy verlangt das Client-Zertifikat

Auf den Server gehört ausschliesslich das öffentliche CA-Zertifikat. Legt es dort ab, wo Caddy es lesen kann:

scp ~/client-ca/client-ca.crt benutzer@server:/tmp/
ssh benutzer@server
sudo install -o root -g caddy -m 0644 /tmp/client-ca.crt /etc/caddy/client-ca.crt

Im Site-Block des Dienstes bekommt die tls-Direktive einen Unterblock client_auth:

cloud.example.ch {
	tls {
		client_auth {
			mode require_and_verify
			trust_pool file /etc/caddy/client-ca.crt
		}
	}
	reverse_proxy 127.0.0.1:8080
}

trust_pool file nennt die Datei mit dem CA-Zertifikat, gegen das geprüft wird. mode legt fest, wie streng Caddy dabei ist. Die Dokumentation kennt vier Stufen:

  • request — fragt nach einem Zertifikat, lässt aber auch ohne durch und prüft keines.
  • require — verlangt ein Zertifikat, prüft aber nicht, von wem es stammt.
  • verify_if_given — lässt auch ohne Zertifikat durch, prüft aber jedes, das vorgezeigt wird.
  • require_and_verify — verlangt ein gültiges Zertifikat und prüft es.

Für den Schutz eines ganzen Dienstes ist nur die letzte Stufe sinnvoll. Ist ein trust_pool gesetzt, ist sie laut Dokumentation ohnehin die Vorgabe; ohne trust_pool fällt Caddy auf require zurück, und dann genügt irgendein Zertifikat. Schreibt die Zeile trotzdem hin, damit niemand raten muss. Danach wie gewohnt:

sudo caddy fmt --overwrite /etc/caddy/Caddyfile
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy

Testen, bevor ihr den Browser bemüht

Am schnellsten prüft ihr das Ergebnis mit curl von einem Rechner, auf dem Zertifikat und Schlüssel liegen. Drei Aufrufe, drei erwartete Ergebnisse:

# ohne Zertifikat: Verbindung muss scheitern
curl -sS https://cloud.example.ch/

# mit Zertifikat: Antwort des Dienstes
curl -sS --cert laptop-mario.crt --key laptop-mario.key https://cloud.example.ch/

# mit einem selbst gebauten fremden Zertifikat: muss ebenfalls scheitern
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
  -keyout fremd.key -out fremd.crt -subj "/CN=fremd" -days 1
curl -sS --cert fremd.crt --key fremd.key https://cloud.example.ch/

Ohne Zertifikat meldet curl einen TLS-Abbruch mit certificate required, beim fremden Zertifikat unknown ca. Genau so soll es aussehen. Nur der mittlere Aufruf darf eine Antwort des Dienstes liefern. Lasst den dritten Test nicht weg: Nur er zeigt, dass Caddy wirklich gegen eure CA prüft.

Warum die Version zählt

Ende Februar 2026 wurde in Caddy eine Lücke geschlossen, die genau diesen Aufbau betrifft. Laut dem Sicherheitshinweis GHSA-hffm-g8v7-wrv7 (CVE-2026-27586) haben zwei verschluckte Fehlermeldungen dazu geführt, dass die Client-Authentifizierung "silently fail open", wenn die CA-Datei fehlt, nicht lesbar oder beschädigt ist. Caddy startete dann ohne Warnung und nahm jedes Client-Zertifikat an, das von einer der im System hinterlegten öffentlichen Zertifizierungsstellen stammte. Ein Tippfehler im Pfad hätte den Schutz also lautlos abgeschaltet. Behoben ist das mit Caddy 2.11.1.

Mit einer aktuellen Version sieht derselbe Fehler anders aus: Zeigt trust_pool file auf eine Datei, die es nicht gibt, bricht schon caddy validate mit einer Meldung ab, die den Pfad und no such file or directory nennt. So muss es sein: Ein Fehler in der Konfiguration muss den Start verhindern, nicht den Schutz abschalten. Prüft deshalb mit caddy version, dass ihr nicht auf einer älteren Fassung sitzt, etwa aus dem Paketarchiv einer älteren Distribution.

Ein Snippet für mehrere Dienste

Sollen mehrere Dienste denselben Schutz bekommen, schreibt ihr den Block einmal als Snippet und bindet ihn überall ein. Ein Snippet steht in runden Klammern ausserhalb aller Site-Blöcke:

(mtls) {
	tls {
		client_auth {
			mode require_and_verify
			trust_pool file /etc/caddy/client-ca.crt
		}
	}
}

cloud.example.ch {
	import mtls
	reverse_proxy 127.0.0.1:8080
}

fotos.example.ch {
	import mtls
	reverse_proxy 127.0.0.1:2283
}

Eine Eigenheit nimmt euch Caddy dabei von selbst ab. Weil TLS-Einstellungen am Hostnamen aus dem Handshake hängen, die HTTP-Anfrage aber einen eigenen Host-Header mitbringt, könnte ein Client sich mit dem Namen eines ungeschützten Dienstes verbinden und dann einen geschützten anfragen. Die globale Option strict_sni_host verhindert das, und sie ist laut Dokumentation automatisch aktiv, sobald Client-Authentifizierung konfiguriert ist. Wer es ausprobiert, bekommt den Status 421 Misdirected Request.

Schritt 4: Das Client-Zertifikat auf die Geräte bringen

Jetzt braucht jedes Gerät seine .p12-Datei. Übertragt sie auf einem Weg, den ihr kontrolliert, und das Exportpasswort auf einem anderen. Die Bedienschritte unterscheiden sich je nach System:

  • Firefox führt einen eigenen Zertifikatsspeicher. Unter Einstellungen, Datenschutz & Sicherheit, Abschnitt Zertifikate, öffnet Zertifikate anzeigen, wechselt auf den Reiter Ihre Zertifikate und wählt Importieren. Die Schritte entsprechen der Importanleitung von DigiCert, die dort für Firefox und Windows beschrieben ist.
  • Chrome, Edge und andere Programme unter Windows greifen auf den Zertifikatsspeicher von Windows zu. Ein Doppelklick auf die .p12-Datei startet den Importassistenten; als Ziel wählt ihr den aktuellen Benutzer und den Speicher Eigene Zertifikate.
  • Android verwaltet Zertifikate in den Einstellungen unter Sicherheit & Datenschutz, Weitere Sicherheitseinstellungen, Verschlüsselung & Anmeldedaten, Ein Zertifikat installieren; den Pfad beschreibt Google in der Pixel-Hilfe, die Bezeichnungen weichen je nach Hersteller leicht ab. Wählt dort die Variante für VPN- und App-Nutzerzertifikate, wie es etwa die Anleitung des MIT für dessen Client-Zertifikate zeigt — nicht die für CA-Zertifikate.
  • Linux auf der Kommandozeile braucht keinen Import. curl nimmt Zertifikat und Schlüssel direkt mit --cert und --key, wie im Test oben.

Beim ersten Aufruf fragt der Browser, welches Zertifikat er vorzeigen soll, und zeigt dabei den Namen aus -name. Danach öffnet sich die gewohnte Anmeldeseite. Prüft auch die App des Dienstes: Nicht jede mobile App kann ein Client-Zertifikat aus dem Systemspeicher vorzeigen, und eine, die es nicht kann, ist ab jetzt ausgesperrt. Testet das, bevor ihr euch unterwegs darauf verlasst.

Schritt 5: Nur den Admin-Bereich hinter das Zertifikat stellen

Manchmal soll der Dienst öffentlich bleiben und nur ein Teil davon geschützt werden, etwa die Verwaltungsoberfläche unter /admin. Eine Einstellung pro Pfad gibt es bei client_auth nicht, weil das Zertifikat im TLS-Handshake abgefragt wird, bevor überhaupt ein Pfad bekannt ist. Der Umweg führt über die mildere Stufe verify_if_given und einen Matcher, der prüft, ob ein Zertifikat vorgezeigt wurde:

vault.example.ch {
	tls {
		client_auth {
			mode verify_if_given
			trust_pool file /etc/caddy/client-ca.crt
		}
	}

	@admin_ohne_zertifikat {
		path /admin*
		vars {tls_client_subject} ""
	}
	respond @admin_ohne_zertifikat "Nur mit Zertifikat" 403

	reverse_proxy 127.0.0.1:8000
}

Der Matcher vars vergleicht den Wert eines Platzhalters, hier {tls_client_subject}, also den Namen aus dem Zertifikat. Ist er leer, wurde keines vorgezeigt, und für Pfade unter /admin gibt es einen 403. Ein vorgezeigtes Zertifikat einer fremden Stelle scheitert schon im Handshake, weil verify_if_given jedes vorgezeigte Zertifikat prüft. Alles ausserhalb von /admin bleibt ohne Zertifikat erreichbar.

Zwei Nebenwirkungen solltet ihr kennen. Erstens fragt Caddy jetzt auf der ganzen Domain nach einem Zertifikat, nicht nur unter /admin; Browser mit installiertem Zertifikat bieten es deshalb auch auf der öffentlichen Seite an. Zweitens schützt dieser Aufbau nur Pfade, die ihr kennt; liegt dieselbe Funktion noch in einer API, bleibt sie offen. Eine eigene Subdomain mit require_and_verify ist deshalb robuster.

Wer ist angemeldet: Zertifikatsdaten an den Dienst weitergeben

Caddy kennt nach dem Handshake die Daten des Client-Zertifikats und stellt sie als Platzhalter bereit. Die Übersicht der Platzhalter nennt unter anderem {tls_client_subject}, {tls_client_issuer}, {tls_client_serial} und {tls_client_fingerprint}. Mit header_up reicht ihr davon weiter, was der Dienst dahinter wissen soll:

cloud.example.ch {
	import mtls
	reverse_proxy 127.0.0.1:8080 {
		header_up X-Client-Subject {tls_client_subject}
	}
}

Der Dienst sieht dann eine Kopfzeile wie X-Client-Subject: CN=laptop-mario, aber nie das Zertifikat oder gar den Schlüssel. In unserem Test mit Caddy 2.11.4 hat Caddy eine vom Client mitgeschickte, gefälschte Kopfzeile gleichen Namens dabei überschrieben. Darauf verlassen darf sich der Dienst trotzdem nur, wenn er ausschliesslich über Caddy erreichbar ist, also an 127.0.0.1 gebunden, wie in der Caddy-Grundanleitung beschrieben. Sonst kann jeder Caddy umgehen und die Kopfzeile selbst setzen.

Schritt 6: Ein Gerät ist weg — Zertifikat sperren

Das klassische Werkzeug zum Widerrufen von X.509-Zertifikaten ist eine Sperrliste. Die Dokumentation der tls-Direktive erwähnt allerdings an keiner Stelle Sperrlisten oder eine andere Widerrufsprüfung für Client-Zertifikate. Verlasst euch also nicht darauf.

Der verlässliche Weg ist eine Sperrregel mit demselben Matcher wie in Schritt 5, gestützt auf den Fingerabdruck aus eurem Register:

(mtls) {
	tls {
		client_auth {
			mode require_and_verify
			trust_pool file /etc/caddy/client-ca.crt
		}
	}
	@gesperrt vars {tls_client_fingerprint} 151a598999f66b8a2bf831977b32d6a273ba945734ce0cf3f330fa1954206d93
	respond @gesperrt "Zertifikat gesperrt" 403
}

Der Wert oben ist ein Beispiel; tragt den Fingerabdruck des verlorenen Geräts aus register.txt ein. Weitere gesperrte Geräte hängt ihr mit Leerzeichen getrennt an dieselbe Zeile, denn vars nimmt mehrere Werte und trifft, sobald einer davon passt. Nach caddy validate und systemctl reload caddy bekommt das gesperrte Gerät auf jedem Dienst, der das Snippet einbindet, einen 403. Alle anderen Geräte arbeiten unverändert weiter.

Zur Schreibweise eine Warnung, weil sie Zeit kostet: Caddy liefert den Fingerabdruck in Kleinbuchstaben ohne Doppelpunkte, OpenSSL dagegen in Grossbuchstaben mit Doppelpunkten. Bei der Seriennummer ist es noch tückischer. OpenSSL zeigt sie hexadezimal an, der Platzhalter {tls_client_serial} lieferte in unserem Test dieselbe Zahl in Dezimalschreibweise. Wer eine Seriennummer aus openssl x509 -serial in die Sperrregel kopiert, sperrt deshalb niemanden. Der Fingerabdruck in der Umformung aus Schritt 2 umgeht dieses Problem. Im Zweifel lasst euch den Wert einmal von Caddy selbst ausgeben, etwa mit einem vorübergehenden respond {tls_client_fingerprint}.

Daneben bleibt die Laufzeit das zuverlässigste Sperrmittel, weil sie ohne euer Zutun wirkt. Den Ablauf behaltet ihr am besten mit einer Erinnerung im Blick, etwa über einen systemd-Timer mit Benachrichtigung, der openssl x509 -checkend über die Zertifikate im Register laufen lässt. Und falls der CA-Schlüssel selbst abhandenkommt, hilft keine Sperrregel mehr: Dann legt ihr eine neue CA an, ersetzt client-ca.crt auf dem Server und stattet alle Geräte neu aus.

Wo mTLS an seine Grenzen kommt

Ein Client-Zertifikat schützt den Weg zum Dienst, nicht den Dienst selbst. Wer ein entsperrtes Gerät in der Hand hält, hat das Zertifikat gleich mit. mTLS ergänzt Passwort und zweiten Faktor, es ersetzt sie nicht.

Ausserdem muss die TLS-Verbindung tatsächlich bei Caddy enden. Steht ein fremder Proxy davor, etwa der Proxy-Modus von Cloudflare mit der orangen Wolke, baut Cloudflare die Verbindung zu euren Geräten auf und Caddy sieht nur noch Cloudflare. Ein Client-Zertifikat eures Laptops kommt dort nie an. Für mTLS muss der Eintrag im DNS direkt auf euren Server zeigen.

Und schliesslich will die Absicherung gepflegt werden: Zertifikate laufen ab, Geräte werden ersetzt, neue Familienmitglieder wollen Zugang. Bei drei Geräten ist das wenig Arbeit, bei zwanzig ist ein VPN oder ein Werkzeug, das die Ausstellung automatisiert, die einfachere Antwort. Spätestens wenn die Tabelle mit den Fingerabdrücken länger ist als die Einkaufsliste, ist dieser Punkt erreicht.

Was ihr vorher festlegen solltet

Die Befehle sind in einer Stunde abgearbeitet. Die folgenden Entscheidungen solltet ihr vorher treffen, weil sie sich später nur mühsam ändern lassen:

  • Wo der CA-Schlüssel liegt — und wo seine Kopie liegt. Nicht auf dem Server, nicht unverschlüsselt, nicht nur an einem Ort.
  • Wie lange Client-Zertifikate gelten. Kürzer heisst sicherer und mehr Arbeit. Ein Jahr ist ein vernünftiger Anfang; für Geräte, die öfter verloren gehen können, darf es weniger sein.
  • Nach welchem Schema ihr die Namen vergebt. person-geraet ist im Protokoll lesbar und beim Sperren auffindbar. test2 ist es nicht.
  • Wo das Register liegt. Name, Fingerabdruck und Ablaufdatum jedes ausgestellten Zertifikats, an einem Ort, den ihr auch erreicht, wenn das Gerät weg ist.
  • Welche Dienste mit welcher Stufe laufen. Ganzer Dienst mit require_and_verify oder nur ein Pfad mit verify_if_given — und ob die zugehörige App überhaupt Client-Zertifikate kann.
  • Wie ihr reinkommt, wenn es klemmt. Eine SSH-Sitzung auf den Server oder ein Weg im lokalen Netz, der nicht über den geschützten Dienst führt.

Fazit: mTLS sperrt Fremde aus, bevor sie das Login überhaupt sehen

Ein selbst gehosteter Dienst im Internet ist so sicher wie das, was vor seinem Login liegt. Bei den meisten Installationen ist das nichts: Jeder Scanner der Welt darf die Anmeldeseite abrufen, Benutzernamen durchprobieren und auf die nächste Lücke in genau diesem Code warten. Mit mTLS endet dieser Versuch im Handshake, noch bevor der Dienst davon erfährt. Caddy braucht dafür sechs Zeilen, OpenSSL eine Handvoll Befehle.

Dass Let's Encrypt die Client-Authentifizierung in diesem Jahr gestrichen hat, wirkt auf den ersten Blick wie ein Verlust und ist in Wahrheit eine Klarstellung. Wer bestimmt, welche Geräte an eure Dienste dürfen, gehört nicht in eine öffentliche Zertifizierungsstelle, sondern in euren Schrank. Der Preis dafür ist Verantwortung: für den CA-Schlüssel, für das Register, für die Ablaufdaten. Wer sie übernimmt, hat das stärkste Schloss, das ein Reverse Proxy zu bieten hat — ohne Cloud und ohne irgendjemandes Erlaubnis.

Quellen und weiterführende Links

Ähnliche Beiträge

  • Versenden und Empfangen von Bitcoin

    In diesem Artikel erfahrt Ihr die Grundlagen zum Empfangen und Versenden von Bitcoin. Bitcoin-Transaktionen sind das Herzstück des Bitcoin-Netzwerks. Sie ermöglichen den Transfer von Bitcoins zwischen den Teilnehmern und stellen...

  • ElectRS vs ElectrumX vs Fulcrum

    Drei Wege zum eigenen Electrum Server: electrs, ElectrumX und Fulcrum im Vergleich - Speicherbedarf, Anforderungen an die Full Node und der Pflegestand der Projekte im August 2026.

  • Email Client Thunderbird absichern

    Thunderbird ist ab Werk besser eingestellt als sein Ruf. Welche Schalter ihr trotzdem setzen solltet – und welche verbreiteten Härtungstipps Thunderbird unsicherer machen.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert