En corto
| |
cert-manager crea un Ingress temporal (cm-acme-http-solver-…) para responder al reto, pero sin clase tu ingress controller no lo adopta; la petición a /.well-known/acme-challenge/… la contesta el Ingress de la aplicación, que redirige a HTTPS con 308. Pon la clase en el solver:
| |
Aplica y borra el reto atascado para que lo reintente:
| |
Diagnóstico
| |
Antes de pedirle a Let’s Encrypt que valide, cert-manager hace un self-check: intenta él mismo la URL del reto. El Reason del Challenge es el resultado de ese self-check y casi siempre apunta a la causa.
Reason | Causa | Arreglo |
|---|---|---|
wrong status code '308' (o 301/302) | El Ingress del solver no tiene clase; responde el de la app con ssl-redirect | ingressClassName en el solver |
wrong status code '404' | El solver no está corriendo, o el DNS del host apunta a otra IP | kubectl get pods -n <ns> | grep acme-solver; dig app.example.com |
dial tcp … i/o timeout / connection refused | Desde dentro del clúster no se llega a la IP pública: NAT sin hairpin, DNS distinto dentro y fuera | NAT reflection o split DNS en el firewall (pfSense) |
tls: handshake failure | El Ingress de la app ya tiene TLS con un secreto vacío | Anotación cert-manager.io/issue-temporary-certificate: "true" |
wrong status code '503' | Varios Ingress con el mismo host y el controller no los combina | acme.cert-manager.io/http01-edit-in-place: "true" en el Ingress |
Por qué ingressClassName
Desde Kubernetes 1.18 la clase de un Ingress va en spec.ingressClassName; la anotación kubernetes.io/ingress.class quedó obsoleta y los controllers modernos (ingress-nginx 1.x, Traefik) pueden ignorarla. En cert-manager, el campo class del solver sigue escribiendo la anotación; ingressClassName (desde la 1.12) escribe el campo. Usa uno de los dos, y en 2026 el que toca es ingressClassName.
Sin clase, el Ingress temporal existe pero nadie lo sirve. El controller enruta la petición por host, encuentra el Ingress de la app, y aplica sus reglas: ssl-redirect → 308. De ahí que el error parezca de la app y no de cert-manager.
Alternativa: DNS-01 con Cloudflare
Cuando el clúster no es alcanzable desde Internet por el puerto 80, o quieres un wildcard, el reto va por DNS. Token en Cloudflare con Zone → DNS → Edit y Zone → Zone → Read sobre la zona:
| |
Con DNS-01 no hay Ingress temporal, ni self-check por HTTP, ni hairpin; el self-check consulta el registro TXT, y si el DNS del clúster resuelve contra un servidor interno con la zona cacheada, el Reason será DNS record for … not yet propagated: paciencia o --dns01-recursive-nameservers=1.1.1.1:53 en cert-manager.
Trampas
- Staging primero.
https://acme-staging-v02.api.letsencrypt.org/directoryno tiene límites de emisión; producción bloquea 5 emisiones por nombre y semana, y cada Challenge fallido que llega a Let’s Encrypt cuenta como intento. - Borrar el Challenge, no el Certificate. Borrar el
Ordero elChallengefuerza un reintento con la configuración nueva; borrar elCertificatetambién borra el secreto y deja la app sin TLS mientras tanto. pathType: Exacten el Ingress del solver (cert-manager reciente) choca con algunos controllers viejos; si ves 503 en OpenShift, es eso.- Un Issuer por namespace vs ClusterIssuer: el Ingress tiene que anotar el que exista (
cert-manager.io/cluster-issuerocert-manager.io/issuer); anotar el que no existe deja el Certificate enIssuingpara siempre sin Challenge que describir.
Referencias: HTTP01 solver, Cloudflare DNS01 y Troubleshooting ACME en la documentación de cert-manager.