Dasselbe Schloss, deine eigene Tür

Die Home-Assistant-App über Cloudflare, an ein Zertifikat gebunden setzt das Schloss an Cloudflares Edge. Das Telefon hat ein Zertifikat, Cloudflare prüft es, und alles ohne eins erreicht das Haus nie. Es funktioniert, es kostet nichts, und es heißt, dass Cloudflare das Tor hält und den Verkehr im Vorbeigehen liest.

Das hier ist dasselbe Schloss mit der Tür im eigenen Haus. nginx oder Caddy prüft das Zertifikat. Deine eigene Zertifizierungsstelle stellt es aus. Niemand sonst ist im Weg. Die Anmeldeseite bleibt für alles ohne Zertifikat unsichtbar, also ändert sich nicht, ob Fremde hineinkommen. Es ändert sich, wer die Arbeit macht und wer das Risiko trägt, wenn etwas schiefgeht.

Alles hier wurde gebaut und getestet: nginx 1.30.5 und Caddy 2.11.7 in Podman, Zertifikate von step-ca 0.30.2, jeder Proxy angesprochen ohne Zertifikat, mit einem guten Zertifikat, einem widerrufenen und einem WebSocket-Upgrade, über IPv4 und IPv6. Das Backend im Test war ein kleiner Platzhalter, der die empfangenen Header zurückgibt, also zeigen die Ergebnisse genau, was Home Assistant übergeben würde.

Was sich ändert, wenn Cloudflare wegfällt

mTLS zu Hause mit nginx oder CaddyTelefonhat ein ZertifikatAlle anderenkein ZertifikatEine Flutbraucht keinsHeimnetzRouterPort 443kein DDoS-Schutznginx oder Caddyführt zu deiner CA?nicht widerrufen?IPv4 und IPv6step-caHome Assistantvertraut dem ProxyAbgewiesenim Handshakedurchabgelehnt
Alles kommt jetzt an deinem Router an. Der Proxy weist alles ohne gutes Zertifikat ab, bevor Home Assistant es sieht. Eine Flut braucht kein Zertifikat, und ohne etwas davor, das sie auffängt, landet sie auf deiner Leitung.
Cloudflare-Variantenginx oder Caddy zu Hause
Wer das Zertifikat prüftCloudflares Edge, mit einer WAF-RegelDein Proxy, während des TLS-Handshakes
ZertifizierungsstelleVon Cloudflare verwaltetDeine eigene, step-ca für Client-Zertifikate
Wer den Verkehr entschlüsselt siehtCloudflare, dann Home AssistantDein Proxy und Home Assistant, sonst niemand
Weg von außen hineinEin Tunnel, nur ausgehendEine Portweiterleitung zum Proxy
Ein DDoS landet beiCloudflareDeiner Leitung zu Hause
Die Verbindung eines Fremden kostetZu Hause nichtsEinen TLS-Handshake am Proxy, nie eine Anmeldung bei Home Assistant
IPv4 und IPv6Beides, Cloudflare beantwortet jedesBeides, wenn die Leitung beides hat und der Proxy auf beidem lauscht
WiderrufEingebaut, von der Regel geprüftnginx prüft eine CRL; Caddy führt eine Erlaubnisliste
Wenn der Anbieter ausfälltKein FernzugriffNichts ändert sich
Laufende KostenKostenloser TarifKostenlos, plus deine Zeit für die CA

Die zwei Zeilen, auf die es am meisten ankommt, ziehen in entgegengesetzte Richtungen. Niemand sonst liest deinen Verkehr oder kann dich abschalten, und das ist Souveränität, der ganze Grund, es zu tun. Aber die Portweiterleitung ist zurück, also treffen Scans und Fluten deine Leitung statt Cloudflares. Der Unterschied zur einfachen Portweiterleitung aus dem Vergleich im Cloudflare-Beitrag ist, was sie bei der Ankunft vorfinden: einen Proxy, der die Verbindung im Handshake fallen lässt, nicht die Anmeldeseite von Home Assistant.

Sei dir klar darüber, was das bringt und was nicht. Dein eigener Reverse Proxy gibt dir überhaupt keinen DDoS-Schutz. mTLS hält einen Fremden davon ab hineinzukommen. Es tut nichts dagegen, dass ein Fremder deine Leitung zuschüttet, und eine Flut braucht kein Zertifikat, um sie zu füllen. Cloudflare fängt das für dich in jedem Tarif ab, den kostenlosen eingeschlossen1. Hier ist niemand davor, der es abfängt, also nimmt eine Flut, die groß genug ist, das ganze Haus vom Netz, und der Proxy kann nur Verbindungen abweisen, die er überhaupt zu sehen bekommt.

nginx oder Caddy

Beide erledigen die Aufgabe. Sie unterscheiden sich an einer Stelle, auf die es ankommt. Beim Widerruf.

Widerruf mit nginx und mit Caddynginx: eine CRL, von einem Timer geholtstep-cawiderrufen, neue CRL/1.0/crleinfaches HTTPTimerholen, neu ladennginxprüft die KetteCaddy: eine Erlaubnisliste, von Hand gepflegtDujede NeuausstellungErlaubnislisteexakte ZertifikateNeu ladennach jeder ÄnderungCaddyVerifier leaf
nginx bekommt per Timer eine CRL und prüft sie bei jeder Verbindung. Caddy unterstützt keine CRL, also ist Widerruf eine Liste exakter Zertifikate, die du von Hand pflegst.
nginx 1.30.5Caddy 2.11.7
Kein ZertifikatHandshake gelingt, 400 No required SSL certificate was sentHandshake abgewiesen, curl Exit 55
Widerrufenes ZertifikatAbgewiesen, 400 The SSL certificate errorDirekt hereingelassen, wenn nur die CA geprüft wird
WiderrufsmethodeEine CRL, über die ganze Kette geprüftEine Erlaubnisliste von Zertifikaten, der Verifier leaf
WebSocket für die AppBraucht gesetzte Upgrade- und Connection-HeaderFunktioniert ohne zusätzliche Konfiguration
IPv6Nur mit zusätzlichem listen [::]:443Lauscht standardmäßig auf beidem
KonfigurationEtwa 30 ZeilenEtwa 15 Zeilen

Der Widerruf ist das, worüber du entscheiden musst. Caddys Doku nennt genau einen Verifier, der mit dem Standard-Caddy ausgeliefert wird, leaf, und der prüft, ob das Zertifikat „is one of a defined set of permitted certificates“2. Es gibt keine CRL und kein OCSP für Client-Zertifikate. Lass Caddy nur auf die CA schauen, und ein widerrufenes Telefon spaziert herein. Genau das hat der Test gezeigt: phone2, eine Stunde vorher widerrufen, bekam eine 200. Die Erlaubnisliste schließt die Lücke. Sie ist auch eine Liste, die du von Hand pflegst, solange du das betreibst.

Die nginx-Konfiguration

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name ha.example.com;

    ssl_certificate         /etc/nginx/pki/server-chain.pem;
    ssl_certificate_key     /etc/nginx/pki/server.key;
    ssl_protocols           TLSv1.2 TLSv1.3;

    # The mTLS half: only certificates from our CA, and not revoked.
    ssl_client_certificate  /etc/nginx/pki/client-ca.pem;
    ssl_verify_client       on;
    ssl_verify_depth        2;
    ssl_crl                 /etc/nginx/pki/crl-chain.pem;

    location / {
        proxy_pass http://192.0.2.10:8123;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
    }
}
ZeileWarum sie da ist
listen [::]:443 sslOhne sie spricht nginx nur IPv4, und ein Telefon in einem IPv6-Netz hat keinen Landeplatz
ssl_verify_client onKein Zertifikat, kein Zutritt. optional würde die Anfrage durchlassen, damit später über sie geurteilt wird
ssl_verify_depth 2step-ca signiert von einer Zwischen-CA aus, also ist die Kette Telefon, Zwischen-CA, Root. Die Voreinstellung 1 hört zu früh auf3
ssl_client_certificateDie Zwischen-CA und der Root in einer Datei
ssl_crlDie CRL von step-ca und eine Root-CRL in einer Datei. nginx prüft jedes Glied der Kette, also braucht es beide
Upgrade und ConnectionDie App spricht über einen WebSocket mit Home Assistant. Ohne diese Zeilen lädt die App und bleibt dann einfach stehen

Die CRL-Datei ist der Teil, der Sorgfalt braucht, und alles dazu steht in Client-Zertifikate von step-ca: warum die CRL von step-ca abgewiesen wird, bis eine Vorlage ihren Geltungsbereich richtet, warum nginx auch eine CRL vom Root will, und der Timer, der sie auffrischt, bevor sie abläuft. Lass das weg, und nginx sperrt jedes Telefon aus, auch die guten.

Was der Test zurückbekam:

ClientIPv4IPv6
Kein Zertifikat400 No required SSL certificate was sent400
phone1200, X-Forwarded-For: 127.0.0.1200, X-Forwarded-For: ::1
phone2, widerrufen400 The SSL certificate error400
phone1, WebSocket-Upgrade200, Upgrade: websocket durchgereicht200, Upgrade: websocket durchgereicht

Bevor listen [::]:8443 drin war, kam die IPv6-Anfrage gar nicht erst zustande. curl Exit 7.

Die Caddy-Konfiguration

ha.example.com {
	tls /etc/caddy/pki/server-chain.pem /etc/caddy/pki/server.key {
		client_auth {
			mode require_and_verify
			trust_pool file /etc/caddy/pki/client-ca.pem
			verifier leaf {
				file /etc/caddy/pki/allowed-phones.pem
			}
		}
	}
	reverse_proxy 192.0.2.10:8123
}

require_and_verify heißt, das Zertifikat muss da sein und muss zur CA führen2. verifier leaf ist der Widerruf. Nur die Zertifikate in allowed-phones.pem kommen hinein, also heißt ein Telefon widerrufen, sein Zertifikat aus dieser Datei zu nehmen und neu zu laden.

ClientIPv4IPv6
Kein ZertifikatHandshake abgewiesenHandshake abgewiesen
phone1, auf der Liste200, X-Forwarded-For: 127.0.0.1200, X-Forwarded-For: ::1
phone2, widerrufen, nur CA-Prüfung200
phone2, nicht auf der ListeHandshake abgewiesenHandshake abgewiesen
phone1, WebSocket-Upgrade über HTTP/1.1200, Upgrade: websocket durchgereicht

Die Erlaubnisliste hat eine eigene Falle, und der Test ist geradewegs hineingelaufen. Die Liste enthält exakte Zertifikate, keine Namen. Stell das Zertifikat eines Telefons neu aus, und das neue steht nicht auf der Liste. phone1 wurde mitten im Test neu ausgestellt, und Caddy wies es mit tls alert bad certificate ab, bis die Liste neu gebaut war. Jede Erneuerung sind jetzt zwei Arbeiten. Das Zertifikat ausstellen, dann die Liste aktualisieren. Vergiss die zweite, und das Telefon ist für nix und wieder nix ausgesperrt.

Caddy loggt beim Start außerdem enabling strict SNI-Host enforcement because TLS client auth is configured. Das ist Caddy, das das Richtige tut: eine Anfrage muss im Handshake denselben Hostnamen verlangen wie in der HTTP-Anfrage, damit niemand die Zertifikatsprüfung für eine Seite besteht und dann nach einer anderen fragt.

Die Seite von Home Assistant

Dieselben Einstellungen wie in der Cloudflare-Variante, auf einen anderen Proxy gerichtet. Unter Settings → System → Network → HTTP server4:

EinstellungWertWarum
Trust X-Forwarded-ForAnOhne sie lehnt Home Assistant weitergeleitete Anfragen ab
Trusted proxiesNur die Adresse des Proxys, IPv4 und IPv6, zum Beispiel 192.0.2.20 und 2001:db8::20Nur diese Kiste darf behaupten, für jemanden weiterzuleiten
Enable IP banningAnSperren treffen die echte Client-Adresse, die der Proxy weitergibt
Home Assistant URL, Internethttps://ha.example.comDie App übernimmt das als ihre externe URL

Trag die IPv6-Adresse des Proxys genauso ein wie seine IPv4-Adresse. Der Test zeigt, dass nginx über IPv6 ::1 als Client-Adresse weiterreicht, und im Betrieb reicht es weiter, wo auch immer das Telefon herkam. Vertraut Home Assistant nur der IPv4-Adresse des Proxys, wird jede Anfrage abgewiesen, die es vom Proxy über IPv6 erreicht.

Das Telefon

Von step-ca braucht das Telefon drei Dateien. Alles dazu steht im Beitrag über Client-Zertifikate mit step-ca, und die Einrichtung der CA selbst in Deine eigene Zertifizierungsstelle.

DateiInstallieren als
Root-ZertifikatCA-Zertifikat
ZwischenzertifikatCA-Zertifikat
Die PKCS12-Datei des TelefonsVPN- und App-Benutzerzertifikat5

Das Serverzertifikat am Proxy kommt auch von step-ca. Die Home-Assistant-App für Android vertraut CA-Zertifikaten, die der Benutzer installiert6, also akzeptiert sie es, und das ist ein zweites Schloss, kein Ärgernis. Ein Gerät, das deinen Root nicht hat, vertraut dem Server nicht, bevor die Frage nach seinem eigenen Zertifikat überhaupt aufkommt.

Dann stell die Verbindungssicherheit der App auf Most secure, genau wie in der Cloudflare-Variante7.

Wo es an seine Grenzen kommt

FallWas passiertWas tun
CRL läuft abnginx weist jedes Telefon abPer Timer auffrischen, wie im step-ca-Beitrag
Root-CRL läuft abDasselbe, ein Jahr späterEin Termin im Kalender
Caddy, Telefon widerrufenKommt hinein, solange es nicht von der Erlaubnisliste istDie Liste pflegen, oder nginx nehmen
Caddy, Telefon neu ausgestelltAusgesperrt, bis die Liste aktualisiert istDie Liste bei jedem Zertifikat aktualisieren
Leitung zu Hause hinter Carrier-Grade NAT, kein IPv6Nichts erreicht die PortweiterleitungIPv6, oder die Cloudflare-Variante
Eine Flut auf die Leitung zu HauseNimmt das Haus vom Netz, nicht nur Home AssistantDie Cloudflare-Variante, wenn dich das mehr sorgt als ein Dritter
iPhone, Wear OSWie in der Cloudflare-Variante: Android ist die dokumentierte App, Wear OS will die PKCS12-Datei bei der Einrichtung5Testen, bevor du dich darauf verlässt

Nichts davon ist versteckt. Jedes davon ist eine Aufgabe. Schreib sie mit einem Datum oder einem Befehl daneben auf, und sie bleibt eine Aufgabe und wird keine Überraschung.

Das Ganze selbst besitzen

Die Cloudflare-Variante und diese hier halten denselben Fremden draußen. Der Unterschied ist, wem du vertrauen musst, während sie das tun.

Mit Cloudflare vertraust du einer Firma, dass sie das Tor hält, den Verkehr entschlüsselt und es weiter kostenlos anbietet. Hiermit vertraust du dir selbst, dass du eine CRL auffrischst, eine Liste in Ordnung hältst und merkst, wenn ein Datum heranrückt. Keins von beiden kommt ohne Vertrauen aus. Das eine verteilt es an jemanden, dem du nichts zahlst und den du nicht anrufen kannst. Das andere behält es im Haus, wo du siehst, wenn es scheitert, und es reparieren kannst.

Für die Haustür eines Hauses weiß ich, was mir lieber ist. Die Arbeit sind ein paar Dateien und ein Timer. Das Einzige, was es wirklich verlangt, ist also, dass du dich darum kümmerst, und das hieß es schon immer, etwas zu besitzen.


  1. Cloudflare — DDoS Protection — automatische Erkennung und Abwehr von DDoS, „Available on all plans“. ↩︎

  2. Caddy — tls directive, client_auth — require_and_verify: „Require clients to present a valid certificate that is verified“; „The one verifier, currently, shipped in standard Caddy is leaf“, der prüft, ob das Zertifikat „is one of a defined set of permitted certificates“. ↩︎ ↩︎

  3. nginx — ngx_http_ssl_module — ssl_verify_depth steht standardmäßig auf 1; ssl_crl: „When using intermediate certificates, their CRLs should be specified in the same file.“ ↩︎

  4. Home Assistant — HTTP integration — Reverse Proxys, vertrauenswürdige Proxys und IP-Sperren; Einstellungen in der Oberfläche seit 2026.8. ↩︎

  5. Home Assistant Companion — Getting Started, TLS client authentication — installieren als „VPN & app user certificate“; Wear OS braucht das Zertifikat als PKCS12 bei der Einrichtung. ↩︎ ↩︎

  6. home-assistant/android — network_security_config.xml — die Vertrauensanker sind system plus user: „Additionally trust user added CAs“. ↩︎

  7. Home Assistant Companion — Connection security level — Most secure benutzt eine unverschlüsselte URL nur im Heimnetz. ↩︎