cert-manager es un controlador de Kubernetes que solicita certificados TLS a autoridades como Let's Encrypt, los guarda en Secrets y los renueva antes de que caduquen. Con él basta una anotación en un Ingress para publicar un servicio por HTTPS sin tocar un certificado a mano. En este tutorial instalarás cert-manager con Helm, configurarás emisores de Let's Encrypt de pruebas y de producción, publicarás una aplicación con TLS y obtendrás un certificado comodín mediante el desafío DNS-01.
Requisitos previos
Para seguir esta guía necesitas:
- Un clúster de Kubernetes en una versión soportada por la release de cert-manager que instales, por ejemplo sobre VPS de CubePath con Ubuntu 24.04.
kubectlconfigurado con permisos de administrador del clúster y Helm 3 instalado en tu equipo.- Un controlador de Ingress accesible desde Internet por los puertos 80 y 443. Esta guía usa Traefik, que k3s instala por defecto con la clase
traefik; si usas otro, cambiaingressClassNamepor el de tu controlador. - Un dominio con un registro DNS de tipo A (
app.your_domain) que apunte a la IP pública de tu controlador de Ingress. - Para el paso 6 (certificado comodín), que el DNS del dominio esté gestionado en Cloudflare y un token de API suyo.
Comprueba la clase de Ingress disponible y que el dominio resuelve a la IP correcta:
kubectl get ingressclass
dig +short app.your_domain
NAME CONTROLLER PARAMETERS AGE
traefik traefik.io/ingress-controller <none> 30d
203.0.113.10
Cómo funciona cert-manager
cert-manager añade varios recursos al clúster. Estos son los que usarás:
| Recurso | Ámbito | Para qué sirve |
|---|---|---|
Issuer | Un namespace | Configura una autoridad de certificación usable solo en su namespace |
ClusterIssuer | Todo el clúster | Igual que Issuer, pero usable desde cualquier namespace |
Certificate | Un namespace | Pide un certificado para unos nombres y lo guarda en un Secret |
CertificateRequest, Order, Challenge | Un namespace | Recursos intermedios que cert-manager crea durante la emisión; útiles para depurar |
Con Let's Encrypt (protocolo ACME), la autoridad comprueba que controlas el dominio con un desafío:
- HTTP-01: cert-manager publica un archivo temporal en
http://<dominio>/.well-known/acme-challenge/a través de tu Ingress. Es el más sencillo, pero no admite comodines y necesita el puerto 80 abierto. - DNS-01: cert-manager crea un registro TXT en tu DNS mediante la API del proveedor. Admite comodines (
*.your_domain) y no necesita exponer nada.
Paso 1: Instalar cert-manager con Helm
Añade el repositorio oficial de Jetstack, el equipo que mantiene cert-manager:
helm repo add jetstack https://charts.jetstack.io
helm repo update
Instala el chart en el namespace cert-manager. La opción crds.enabled=true hace que Helm instale también las CRD (Certificate, ClusterIssuer...):
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--set crds.enabled=true
Consejoen producción añade
--version vX.Y.Zcon la versión que hayas probado.helm search repo jetstack/cert-managermuestra la última disponible.
Comprueba que los tres componentes están en ejecución: el controlador, el webhook que valida los recursos y el cainjector:
kubectl get pods -n cert-manager
NAME READY STATUS RESTARTS AGE
cert-manager-7d9f8c6b5d-q8xkz 1/1 Running 0 50s
cert-manager-cainjector-5c7b9d8f6-2mlpv 1/1 Running 0 50s
cert-manager-webhook-6b8c7d9f5-vt4rn 1/1 Running 0 50s
Paso 2: Crear los emisores de Let's Encrypt
Let's Encrypt tiene dos entornos: staging, con límites de uso muy altos pero certificados no reconocidos por los navegadores, y producción. Prueba siempre primero con staging: si algo falla y repites muchas veces contra producción, alcanzarás sus límites y tendrás que esperar.
Crea un archivo con los dos ClusterIssuer. Sustituye admin@your_domain por tu correo, que Let's Encrypt asocia a la cuenta ACME:
nano letsencrypt-issuers.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: admin@your_domain
privateKeySecretRef:
name: letsencrypt-staging-account-key
solvers:
- http01:
ingress:
ingressClassName: traefik
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@your_domain
privateKeySecretRef:
name: letsencrypt-prod-account-key
solvers:
- http01:
ingress:
ingressClassName: traefik
privateKeySecretRef es el Secret donde cert-manager guarda la clave de la cuenta ACME, que crea automáticamente. Con ingressClassName, cert-manager crea un Ingress temporal de esa clase para responder al desafío HTTP-01.
Aplica el archivo y comprueba que ambos emisores han registrado su cuenta:
kubectl apply -f letsencrypt-issuers.yaml
kubectl get clusterissuer
NAME READY AGE
letsencrypt-prod True 10s
letsencrypt-staging True 10s
Si READY es False, kubectl describe clusterissuer letsencrypt-staging muestra el motivo, normalmente un error de red hacia Let's Encrypt.
Paso 3: Desplegar una aplicación de prueba
Necesitas un servicio al que apuntar el Ingress. Crea un namespace con un Nginx sencillo:
kubectl create namespace web
kubectl create deployment hello --image=nginx:stable --namespace web
kubectl expose deployment hello --port=80 --namespace web
kubectl -n web rollout status deploy/hello
Paso 4: Obtener un certificado de staging para el Ingress
cert-manager vigila los Ingress con la anotación cert-manager.io/cluster-issuer. Cuando la encuentra, crea un recurso Certificate con los nombres de la sección tls y guarda el certificado en el Secret indicado en secretName.
nano hello-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: hello
namespace: web
annotations:
cert-manager.io/cluster-issuer: letsencrypt-staging
spec:
ingressClassName: traefik
tls:
- hosts:
- app.your_domain
secretName: hello-tls
rules:
- host: app.your_domain
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: hello
port:
number: 80
Aplica el Ingress y sigue el estado del certificado. La emisión suele tardar menos de un minuto:
kubectl apply -f hello-ingress.yaml
kubectl get certificate -n web --watch
NAME READY SECRET AGE
hello-tls False hello-tls 5s
hello-tls True hello-tls 38s
Pulsa Ctrl+C cuando READY pase a True. Mientras tanto, si quieres ver el desafío en curso, ejecuta kubectl get challenges -n web en otra terminal.
Comprueba el certificado que sirve el Ingress. Como es de staging, curl no confía en él, así que usa -k para la prueba y fíjate en el emisor:
curl -skv https://app.your_domain 2>&1 | grep -E "issuer:|expire date:"
* expire date: Dec 23 09:41:12 2026 GMT
* issuer: C=US; O=(STAGING) Let's Encrypt; CN=(STAGING) Puzzling Parsnip E7
El nombre exacto del certificado intermedio puede variar, pero debe incluir (STAGING).
Paso 5: Pasar a certificados de producción
Una vez que staging funciona, cambia la anotación al emisor de producción:
kubectl annotate ingress hello -n web cert-manager.io/cluster-issuer=letsencrypt-prod --overwrite
El Secret todavía contiene el certificado de staging, que es válido, así que cert-manager no lo sustituiría hasta la renovación. Bórralo para forzar una emisión inmediata con el nuevo emisor:
kubectl delete secret hello-tls -n web
kubectl get certificate -n web --watch
Cuando READY vuelva a ser True, comprueba el certificado sin -k. Si curl no da error, el certificado es de confianza:
curl -sv https://app.your_domain -o /dev/null 2>&1 | grep -E "issuer:|SSL certificate verify"
* issuer: C=US; O=Let's Encrypt; CN=E7
* SSL certificate verify ok.
Para cualquier Ingress nuevo, basta con añadir la anotación cert-manager.io/cluster-issuer: letsencrypt-prod y una sección tls con su propio secretName.
Paso 6: Emitir un certificado comodín con DNS-01
Los certificados comodín (*.your_domain) solo se pueden validar por DNS. Este ejemplo usa Cloudflare; cert-manager también incluye soporte para Route 53, Google Cloud DNS, Azure DNS, DigitalOcean y otros, y para el resto existen webhooks externos.
En el panel de Cloudflare, crea un token de API con los permisos Zone - DNS - Edit y Zone - Zone - Read, limitado a tu zona. Guárdalo en un Secret. Para un ClusterIssuer, el Secret debe estar en el namespace cert-manager:
kubectl create secret generic cloudflare-api-token \
--namespace cert-manager \
--from-literal=api-token=your_cloudflare_api_token
Crea un ClusterIssuer que use DNS-01 con ese token:
nano letsencrypt-dns.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-dns
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@your_domain
privateKeySecretRef:
name: letsencrypt-dns-account-key
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
Ahora pide el certificado directamente con un recurso Certificate, sin Ingress. Incluye también el dominio raíz, porque *.your_domain no lo cubre:
nano wildcard-certificate.yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard
namespace: web
spec:
secretName: wildcard-tls
dnsNames:
- "your_domain"
- "*.your_domain"
issuerRef:
name: letsencrypt-dns
kind: ClusterIssuer
kubectl apply -f letsencrypt-dns.yaml -f wildcard-certificate.yaml
kubectl get certificate wildcard -n web --watch
La validación DNS tarda algo más que la HTTP, porque cert-manager espera a que el registro TXT se propague. Cuando esté listo, cualquier Ingress del namespace web puede usar el Secret wildcard-tls en su sección tls, sin anotación de cert-manager.
Notaun Secret solo puede usarse desde su propio namespace. Si necesitas el comodín en varios namespaces, crea un
Certificateen cada uno.
Paso 7: Comprobar la renovación automática
Los certificados de Let's Encrypt duran 90 días. Por defecto, cert-manager los renueva cuando ha pasado dos tercios de su validez, es decir, unos 30 días antes de que caduquen, sin intervención. Consulta las fechas de un certificado:
kubectl describe certificate hello-tls -n web | grep -E "Not After|Renewal Time"
Not After: 2026-12-23T09:52:40Z
Renewal Time: 2026-11-23T09:52:40Z
Si necesitas cambiar ese margen, añade renewBefore (por ejemplo renewBefore: 720h) en la spec de un Certificate. Revisa de vez en cuando que todos los certificados del clúster están listos:
kubectl get certificate -A
Solución de problemas
Cuando un certificado se queda en READY False, sigue la cadena de recursos de arriba abajo; el primero con error indica la causa:
kubectl describe certificate hello-tls -n web
kubectl get certificaterequest,order,challenge -n web
kubectl describe challenge -n web
El desafío HTTP-01 se queda en pending con connection refused o timeout. Let's Encrypt no llega al puerto 80 de tu Ingress. Comprueba el registro A del dominio, el firewall del servidor y que el controlador de Ingress escucha en el puerto 80. Desde fuera del clúster, curl -I http://app.your_domain/.well-known/acme-challenge/test debe llegar a tu Ingress (un 404 es correcto).
Error wrong status code '404' durante el desafío. El Ingress temporal de cert-manager no está siendo atendido por tu controlador. Revisa que ingressClassName en el ClusterIssuer coincide con kubectl get ingressclass.
El desafío DNS-01 falla con errores de autenticación. El token no tiene los permisos de la zona o el Secret está en un namespace distinto de cert-manager. Los registros del controlador dan el detalle: kubectl logs -n cert-manager deploy/cert-manager.
too many certificates already issued o errores de límite. Has alcanzado un límite de Let's Encrypt en producción. Espera al periodo indicado en el mensaje y haz las pruebas con el emisor de staging.
Conclusión
Tienes cert-manager instalado, emisores de Let's Encrypt de pruebas y de producción, un Ingress que obtiene y renueva su certificado con una sola anotación y un certificado comodín validado por DNS. Como siguientes pasos puedes redirigir todo el tráfico HTTP a HTTPS en tu controlador de Ingress, exportar las métricas de cert-manager a Prometheus para alertar de certificados no renovados o usar un Issuer de tipo CA para emitir certificados internos entre servicios.
