Arquitectura real de certificados SSL multi-dominio con Caddy y Cloudflare (con código)
Gestionar certificados SSL en entornos multi-dominio (SaaS, plataformas de clientes, paneles) implica resolver un problema clave:
- Algunos dominios están bajo tu control (Cloudflare con API)
- Otros no (clientes externos)
- Algunos usan proxy (Cloudflare naranja)
- Otros no
En este artículo se muestra cómo construir una arquitectura robusta con Caddy que:
- Usa DNS-01 cuando es posible
- Usa HTTP-01 como fallback
- Evita fallos en renovación automática
Problema base
Caddy usa ACME (Let's Encrypt) y necesita validar dominios.
Pero:
Sin acceso al DNS → DNS-01 falla
Por tanto, necesitas una estrategia híbrida.
Objetivo de la arquitectura
Diseñar políticas TLS que:
- Usen DNS-01 para dominios conocidos (Cloudflare con token)
- Usen HTTP-01 para dominios externos
- Permitan fallback automático
Estructura de políticas TLS en Caddy (API)
Caddy permite definir múltiples políticas en:
Cada política puede tener:
subjectsissuers- configuración de challenge
Política específica (Cloudflare con DNS-01)
Ejemplo para dominios que controlas:
"subjects": ["cineart.es", "*.cineart.es"],
"issuers": [
{
"module": "acme",
"challenges": {
"dns": {
"provider": {
"name": "cloudflare",
"api_token": "{env.CF_API_TOKEN_ACCOUNT_1}"
}
}
}
}
]
}
Esto garantiza:
- Validación DNS-01
- Compatible con proxy naranja
- Soporte wildcard
Segunda cuenta de Cloudflare
"subjects": ["otrodominio.com"],
"issuers": [
{
"module": "acme",
"challenges": {
"dns": {
"provider": {
"name": "cloudflare",
"api_token": "{env.CF_API_TOKEN_ACCOUNT_2}"
}
}
}
}
]
}
Política catch-all (la clave)
Aquí está la parte importante:
"issuers": [
{
"module": "acme"
},
{
"module": "acme",
"challenges": {
"dns": {
"provider": {
"name": "cloudflare",
"api_token": "{env.CF_FALLBACK_TOKEN}"
}
}
}
}
]
}
Qué significa esto realmente
Orden de ejecución:
2. Si falla → intentar DNS-01 con Cloudflare
Esto permite:
- Dominios externos → HTTP-01 funciona
- Dominios Cloudflare conocidos → usan políticas específicas
- Fallback razonable
Automatización en PHP (ejemplo real)
Basado en tu sistema:
{
$policy = [
'issuers' => [
[
'module' => 'acme'
],
[
'module' => 'acme',
'challenges' => [
'dns' => [
'provider' => [
'name' => 'cloudflare',
'api_token' => getenv('CF_FALLBACK_TOKEN')
]
]
]
]
]
];
self::upsertPolicy($caddyApi, $policy);
}
Forzar regeneración de políticas
require_once 'app/Services/SystemService.php';
SystemService::ensureTlsCatchAllPolicy('http://localhost:2019');
"
Verificar configuración activa
Salida esperada:
Forzar renovación / emisión
-H "Content-Type: application/json" \
-d "$(curl -s localhost:2019/config/)"
Debug en tiempo real
Logs de Caddy:
Buscar:
challenge failed
http-01
dns-01
Caso real: dominio externo sin Cloudflare
Resultado:
- HTTP-01 funciona
- Certificado emitido correctamente
Caso real: dominio en Cloudflare (tu cuenta)
Resultado:
- HTTP-01 falla
- DNS-01 funciona (token disponible)
Caso problemático (cliente externo)
Resultado:
DNS-01 → sin acceso
→ ERROR
Mitigaciones
Opción 1
El cliente te da API token:
→ DNS-01 funciona
Opción 2
Cliente desactiva proxy:
Opción 3 (manual)
Certificado de origen de Cloudflare:
→ No ACME automático
Comparación con Nginx y Apache
Esto no es exclusivo de Caddy:
- Nginx + Certbot → mismo problema
- Apache HTTP Server → igual
Ejemplo Certbot HTTP-01:
Fallará detrás de proxy Cloudflare.
Buenas prácticas finales
- Siempre tener HTTP-01 como fallback
- Usar DNS-01 solo cuando tengas control del DNS
- Evitar dependencias de terceros sin acceso API
- Monitorizar certificados
- Diseñar pensando en multi-tenant
Conclusión
Una arquitectura correcta de TLS en entornos multi-dominio no depende del servidor, sino de:
- Control del DNS
- Presencia de proxies
- Estrategia de validación
Caddy permite resolver esto elegantemente mediante políticas dinámicas y múltiples issuers, pero los límites siguen siendo los del protocolo ACME.