--- title: "cert-manager en Kubernetes: TLS automático con Let's Encrypt" description: "Instala cert-manager con Helm, crea emisores de Let's Encrypt y obtén certificados TLS que se renuevan solos para tus Ingress, incluidos comodines por DNS-01." category: "Kubernetes Avanzado" language: es source: https://cubepath.com/es/docs/kubernetes-avanzado/cert-manager-para-tls-automatico-en-kubernetes updated: 2026-09-25 --- # cert-manager en Kubernetes: TLS automático con Let's Encrypt 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. - `kubectl` configurado 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, cambia `ingressClassName` por 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: ```bash kubectl get ingressclass dig +short app.your_domain ``` ```output NAME CONTROLLER PARAMETERS AGE traefik traefik.io/ingress-controller 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:///.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: ```bash 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`...): ```bash helm install cert-manager jetstack/cert-manager \ --namespace cert-manager \ --create-namespace \ --set crds.enabled=true ``` > Consejo: en producción añade `--version vX.Y.Z` con la versión que hayas probado. `helm search repo jetstack/cert-manager` muestra la última disponible. Comprueba que los tres componentes están en ejecución: el controlador, el webhook que valida los recursos y el `cainjector`: ```bash kubectl get pods -n cert-manager ``` ```output 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: ```bash nano letsencrypt-issuers.yaml ``` ```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: ```bash kubectl apply -f letsencrypt-issuers.yaml kubectl get clusterissuer ``` ```output 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: ```bash 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`. ```bash nano hello-ingress.yaml ``` ```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: ```bash kubectl apply -f hello-ingress.yaml kubectl get certificate -n web --watch ``` ```output 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: ```bash curl -skv https://app.your_domain 2>&1 | grep -E "issuer:|expire date:" ``` ```output * 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: ```bash 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: ```bash 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: ```bash curl -sv https://app.your_domain -o /dev/null 2>&1 | grep -E "issuer:|SSL certificate verify" ``` ```output * 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`: ```bash 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: ```bash nano letsencrypt-dns.yaml ``` ```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: ```bash nano wildcard-certificate.yaml ``` ```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 ``` ```bash 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. > Nota: un Secret solo puede usarse desde su propio namespace. Si necesitas el comodín en varios namespaces, crea un `Certificate` en 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: ```bash kubectl describe certificate hello-tls -n web | grep -E "Not After|Renewal Time" ``` ```output 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: ```bash 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: ```bash 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.