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
| Versión con Cloudflare | nginx o Caddy en casa | |
|---|---|---|
| Quién comprueba el certificado | El borde de Cloudflare, con una regla WAF | Tu proxy, durante el saludo TLS |
| Autoridad de certificación | Gestionada por Cloudflare | La tuya, step-ca para certificados de cliente |
| Quién ve el tráfico descifrado | Cloudflare, y luego Home Assistant | Tu proxy y Home Assistant, nadie más |
| Entrada desde fuera | Un túnel, solo saliente | Una redirección de puerto al proxy |
| Un DDoS cae sobre | Cloudflare | Tu línea de casa |
| Lo que cuesta la conexión de un desconocido | Nada en casa | Un saludo TLS en el proxy, nunca un inicio de sesión de Home Assistant |
| IPv4 e IPv6 | Las dos, Cloudflare responde por cualquiera | Las dos si la línea tiene las dos y el proxy escucha en las dos |
| Revocación | Integrada, la comprueba la regla | nginx comprueba una CRL; Caddy mantiene una lista de permitidos |
| Cuando el proveedor está caído | Sin acceso remoto | No cambia nada |
| Coste de funcionamiento | Plan gratuito | Gratis, 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.
| nginx 1.30.5 | Caddy 2.11.7 | |
|---|---|---|
| Sin certificado | El saludo termina, 400 No required SSL certificate was sent | Saludo rechazado, salida 55 de curl |
| Certificado revocado | Rechazado, 400 The SSL certificate error | Entra sin más con solo la comprobación de la CA |
| Método de revocación | Una CRL, comprobada en toda la cadena | Una lista de certificados permitidos, el verificador leaf |
| WebSocket para la app | Necesita las cabeceras Upgrade y Connection | Funciona sin configuración extra |
| IPv6 | Solo añadiendo listen [::]:443 | Escucha en las dos por defecto |
| Configuración | Unas 30 líneas | Unas 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ínea | Por qué está ahí |
|---|---|
listen [::]:443 ssl | Sin ella nginx es solo IPv4, y un móvil en una red IPv6 no tiene dónde aterrizar |
ssl_verify_client on | Sin certificado, no se entra. optional dejaría pasar la petición para juzgarla después |
ssl_verify_depth 2 | step-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_certificate | El intermedio y la raíz en un archivo |
ssl_crl | La 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 Connection | La 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:
| Cliente | IPv4 | IPv6 |
|---|---|---|
| Sin certificado | 400 No required SSL certificate was sent | 400 |
| phone1 | 200, X-Forwarded-For: 127.0.0.1 | 200, X-Forwarded-For: ::1 |
| phone2, revocado | 400 The SSL certificate error | 400 |
| phone1, upgrade a WebSocket | 200, Upgrade: websocket pasado | 200, 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.
| Cliente | IPv4 | IPv6 |
|---|---|---|
| Sin certificado | Saludo rechazado | Saludo rechazado |
| phone1, en la lista | 200, X-Forwarded-For: 127.0.0.1 | 200, X-Forwarded-For: ::1 |
| phone2, revocado, solo comprobación de CA | 200 | |
| phone2, fuera de la lista | Saludo rechazado | Saludo rechazado |
| phone1, upgrade a WebSocket por HTTP/1.1 | 200, 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:
| Ajuste | Valor | Por qué |
|---|---|---|
| Trust X-Forwarded-For | Activado | Sin él Home Assistant rechaza las peticiones que pasan por proxy |
| Trusted proxies | Solo la dirección del proxy, IPv4 e IPv6, por ejemplo 192.0.2.20 y 2001:db8::20 | Solo esa máquina puede decir que reenvía en nombre de alguien |
| Enable IP banning | Activado | Los bloqueos caen sobre la dirección real del cliente que pasa el proxy |
| Home Assistant URL, Internet | https://ha.example.com | La 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.
| Archivo | Instalar como |
|---|---|
| Certificado raíz | Certificado de CA |
| Certificado intermedio | Certificado de CA |
| El archivo PKCS12 del móvil | Certificado 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
| Caso | Qué pasa | Qué hacer |
|---|---|---|
| Caduca la CRL | nginx rechaza todos los móviles | Refrescarla con un temporizador, como en la entrada de step-ca |
| Caduca la CRL de la raíz | Lo mismo, un año después | Una fecha en la agenda |
| Caddy, móvil revocado | Entra salvo que esté fuera de la lista de permitidos | Mantener la lista, o usar nginx |
| Caddy, móvil con certificado nuevo | Fuera hasta que se actualiza la lista | Actualizar la lista con cada certificado |
| Línea de casa detrás de CGNAT, sin IPv6 | Nada puede llegar a la redirección de puerto | IPv6, o la versión de Cloudflare |
| Una inundación dirigida a la línea de casa | Deja la casa sin conexión, no solo Home Assistant | La versión de Cloudflare, si eso te preocupa más que un tercero |
| iPhone, Wear OS | Igual que en la versión de Cloudflare: Android es la app documentada, Wear OS quiere el PKCS12 durante la configuración5 | Probarlo 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.
Cloudflare — DDoS Protection — detección y mitigación automática de DDoS, «Available on all plans». ↩︎
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 isleaf», que comprueba que el certificado «is one of a defined set of permitted certificates». ↩︎ ↩︎nginx — ngx_http_ssl_module —
ssl_verify_depthes 1 por defecto;ssl_crl: «When using intermediate certificates, their CRLs should be specified in the same file.» ↩︎Home Assistant — HTTP integration — proxies inversos, proxies de confianza y bloqueo de IP; ajustes en la interfaz desde la 2026.8. ↩︎
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. ↩︎ ↩︎
home-assistant/android — network_security_config.xml — los anclajes de confianza son
systemmásuser: «Additionally trust user added CAs». ↩︎Home Assistant Companion — Connection security level — Most secure solo usa una URL sin cifrar en la red de casa. ↩︎