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:

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ÁmbitoPara qué sirve
IssuerUn namespaceConfigura una autoridad de certificación usable solo en su namespace
ClusterIssuerTodo el clústerIgual que Issuer, pero usable desde cualquier namespace
CertificateUn namespacePide un certificado para unos nombres y lo guarda en un Secret
CertificateRequest, Order, ChallengeUn namespaceRecursos 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

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.

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.