El mismo candado, tu propia puerta

La app de Home Assistant a través de Cloudflare, cerrada con un certificado pone el candado en el borde de Cloudflare. El móvil tiene un certificado, Cloudflare lo comprueba, y nada que no lo tenga llega a la casa. Funciona, es gratis, y significa que Cloudflare guarda la puerta y lee el tráfico al pasar.

Esto es el mismo candado con la puerta en tu propia casa. nginx o Caddy comprueba el certificado. Tu propia autoridad de certificación lo emite. No hay nadie más en el camino. La página de inicio de sesión sigue siendo invisible para todo lo que no tenga certificado, así que lo que cambia no es si entran desconocidos. Es quién hace el trabajo, y quién carga con el riesgo cuando sale mal.

Todo lo de aquí se construyó y se probó: nginx 1.30.5 y Caddy 2.11.7 en Podman, certificados de step-ca 0.30.2, cada proxy atacado sin certificado, con un certificado bueno, con uno revocado y con un upgrade a WebSocket, por IPv4 e IPv6. El backend de la prueba era un pequeño sustituto que devuelve las cabeceras que recibe, así que los resultados muestran exactamente lo que le llegaría a Home Assistant.

Qué cambia cuando Cloudflare se va

mTLS en casa con nginx o CaddyMóviltiene cert.Cualquier otrosin cert.Inundaciónno necesita cert.Red de casaRouterpuerto 443sin escudoanti-DDoSnginx o Caddy¿llega a tu CA?¿no revocado?IPv4 e IPv6step-caHome Assistantconfía en el proxyRechazadoen el saludopasafalla
Ahora todo llega a tu router. El proxy rechaza todo lo que no traiga un certificado bueno antes de que Home Assistant lo vea. Una inundación no necesita certificado, y sin nada delante que la absorba, cae sobre tu línea.
Versión con Cloudflarenginx o Caddy en casa
Quién comprueba el certificadoEl borde de Cloudflare, con una regla WAFTu proxy, durante el saludo TLS
Autoridad de certificaciónGestionada por CloudflareLa tuya, step-ca para certificados de cliente
Quién ve el tráfico descifradoCloudflare, y luego Home AssistantTu proxy y Home Assistant, nadie más
Entrada desde fueraUn túnel, solo salienteUna redirección de puerto al proxy
Un DDoS cae sobreCloudflareTu línea de casa
Lo que cuesta la conexión de un desconocidoNada en casaUn saludo TLS en el proxy, nunca un inicio de sesión de Home Assistant
IPv4 e IPv6Las dos, Cloudflare responde por cualquieraLas dos si la línea tiene las dos y el proxy escucha en las dos
RevocaciónIntegrada, la comprueba la reglanginx comprueba una CRL; Caddy mantiene una lista de permitidos
Cuando el proveedor está caídoSin acceso remotoNo cambia nada
Coste de funcionamientoPlan gratuitoGratis, más tu tiempo con la CA

Las dos filas que más importan tiran en direcciones opuestas. Nadie más lee tu tráfico ni puede apagarte, y eso es soberanía, el motivo entero para hacerlo. Pero la redirección de puerto vuelve, así que los escaneos y las inundaciones dan contra tu línea en vez de contra la de Cloudflare. La diferencia con la redirección de puerto normal de la comparación de la entrada de Cloudflare es lo que encuentran al llegar: un proxy que corta la conexión durante el saludo, no la página de inicio de sesión de Home Assistant.

Ten claro lo que eso compra y lo que no. Tu propio proxy inverso no te da ninguna protección contra DDoS. mTLS impide que entre un desconocido. No hace nada para impedir que un desconocido te llene la línea, y una inundación no necesita certificado para llenarla. Cloudflare te absorbe eso en todos los planes, el gratuito incluido1. Aquí no hay nadie delante que lo absorba, así que una inundación lo bastante grande deja a toda la casa sin internet, y el proxy solo puede rechazar las conexiones que llega a ver.

nginx o Caddy

Los dos hacen el trabajo. Se diferencian en un sitio que importa. La revocación.

Revocación con nginx y con Caddynginx: una CRL, descargada por un temporizadorstep-carevoca, CRL nueva/1.0/crlHTTP normalTemporizadordescarga, recarganginxcomprueba la cadenaCaddy: una lista de permitidos, a manoTúen cada reemisiónPermitidoscerts. exactosRecargatras cada cambioCaddyverificador leaf
A nginx le da una CRL un temporizador y la comprueba en cada conexión. Caddy no admite CRL, así que la revocación es una lista de certificados exactos que mantienes a mano.
nginx 1.30.5Caddy 2.11.7
Sin certificadoEl saludo termina, 400 No required SSL certificate was sentSaludo rechazado, salida 55 de curl
Certificado revocadoRechazado, 400 The SSL certificate errorEntra sin más con solo la comprobación de la CA
Método de revocaciónUna CRL, comprobada en toda la cadenaUna lista de certificados permitidos, el verificador leaf
WebSocket para la appNecesita las cabeceras Upgrade y ConnectionFunciona sin configuración extra
IPv6Solo añadiendo listen [::]:443Escucha en las dos por defecto
ConfiguraciónUnas 30 líneasUnas 15 líneas

La revocación es lo que hay que decidir. La documentación de Caddy lista exactamente un verificador incluido en el Caddy estándar, leaf, y comprueba si el certificado «is one of a defined set of permitted certificates»2. No hay CRL ni OCSP para certificados de cliente. Apunta Caddy solo a la CA y un móvil revocado entra como si nada. La prueba mostró justo eso: phone2, revocado una hora antes, recibió un 200. La lista de permitidos cierra el hueco. También es una lista que mantienes a mano, mientras lo tengas funcionando.

La configuración de nginx

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;
    }
}
LíneaPor qué está ahí
listen [::]:443 sslSin ella nginx es solo IPv4, y un móvil en una red IPv6 no tiene dónde aterrizar
ssl_verify_client onSin certificado, no se entra. optional dejaría pasar la petición para juzgarla después
ssl_verify_depth 2step-ca firma desde un intermedio, así que la cadena es móvil, intermedio, raíz. El valor por defecto de 1 se queda corto3
ssl_client_certificateEl intermedio y la raíz en un archivo
ssl_crlLa CRL de step-ca y una CRL de la raíz en un archivo. nginx comprueba cada eslabón de la cadena, así que necesita las dos
Upgrade y ConnectionLa app habla con Home Assistant por un WebSocket. Sin estas la app carga y luego se queda ahí parada

El archivo de la CRL es la parte que requiere cuidado, y está todo en Certificados de cliente de step-ca: por qué se rechaza la CRL de step-ca hasta que una plantilla arregla su alcance, por qué nginx también quiere una CRL de la raíz, y el temporizador que la refresca antes de que caduque. Sáltate eso y nginx deja fuera a todos los móviles, los buenos incluidos.

Lo que devolvió la prueba:

ClienteIPv4IPv6
Sin certificado400 No required SSL certificate was sent400
phone1200, X-Forwarded-For: 127.0.0.1200, X-Forwarded-For: ::1
phone2, revocado400 The SSL certificate error400
phone1, upgrade a WebSocket200, Upgrade: websocket pasado200, Upgrade: websocket pasado

Antes de añadir listen [::]:8443, la petición IPv6 ni siquiera conectaba. Salida 7 de curl.

La configuración de Caddy

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 significa que el certificado tiene que estar y tiene que encadenar hasta la CA2. verifier leaf es la revocación. Solo entran los certificados de allowed-phones.pem, así que revocar un móvil significa sacar su certificado de ese archivo y recargar.

ClienteIPv4IPv6
Sin certificadoSaludo rechazadoSaludo rechazado
phone1, en la lista200, X-Forwarded-For: 127.0.0.1200, X-Forwarded-For: ::1
phone2, revocado, solo comprobación de CA200
phone2, fuera de la listaSaludo rechazadoSaludo rechazado
phone1, upgrade a WebSocket por HTTP/1.1200, Upgrade: websocket pasado

La lista de permitidos tiene su propia trampa, y la prueba se metió de lleno en ella. La lista guarda certificados exactos, no nombres. Vuelve a emitir el certificado de un móvil y el nuevo no está en la lista. phone1 se volvió a emitir a mitad de las pruebas y Caddy lo rechazó con tls alert bad certificate hasta que se reconstruyó la lista. Ahora cada renovación son dos trabajos. Emitir el certificado, y luego actualizar la lista. Olvida el segundo y el móvil se queda fuera para nada.

Caddy también registra enabling strict SNI-Host enforcement because TLS client auth is configured al arrancar. Es él haciendo lo correcto: una petición tiene que pedir el mismo nombre de host en el saludo que en la petición HTTP, para que nadie pueda pasar la comprobación de certificado de un sitio y luego pedir otro.

El lado de Home Assistant

Los mismos ajustes que en la versión de Cloudflare, apuntando a otro proxy. En Settings → System → Network → HTTP server4:

AjusteValorPor qué
Trust X-Forwarded-ForActivadoSin él Home Assistant rechaza las peticiones que pasan por proxy
Trusted proxiesSolo la dirección del proxy, IPv4 e IPv6, por ejemplo 192.0.2.20 y 2001:db8::20Solo esa máquina puede decir que reenvía en nombre de alguien
Enable IP banningActivadoLos bloqueos caen sobre la dirección real del cliente que pasa el proxy
Home Assistant URL, Internethttps://ha.example.comLa app la toma como su URL externa

Lista la dirección IPv6 del proxy además de la IPv4. La prueba muestra a nginx reenviando ::1 como dirección del cliente por IPv6, y en uso reenvía la que traiga el móvil. Si Home Assistant solo confía en la dirección IPv4 del proxy, toda petición que le llegue del proxy por IPv6 se rechaza.

El móvil

De step-ca, el móvil necesita tres archivos. Está todo en la entrada de certificados de cliente de step-ca, y la configuración de la propia CA en Tu propia autoridad de certificación.

ArchivoInstalar como
Certificado raízCertificado de CA
Certificado intermedioCertificado de CA
El archivo PKCS12 del móvilCertificado de usuario de VPN y aplicaciones5

El certificado de servidor del proxy también viene de step-ca. La app de Home Assistant para Android confía en los certificados de CA que instala el usuario6, así que lo acepta, y eso es un segundo candado, no una molestia. Un dispositivo que no tiene tu raíz no confía en el servidor, antes incluso de que se plantee la cuestión de su propio certificado.

Luego pon el nivel de seguridad de conexión de la app en Most secure, igual que en la versión de Cloudflare7.

Dónde se queda corto

CasoQué pasaQué hacer
Caduca la CRLnginx rechaza todos los móvilesRefrescarla con un temporizador, como en la entrada de step-ca
Caduca la CRL de la raízLo mismo, un año despuésUna fecha en la agenda
Caddy, móvil revocadoEntra salvo que esté fuera de la lista de permitidosMantener la lista, o usar nginx
Caddy, móvil con certificado nuevoFuera hasta que se actualiza la listaActualizar la lista con cada certificado
Línea de casa detrás de CGNAT, sin IPv6Nada puede llegar a la redirección de puertoIPv6, o la versión de Cloudflare
Una inundación dirigida a la línea de casaDeja la casa sin conexión, no solo Home AssistantLa versión de Cloudflare, si eso te preocupa más que un tercero
iPhone, Wear OSIgual que en la versión de Cloudflare: Android es la app documentada, Wear OS quiere el PKCS12 durante la configuración5Probarlo antes de depender de ello

Nada de esto está escondido. Cada cosa es un trabajo. Apúntalo con una fecha o un comando al lado y se queda en trabajo, no en sorpresa.

Que todo sea tuyo

La versión de Cloudflare y esta dejan fuera al mismo desconocido. La diferencia es en quién tienes que confiar mientras lo hacen.

Con Cloudflare confías en que una empresa guarde la puerta, descifre el tráfico y siga ofreciéndolo gratis. Con esto confías en ti mismo para refrescar una CRL, tener una lista en orden y darte cuenta cuando llega una fecha. Ninguna está libre de confianza. Una la reparte a alguien a quien no pagas nada y a quien no puedes llamar. La otra la deja en casa, donde puedes verla fallar y arreglarla.

Para la puerta de una casa, sé cuál prefiero. El trabajo son unos pocos archivos y un temporizador. Como tal, lo único que de verdad pide es que lo cuides, que es lo que siempre ha significado que algo sea tuyo.


  1. Cloudflare — DDoS Protection — detección y mitigación automática de 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», que comprueba que el certificado «is one of a defined set of permitted certificates». ↩︎ ↩︎

  3. nginx — ngx_http_ssl_module — ssl_verify_depth es 1 por defecto; ssl_crl: «When using intermediate certificates, their CRLs should be specified in the same file.» ↩︎

  4. Home Assistant — HTTP integration — proxies inversos, proxies de confianza y bloqueo de IP; ajustes en la interfaz desde la 2026.8. ↩︎

  5. Home Assistant Companion — Getting Started, TLS client authentication — instalar como «VPN & app user certificate»; Wear OS necesita que el certificado se entregue como PKCS12 durante la configuración inicial. ↩︎ ↩︎

  6. home-assistant/android — network_security_config.xml — los anclajes de confianza son system más user: «Additionally trust user added CAs». ↩︎

  7. Home Assistant Companion — Connection security level — Most secure solo usa una URL sin cifrar en la red de casa. ↩︎