··
Para cerrar el puerto 80 a Let's Encrypt necesitaba renovar certs por DNS-01, y para que ninguna conexión TLS llegara al origen sin pasar por Cloudflare añadí Authenticated Origin Pulls. Cuento los dos cambios, las trampas que me encontré y el orden correcto cuando se hacen en la misma sesión.

En el primer post de la serie conté el porqué de meter el VPS detrás de Cloudflare. Toca entrar en el primer cambio técnico, que toca a Traefik por dos sitios. Primero, mover los certificados de Let's Encrypt de validación HTTP-01 a DNS-01 con la API de Cloudflare. Segundo, configurar Authenticated Origin Pulls para que el origen rechace cualquier handshake TLS que no presente el certificado cliente de Cloudflare.
Los dos cambios son independientes en la teoría, pero se hacen seguidos en la práctica y el orden importa. Si los pones al revés, te tiras un buen rato preguntándote por qué tu sitio devuelve 525 desde el navegador.
El reto HTTP-01 es el flujo por defecto de Let's Encrypt y funciona pidiendo a tu servidor que sirva un fichero concreto en http://tu-dominio/.well-known/acme-challenge/<token>. Eso obliga a tener el puerto 80 abierto al exterior, porque LE valida desde una IP que no controlas.
En cuanto decides cerrar el puerto 80 a todo lo que no sea Cloudflare (eso vendrá en el siguiente post), HTTP-01 deja de funcionar. La salida limpia es DNS-01, que valida creando un registro TXT en tu zona DNS. Como Cloudflare gestiona la zona, basta con un token de API con scope Zone:DNS:Edit y Traefik resuelve el reto sin necesitar un puerto abierto.
Lo creé desde el dashboard de CF en My Profile, API Tokens, con plantilla custom y permiso Zone DNS Edit sobre las dos zonas que voy a migrar. Nada más. Cuanto menos scope, menos daño si se filtra. El token vive en dos sitios.
Como variable de entorno CF_DNS_API_TOKEN dentro del contenedor de Traefik. Es la copia que el daemon usa día a día para resolver los retos DNS-01.
Como cache local en un fichero protegido 0600 root dentro del VPS, que sirve de respaldo para el monitor de auto-reinject del que hablo en el cuarto post.
La fuente de verdad real es mi gestor de secretos, Infisical. La copia local es una segunda línea para que el script de monitorización pueda actuar sin pedirle permiso a la red.
En la configuración estática de Traefik defino dos resolvers, el viejo HTTP-01 (que voy a ir abandonando router a router) y el nuevo DNS-01 con provider Cloudflare.
certificatesResolvers:
letsencrypt:
acme:
email: [email protected]
storage: /etc/dokploy/traefik/dynamic/acme.json
httpChallenge:
entryPoint: web
letsencrypt-dns:
acme:
email: [email protected]
storage: /etc/dokploy/traefik/dynamic/acme-dns.json
dnsChallenge:
provider: cloudflare
resolvers:
- 1.1.1.1:53
- 1.0.0.1:53Los dos resolvers conviven sin conflicto. El nuevo guarda los certs en un fichero distinto (acme-dns.json), así que no se pisan datos. Cada router elige el suyo.
Para mover un router del resolver viejo al nuevo hay que hacer dos cosas a la vez, cambiar la línea certResolver y forzar a Traefik a emitir un cert nuevo. Esto último tiene una trampa que descubrí en el primer intento.
Si te limitas a cambiar certResolver: letsencrypt por certResolver: letsencrypt-dns y reinicias Traefik, el cert no se re-emite. ¿Por qué? Porque el cert viejo del resolver HTTP-01 sigue siendo válido durante 90 días desde su última renovación, y Traefik lo sirve tal cual aunque hayas cambiado de resolver. La renovación con el nuevo resolver no se dispara hasta los 60 días aproximados de vida del cert.
La solución es purgar el cert del fichero acme.json antes de reiniciar, así Traefik se ve obligado a pedir uno nuevo y el siguiente que pida lo pide vía DNS-01 porque el resolver del router ya es el nuevo.
# Backup primero, siempre
sudo cp /etc/dokploy/traefik/dynamic/acme.json acme.json.bak-$(date +%Y%m%d-%H%M%S)
# Borrar entradas del cert viejo
sudo python3 -c "
import json
with open('/etc/dokploy/traefik/dynamic/acme.json') as f:
d = json.load(f)
to_remove = {'mi-dominio.com', 'www.mi-dominio.com'}
for r, c in d.items():
certs = c.get('Certificates') or []
c['Certificates'] = [x for x in certs if x.get('domain', {}).get('main') not in to_remove]
with open('/etc/dokploy/traefik/dynamic/acme.json', 'w') as f:
json.dump(d, f, indent='\t')
"
# Reiniciar Traefik para que pida el cert nuevo via DNS-01
sudo docker restart dokploy-traefikEn 20 a 30 segundos Cloudflare propaga el TXT, Let's Encrypt valida y el cert nuevo aparece en acme-dns.json. Verifico que la fecha del cert es de hoy.
echo | openssl s_client -connect mi-dominio.com:443 -servername mi-dominio.com 2>/dev/null \
| openssl x509 -noout -issuer -dates.bak dentro de dynamic/Traefik vigila la carpeta dynamic/ con un watcher de ficheros, y si dejas el backup como acme.json.bak-... dentro de la misma carpeta, lo intenta cargar como configuración válida. Resultado, conflictos y errores en los logs. Mueve los backups fuera de esa carpeta a una ubicación que Traefik no vigile.
Con DNS-01 funcionando, los certificados se renuevan sin abrir el puerto 80. Pero el origen sigue aceptando handshake TLS de cualquier cliente, no solo de Cloudflare. AOP cierra ese hueco.
La idea es que Cloudflare presente un certificado cliente firmado por una CA de Cloudflare durante el handshake TLS al origen, y el origen rechace cualquier handshake que no traiga ese cert. Cloudflare publica la CA pública y la operación es gratuita en cualquier plan.
Descargo la CA de la documentación oficial de CF y la guardo dentro del directorio dynamic/ de Traefik. Defino una tls.options que la usa.
tls:
options:
cloudflare-aop:
clientAuth:
caFiles:
- /etc/dokploy/traefik/dynamic/cloudflare-origin-pull-ca.pem
clientAuthType: RequireAndVerifyClientCertEl clientAuthType: RequireAndVerifyClientCert es la parte importante. RequestClientCert pide el cert pero no rechaza si no llega. RequireAndVerifyClientCert sí.
El router del proyecto referencia esta opción. Variante para router file provider.
routers:
mi-app-websecure:
rule: Host(`mi-dominio.com`)
entryPoints:
- websecure
service: mi-app-service
tls:
certResolver: letsencrypt-dns
options: cloudflare-aop@fileVariante para router docker provider. Misma idea, expresada como label del compose.
traefik.http.routers.mi-app-websecure.tls.options=cloudflare-aop@fileEn el dashboard de CF, dentro de la zona, voy a SSL/TLS, Origin Server, Authenticated Origin Pulls, y activo el toggle Global. Cada zona requiere su propio toggle. La opción Global usa la CA compartida de Cloudflare, que es exactamente la que descargué para Traefik. Las opciones A nivel de zona o Por hostname son para certificados custom subidos al dashboard, no es lo que quiero aquí.
Esta es la lección que pagué con un susto. Si haces los dos cambios juntos, la secuencia segura es esta.
Editar el yml del router con solo certResolver: letsencrypt-dns. Sin AOP todavía.
Poner la nube naranja en el registro DNS. A partir de ese momento las peticiones empiezan a viajar por CF.
Purgar el cert viejo de acme.json y reiniciar Traefik. El cert nuevo se emite vía DNS-01.
Verificar que vía Cloudflare la página responde 200 y que curl -sI muestra server: cloudflare.
Ahora sí, añadir options: cloudflare-aop@file al router. El watcher recoge el cambio en caliente, sin restart.
Verificar el bypass directo, debe fallar en el handshake TLS.
El error que casi me costó downtime fue añadir options: cloudflare-aop@file en el paso 1, antes de la nube naranja. Mientras la nube siga gris, los visitantes siguen yendo directos al VPS por DNS público. Como el router exige cert cliente de la CA de CF y los visitantes no lo presentan, el handshake falla y la app cae para todo el mundo. Lo detecté en segundos y lo revertí, pero deja claro que no es lo mismo el orden lógico que el orden seguro.
Dos comandos. El primero comprueba que vía Cloudflare la web sigue funcionando.
curl -sI https://mi-dominio.com
# debe responder 200/301/307 con server: cloudflareEl segundo intenta saltarse Cloudflare conectando directamente a la IP del VPS pero forzando el SNI correcto.
curl -v --resolve mi-dominio.com:443:<IP-del-VPS> https://mi-dominio.com
# debe terminar en error TLS, algo como:
# OpenSSL/3.x: error:1404C45C:SSL routines:tls_process_server_certificate:tlsv13 alert certificate requiredSi en lugar de error TLS te devuelve un 421 Misdirected Request, no probaste lo que crees. La IP pura sin SNI hace que Traefik responda con un cert por defecto distinto, sin entrar en el router del proyecto. Hay que usar --resolve o algo equivalente para forzar el SNI correcto.
El plan Free de Cloudflare incluye Universal SSL, que cubre el apex del dominio y un nivel de subdominios. No cubre sub-subdominios. Si tienes x.y.tu-dominio.com con nube naranja, el cliente recibe error de cert porque CF no tiene cert emitido para ese hostname.
Las opciones son tres. Pagar Advanced Certificate Manager (10 euros al mes por zona, emite certs a cualquier profundidad), renombrar el subdominio a un solo nivel (mi solución, transparente para servicios admin internos), o dejarlo en nube gris (sin protección de CF para ese dominio). Yo opté por renombrar, perdí solo unos minutos cambiando variables de entorno y un registro DNS.
Si algo va mal en la migración de un proyecto concreto, el rollback es rápido. Quitar options: cloudflare-aop@file del router. Volver a poner certResolver: letsencrypt. Restaurar el acme.json del backup. Cambiar la nube a gris en CF. Restart de Traefik. En menos de dos minutos el proyecto vuelve al estado anterior.
Con DNS-01 y AOP funcionando, ya tengo dos de las cuatro capas que mencioné en el post de motivación. Cloudflare valida los visitantes en su edge, y mi origen exige que cualquier conexión TLS venga firmada por la CA de Cloudflare. Falta cerrar el puerto 80 y 443 a todo lo que no sea Cloudflare a nivel de kernel, y eso es lo que viene en el siguiente post.

Jose, autor del blog
QA Engineer. Escribo en voz alta sobre automatización, IA y arquitectura de software. Si algo te ha servido, escríbeme y cuéntamelo.
Los comentarios están cerrados en este post.
Si esto te ha gustado

ScamDetector combina inteligencia artificial, búsqueda de reputación de teléfonos y escaneo de URLs para ayudarte a identificar estafas digitales. Sin registro, sin datos almacenados.

Repaso completo de las medidas de seguridad que puedes aplicar a un VPS Linux: desde CrowdSec y el firewall hasta el hardening del kernel, pasando por SSH, Docker y las actualizaciones automáticas.

Nuestros posts viven en una base de datos SQLite. Si alguien accede a ella, puede cambiar cualquier artículo sin dejar rastro. Construimos un verificador externo con hashes SHA-256 y firma Ed25519 que vigila la integridad desde un segundo servidor.