Hetzelfde slot, je eigen deur

De Home Assistant-app via Cloudflare, vergrendeld met een certificaat zet het slot aan de rand van Cloudflare. De telefoon heeft een certificaat, Cloudflare controleert het, en alles zonder certificaat bereikt het huis nooit. Het werkt, het is gratis, en het betekent dat Cloudflare de poort bewaakt en het verkeer leest terwijl het passeert.

Dit is hetzelfde slot met de deur in je eigen huis. nginx of Caddy controleert het certificaat. Je eigen certificaatautoriteit geeft het uit. Niemand anders zit in het pad. De aanmeldpagina blijft onzichtbaar voor alles zonder certificaat, dus wat er verandert is niet of vreemden binnenkomen. Het is wie het werk doet, en wie het risico draagt als het misgaat.

Alles hier is gebouwd en getest: nginx 1.30.5 en Caddy 2.11.7 in Podman, certificaten van step-ca 0.30.2, elke proxy aangesproken zonder certificaat, met een goed certificaat, met een ingetrokken certificaat en met een WebSocket-upgrade, over IPv4 en IPv6. De backend in de test was een kleine invaller die de ontvangen headers terugstuurt, dus de resultaten laten precies zien wat Home Assistant zou krijgen.

Wat er verandert als Cloudflare wegvalt

mTLS thuis met nginx of CaddyTelefoonheeft een certIedereen andersgeen certEen floodheeft geen cert nodigThuisnetwerkRouterpoort 443geen DDoS-schildnginx of Caddyketen naar je CA?niet ingetrokken?IPv4 en IPv6step-caHome Assistantvertrouwt de proxyGeweigerdin de handshakedoorfaalt
Alles komt nu bij je router aan. De proxy weigert alles zonder goed certificaat voordat Home Assistant het ziet. Een flood heeft geen certificaat nodig, en zonder iets stroomopwaarts om hem op te vangen landt hij op je lijn.
Cloudflare-versienginx of Caddy thuis
Wie controleert het certificaatDe rand van Cloudflare, met een WAF-regelJe proxy, tijdens de TLS-handshake
CertificaatautoriteitBeheerd door CloudflareJe eigen, step-ca voor clientcertificaten
Wie ziet het verkeer ontsleuteldCloudflare, dan Home AssistantJe proxy en Home Assistant, verder niemand
Toegang van buitenafEen tunnel, alleen uitgaandEen port forward naar de proxy
Een DDoS landt opCloudflareJe thuislijn
De verbinding van een vreemde kostThuis nietsEen TLS-handshake op de proxy, nooit een aanmelding bij Home Assistant
IPv4 en IPv6Beide, Cloudflare antwoordt op allebeiBeide als de lijn beide heeft en de proxy op beide luistert
IntrekkingIngebouwd, gecontroleerd door de regelnginx controleert een CRL; Caddy houdt een toegestane lijst bij
Als de aanbieder plat ligtGeen toegang op afstandEr verandert niets
Lopende kostenGratis abonnementGratis, plus je tijd aan de CA

De twee rijen die het meest tellen trekken elk een andere kant op. Niemand anders leest je verkeer of kan je uitzetten, en dat is soevereiniteit, de hele reden om het te doen. Maar de port forward is terug, dus scans en floods raken jouw lijn in plaats van die van Cloudflare. Het verschil met de gewone port forward in de vergelijking van het Cloudflare-bericht is wat ze vinden als ze aankomen: een proxy die de verbinding tijdens de handshake laat vallen, niet de aanmeldpagina van Home Assistant.

Wees duidelijk over wat dat wel en niet oplevert. Je eigen reverse proxy geeft je helemaal geen DDoS-bescherming. mTLS houdt een vreemde buiten. Het doet niets om te voorkomen dat een vreemde je lijn vol trekt, en een flood heeft geen certificaat nodig om dat te doen. Cloudflare vangt dat voor je op bij elk abonnement, het gratis abonnement inbegrepen1. Hier zit er stroomopwaarts niemand om het op te vangen, dus een flood die groot genoeg is haalt het hele huis van het internet, en de proxy kan alleen verbindingen weigeren die hij te zien krijgt.

nginx of Caddy

Beide doen het werk. Ze verschillen op één plek die ertoe doet. Intrekking.

Intrekking met nginx en met Caddynginx: een CRL, opgehaald door een timerstep-caintrekken, nieuwe CRL/1.0/crlgewone HTTPTimerophalen, herladennginxcontroleert de ketenCaddy: een toegestane lijst, met de handJijbij elke heruitgifteToegestane lijstexacte certsHerladenna elke wijzigingCaddyleaf-verifier
nginx krijgt via een timer een CRL en controleert die bij elke verbinding. Caddy ondersteunt geen CRL, dus intrekking is een lijst met exacte certificaten die je met de hand bijhoudt.
nginx 1.30.5Caddy 2.11.7
Geen certificaatHandshake voltooid, 400 No required SSL certificate was sentHandshake geweigerd, curl exit 55
Ingetrokken certificaatGeweigerd, 400 The SSL certificate errorGewoon binnengelaten met alleen een CA-controle
IntrekkingsmethodeEen CRL, gecontroleerd over de hele ketenEen toegestane lijst van certificaten, de verifier leaf
WebSocket voor de appHeaders Upgrade en Connection moeten gezet wordenWerkt zonder extra config
IPv6Alleen met listen [::]:443 erbijLuistert standaard op beide
ConfigOngeveer 30 regelsOngeveer 15 regels

Intrekking is degene om op te beslissen. De documentatie van Caddy noemt precies één verifier die met standaard-Caddy wordt meegeleverd, leaf, en die controleert of het certificaat “is one of a defined set of permitted certificates”2. Er is geen CRL en geen OCSP voor clientcertificaten. Wijs Caddy alleen naar de CA en een ingetrokken telefoon loopt zo naar binnen. De test liet precies dat zien: phone2, een uur eerder ingetrokken, kreeg een 200. De toegestane lijst dicht het gat. Het is ook een lijst die je met de hand bijhoudt, zolang je het draait.

De nginx-config

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;
    }
}
RegelWaarom hij er staat
listen [::]:443 sslZonder deze regel is nginx alleen IPv4, en een telefoon op een IPv6-netwerk kan nergens landen
ssl_verify_client onGeen certificaat, geen toegang. optional zou het verzoek doorlaten om later te worden beoordeeld
ssl_verify_depth 2step-ca ondertekent vanaf een intermediate, dus de keten is telefoon, intermediate, root. De standaard van 1 houdt te vroeg op3
ssl_client_certificateDe intermediate en de root in één bestand
ssl_crlDe CRL van step-ca en een root-CRL in één bestand. nginx controleert elke schakel in de keten, dus hij heeft beide nodig
Upgrade en ConnectionDe app praat met Home Assistant over een WebSocket. Zonder deze headers laadt de app en blijft hij daarna hangen

Het CRL-bestand is het deel dat zorg vraagt, en alles daarover staat in Clientcertificaten van step-ca: waarom de CRL van step-ca wordt geweigerd tot een sjabloon de scope repareert, waarom nginx ook een CRL van de root wil, en de timer die hem ververst voordat hij verloopt. Sla dat over en nginx sluit elke telefoon buiten, de goede inbegrepen.

Wat de test terugkreeg:

ClientIPv4IPv6
Geen certificaat400 No required SSL certificate was sent400
phone1200, X-Forwarded-For: 127.0.0.1200, X-Forwarded-For: ::1
phone2, ingetrokken400 The SSL certificate error400
phone1, WebSocket-upgrade200, Upgrade: websocket doorgegeven200, Upgrade: websocket doorgegeven

Voordat listen [::]:8443 erin stond, maakte het IPv6-verzoek helemaal geen verbinding. curl exit 7.

De Caddy-config

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 betekent dat het certificaat aanwezig moet zijn en naar de CA moet ketenen2. verifier leaf is de intrekking. Alleen de certificaten in allowed-phones.pem komen binnen, dus een telefoon intrekken betekent zijn certificaat uit dat bestand halen en herladen.

ClientIPv4IPv6
Geen certificaatHandshake geweigerdHandshake geweigerd
phone1, op de lijst200, X-Forwarded-For: 127.0.0.1200, X-Forwarded-For: ::1
phone2, ingetrokken, alleen CA-controle200
phone2, niet op de lijstHandshake geweigerdHandshake geweigerd
phone1, WebSocket-upgrade over HTTP/1.1200, Upgrade: websocket doorgegeven

De toegestane lijst heeft een eigen valkuil, en de test liep er zo in. De lijst bevat exacte certificaten, geen namen. Geef het certificaat van een telefoon opnieuw uit en het nieuwe staat niet op de lijst. phone1 werd halverwege de tests opnieuw uitgegeven en Caddy weigerde hem met tls alert bad certificate tot de lijst opnieuw was opgebouwd. Elke vernieuwing is nu twee klussen. Geef het certificaat uit, en werk dan de lijst bij. Vergeet de tweede en de telefoon zit voor niks buitengesloten.

Caddy logt bij het starten ook enabling strict SNI-Host enforcement because TLS client auth is configured. Dat is hem die het juiste doet: een verzoek moet in de handshake om dezelfde hostnaam vragen als in het HTTP-verzoek, zodat niemand de certificaatcontrole voor de ene site kan halen en dan om een andere kan vragen.

De kant van Home Assistant

Dezelfde instellingen als bij de Cloudflare-versie, gericht op een andere proxy. Onder Settings → System → Network → HTTP server4:

InstellingWaardeWaarom
Trust X-Forwarded-ForAanZonder deze instelling weigert Home Assistant verzoeken via een proxy
Trusted proxiesAlleen het adres van de proxy, IPv4 en IPv6, bijvoorbeeld 192.0.2.20 en 2001:db8::20Alleen die machine mag beweren dat hij voor iemand doorstuurt
Enable IP banningAanBans komen terecht op het echte clientadres dat de proxy doorgeeft
Home Assistant URL, Internethttps://ha.example.comDe app neemt dit over als zijn externe URL

Zet het IPv6-adres van de proxy er ook in, naast het IPv4-adres. De test laat zien dat nginx over IPv6 ::1 doorgeeft als clientadres, en in gebruik geeft hij door waar de telefoon ook vandaan kwam. Als Home Assistant alleen het IPv4-adres van de proxy vertrouwt, wordt elk verzoek dat er via de proxy over IPv6 aankomt geweigerd.

De telefoon

Van step-ca heeft de telefoon drie bestanden nodig. Alles daarover staat in het bericht over clientcertificaten van step-ca, en het inrichten van de CA zelf in Je eigen certificaatautoriteit.

BestandInstalleren als
RootcertificaatCA-certificaat
Intermediate-certificaatCA-certificaat
Het PKCS12-bestand van de telefoonGebruikerscertificaat voor VPN en apps5

Het servercertificaat op de proxy komt ook van step-ca. De Android-app van Home Assistant vertrouwt CA-certificaten die de gebruiker installeert6, dus hij accepteert het, en dat is een tweede slot, geen last. Een apparaat zonder jouw root vertrouwt de server niet, nog voordat de vraag naar zijn eigen certificaat aan de orde komt.

Zet daarna het verbindingsbeveiligingsniveau van de app op Most secure, net als bij de Cloudflare-versie7.

Waar het tekortschiet

GevalWat er gebeurtWat te doen
CRL verlooptnginx weigert elke telefoonVervers hem op een timer, zoals in het step-ca-bericht
Root-CRL verlooptHetzelfde, een jaar laterEen datum in de agenda
Caddy, telefoon ingetrokkenKomt binnen tenzij hij van de toegestane lijst af isHoud de lijst bij, of gebruik nginx
Caddy, telefoon opnieuw uitgegevenBuitengesloten tot de lijst is bijgewerktWerk de lijst bij bij elk certificaat
Thuislijn achter carrier-grade NAT, geen IPv6Niets kan de port forward bereikenIPv6, of de Cloudflare-versie
Een flood gericht op de thuislijnHaalt het hele huis offline, niet alleen Home AssistantDe Cloudflare-versie, als je je daar meer zorgen om maakt dan om een derde partij
iPhone, Wear OSHetzelfde als bij de Cloudflare-versie: Android is de gedocumenteerde app, Wear OS wil de PKCS12 bij de installatie5Test het voordat je erop vertrouwt

Geen van deze is verborgen. Elk is een klus. Schrijf het op met een datum of een commando ernaast en het blijft een klus, geen verrassing.

Het hele ding zelf bezitten

De Cloudflare-versie en deze houden dezelfde vreemde buiten. Het verschil is wie je moet vertrouwen terwijl ze dat doen.

Met Cloudflare vertrouw je een bedrijf om de poort te bewaken, het verkeer te ontsleutelen en het gratis te blijven aanbieden. Hiermee vertrouw je jezelf om een CRL te verversen, een lijst op orde te houden en op te merken wanneer een datum langskomt. Geen van beide is vrij van vertrouwen. De een legt het bij iemand die je niets betaalt en niet kunt bellen. De ander houdt het in huis, waar je het kunt zien falen en kunt repareren.

Voor de voordeur van een huis weet ik wat ik liever heb. Het werk is een paar bestanden en een timer. Als zodanig vraagt het eigenlijk alleen dat je ervoor zorgt, en dat is wat iets bezitten altijd heeft betekend.


  1. Cloudflare — DDoS Protection — automatische detectie en mitigatie van 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”, die controleert of het certificaat “is one of a defined set of permitted certificates”. ↩︎ ↩︎

  3. nginx — ngx_http_ssl_module — ssl_verify_depth staat standaard op 1; ssl_crl: “When using intermediate certificates, their CRLs should be specified in the same file.” ↩︎

  4. Home Assistant — HTTP integration — reverse proxy’s, vertrouwde proxy’s en IP-bans; instellingen in de interface sinds 2026.8. ↩︎

  5. Home Assistant Companion — Getting Started, TLS client authentication — installeren als “VPN & app user certificate”; Wear OS heeft het certificaat als PKCS12 nodig tijdens de onboarding. ↩︎ ↩︎

  6. home-assistant/android — network_security_config.xml — vertrouwensankers zijn system plus user: “Additionally trust user added CAs”. ↩︎

  7. Home Assistant Companion — Connection security level — Most secure gebruikt alleen op het thuisnetwerk een onversleutelde URL. ↩︎