En la lección anterior publicamos www.rutasnorte.example y api.rutasnorte.example con un único Ingress, y cerramos señalando el problema que no admite demora: todo viaja en HTTP en claro. El nombre, el DNS, el teléfono y el correo que un cliente escribe para reservar un billete, y los datos que van hacia la pasarela de pagos, están cruzando internet sin cifrar. Esta lección pone HTTPS en los dos dominios: primero a mano con un certificado autofirmado para entender las piezas, y después de forma automática con cert-manager, que emitirá y renovará certificados de Let's Encrypt sin que nadie tenga que acordarse de nada.

Advertencia de seguridad. Esta lección enseña la mecánica de Kubernetes para gestionar certificados. La configuración criptográfica real de una plataforma en producción —conjuntos de cifrado, versiones de TLS permitidas, políticas de HSTS, custodia de claves privadas, requisitos de cumplimiento normativo— debe revisarla y aprobarla un profesional de seguridad. Los valores que aparecen aquí son didácticos y razonables en 2026, pero envejecen: no los copies a producción sin validarlos con quien corresponda en tu organización.

Contenido

  1. Por qué HTTPS es obligatorio en Rutas Norte
  2. Dónde termina el TLS en un clúster
  3. El Secret kubernetes.io/tls y el bloque tls: del Ingress
  4. Certificado autofirmado para desarrollo
  5. cert-manager: qué problema resuelve e instalación
  6. Los objetos de cert-manager y el flujo de emisión
  7. ACME con Let's Encrypt: HTTP-01 frente a DNS-01
  8. Emisión automática desde el Ingress
  9. El entorno de staging y los límites de emisión
  10. Renovación automática y comprobación del estado
  11. Redirección a HTTPS y HSTS
  12. Más allá: mTLS y mallas de servicio

  1. Por qué HTTPS es obligatorio en Rutas Norte

No es una buena práctica: es un requisito.

Motivo Qué pasa sin TLS
Datos personales Nombre, DNI, teléfono y correo viajan legibles por cualquier red intermedia. En el RGPD, el cifrado en tránsito es una medida técnica esperable
Pagos PCI DSS exige cifrado del canal. Ninguna pasarela seria acepta integrarse por HTTP
Sesiones Un JWT o una cookie interceptada en una wifi pública es una cuenta robada
Integridad Un intermediario puede modificar la respuesta: inyectar scripts, cambiar el importe
Navegadores y SEO Marcan "No seguro", bloquean APIs (geolocalización, service workers) y los buscadores penalizan HTTP. La tienda parecería fraudulenta

Y una precisión: TLS no solo cifra, también autentica. El certificado demuestra que quien responde en www.rutasnorte.example es realmente Rutas Norte y no alguien que ha secuestrado el DNS. Sin esa parte, el cifrado sería con un desconocido.

  1. Dónde termina el TLS en un clúster

"Terminar el TLS" significa descifrar: el punto donde el tráfico deja de estar cifrado. Hay tres opciones.

flowchart LR
    CA["Cliente"] -->|HTTPS| IA["A. Ingress:<br/>descifra aqui"]
    IA -->|"HTTP en claro"| SA["Pods"]
    CB["Cliente"] -->|HTTPS| IB["B. Ingress: descifra<br/>y vuelve a cifrar"]
    IB -->|HTTPS| SB["Pods con certificado propio"]
    CC["Cliente"] -->|HTTPS| IC["C. Ingress: NO descifra,<br/>reenvia por SNI"]
    IC -->|HTTPS| SC["El pod termina el TLS"]
Modelo Ventajas Inconvenientes Cuándo
A. Terminación en el Ingress Un solo sitio con certificados; las aplicaciones no saben de TLS; el controlador puede enrutar por ruta y cabeceras El tráfico interno va en claro dentro del clúster Por defecto, y lo que usa Rutas Norte
B. Reencriptado Cifrado también dentro del clúster Cada aplicación gestiona su certificado Entornos regulados, redes no confiables
C. Passthrough El certificado nunca sale de la aplicación El Ingress no ve HTTP: sin enrutado por ruta, sin reescritura mTLS estricto, protocolos opacos

Rutas Norte usa A. El tráfico interno en claro se compensa con las políticas de red de 04-06, y si hiciera falta cifrado dentro del clúster, la respuesta sería una malla de servicio (08-04).

  1. El Secret kubernetes.io/tls y el bloque tls: del Ingress

Un certificado en Kubernetes es un Secret de tipo kubernetes.io/tls con exactamente dos claves: tls.crt, la cadena de certificados en PEM (el del servidor primero, luego los intermedios), y tls.key, la clave privada en PEM. El tipo obliga a que ambas existan.

Recuerda de 03-02: eso es base64, no cifrado, y cualquiera con permiso de lectura sobre Secrets en ese namespace tiene la clave privada de la plataforma; es el argumento más contundente para el RBAC de 08-01 y para el cifrado en reposo de etcd. El Ingress lo consume con el bloque tls::

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: rutas-norte
  namespace: rutas-norte-pro
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - www.rutasnorte.example
        - api.rutasnorte.example
      secretName: rutasnorte-tls      # Secret del MISMO namespace que el Ingress
  rules:
    # ... las mismas reglas de 04-04, sin cambios

Tres reglas que rompen esto si no se cumplen: el Secret debe estar en el mismo namespace que el Ingress (no hay referencias entre namespaces, así que publicar los tres entornos exige el certificado en los tres, y cert-manager puede emitirlo en cada uno); los hosts del bloque tls deben coincidir con los del certificado, o el navegador dará ERR_CERT_COMMON_NAME_INVALID; y se pueden poner varias entradas tls, cada una con su Secret, eligiendo el controlador por SNI, el nombre que el cliente anuncia al iniciar el saludo TLS.

  1. Certificado autofirmado para desarrollo

Para rutas-norte-dev no pedimos certificados a una autoridad pública: el dominio no existe y el clúster es local. Generamos uno propio.

# Clave privada de 2048 bits y certificado autofirmado a 365 dias
openssl req -x509 -nodes -newkey rsa:2048 -days 365 \
  -keyout /tmp/rutasnorte-dev.key \
  -out    /tmp/rutasnorte-dev.crt \
  -subj "/C=ES/ST=Cantabria/L=Santander/O=Rutas Norte S.L./CN=www.rutasnorte.example" \
  -addext "subjectAltName=DNS:www.rutasnorte.example,DNS:api.rutasnorte.example"

Desglose: -x509 genera un certificado directamente en vez de una solicitud (CSR); -nodes deja la clave privada sin contraseña, imprescindible porque nadie va a teclearla al arrancar; -newkey rsa:2048 crea clave y certificado en un paso; -subj da los datos del titular sin preguntar; y -addext subjectAltName añade el campo que de verdad importa.

Sobre subjectAltName (SAN): los navegadores llevan años ignorando el CN y validando exclusivamente las SAN, de modo que un certificado sin SAN se rechaza aunque el CN sea perfecto —es el error número uno al generar certificados a mano—. Aquí necesitamos dos, una por dominio, para que el mismo certificado sirva a la tienda y a la API.

kubectl create secret tls rutasnorte-tls \
  --cert=/tmp/rutasnorte-dev.crt --key=/tmp/rutasnorte-dev.key -n rutas-norte-dev

# Verificar que las SAN son las esperadas
kubectl get secret rutasnorte-tls -n rutas-norte-dev \
  -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -noout -text \
  | grep -A1 "Subject Alternative Name"

curl -k -s -o /dev/null -w '%{http_code}\n' https://www.rutasnorte.example/
curl    -s https://www.rutasnorte.example/ 2>&1 | head -1
X509v3 Subject Alternative Name:
    DNS:www.rutasnorte.example, DNS:api.rutasnorte.example
200
curl: (60) SSL certificate problem: self-signed certificate

kubectl create secret tls pone el tipo correcto y las dos claves con los nombres correctos.

Con -k funciona; sin -k, falla. Eso es correcto y es la lección: el certificado cifra igual de bien, pero nadie puede verificar quién lo emitió, y el navegador mostrará "La conexión no es privada" con NET::ERR_CERT_AUTHORITY_INVALID. Un cliente que viera ese aviso no compraría. Por eso los autofirmados sirven para desarrollo y pruebas internas, nunca para producción.

  1. cert-manager: qué problema resuelve e instalación

Con certificados de una autoridad pública, el proceso manual es: generar clave y CSR, demostrar que controlas el dominio, recibir el certificado, crear el Secret, y repetirlo cada 90 días, que es lo que duran los de Let's Encrypt. El resultado previsible es el incidente clásico: un sábado por la noche caduca el certificado, la tienda deja de funcionar en pleno puente, y nadie recuerda cómo se renovaba porque lo hizo una persona que ya no está.

cert-manager convierte eso en un bucle de reconciliación como cualquier otro: declaras que quieres un certificado y él lo obtiene, lo guarda en un Secret y lo renueva antes de que caduque, indefinidamente.

# Instalacion con los CRDs incluidos
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.16.2/cert-manager.yaml
kubectl get pods -n cert-manager
kubectl get crds | grep cert-manager.io
cert-manager-6d8b9c7f4d-jk29p             1/1   Running   0   60s
cert-manager-cainjector-5f9b7d8c6-w4xr2   1/1   Running   0   60s
cert-manager-webhook-7c4d9f8b5-mn6vt      1/1   Running   0   60s

certificaterequests.cert-manager.io    certificates.cert-manager.io
challenges.acme.cert-manager.io        clusterissuers.cert-manager.io
issuers.cert-manager.io                orders.acme.cert-manager.io

Tres componentes: cert-manager es el controlador que observa los Certificate y ejecuta el flujo de emisión; cainjector inyecta paquetes de CA en webhooks y APIServices; y webhook valida y aplica valores por defecto a los recursos propios.

Seis CRDs (06-06): cert-manager extiende la API de Kubernetes con tipos propios y un controlador que los reconcilia. Es el patrón operador de 06-07 en estado puro, y probablemente el ejemplo más útil que vas a encontrar.

  1. Los objetos de cert-manager y el flujo de emisión

Objeto Ámbito Quién lo crea Qué es
Issuer Namespace Fuente de certificados válida solo en ese namespace
ClusterIssuer Clúster Tú (administrador) Fuente utilizable desde cualquier namespace
Certificate Namespace Tú, o el Ingress con la anotación "Quiero un certificado para estos dominios, en este Secret"
CertificateRequest / Order / Challenge Namespace cert-manager La petición con su CSR, el pedido ACME y cada desafío a superar

Los tres últimos son internos: no se escriben a mano, pero son donde se lee qué está fallando, siguiendo la cadena CertificateCertificateRequestOrderChallenge.

sequenceDiagram
    participant U as Tu (o el Ingress)
    participant CM as cert-manager
    participant LE as Let's Encrypt (ACME)
    participant K8S as API de Kubernetes

    U->>K8S: crea Certificate (dominios + secretName)
    CM->>K8S: observa el Certificate
    CM->>CM: genera clave privada y CSR
    CM->>K8S: crea CertificateRequest
    CM->>K8S: crea Order
    CM->>LE: solicita el pedido para los dominios
    LE-->>CM: desafios pendientes (HTTP-01 o DNS-01)
    CM->>K8S: crea Challenge
    CM->>K8S: publica la prueba (Ingress temporal o registro TXT)
    CM->>LE: "ya puedes validar"
    LE->>LE: comprueba la prueba desde internet
    LE-->>CM: validado; aqui tienes el certificado
    CM->>K8S: crea/actualiza el Secret kubernetes.io/tls
    CM->>K8S: Certificate con Ready=True
    Note over CM: y programa la renovacion a los 60 dias

  1. ACME con Let's Encrypt: HTTP-01 frente a DNS-01

ACME (Automatic Certificate Management Environment) es el protocolo que automatiza la emisión. Su núcleo es una pregunta: ¿demuestras que controlas el dominio?

Desafío HTTP-01

Let's Encrypt pide publicar un fichero con un contenido concreto en http://<dominio>/.well-known/acme-challenge/<token>; cert-manager crea un pod y un Ingress temporal para servirlo, y lo borra al terminar.

# k8s/base/clusterissuer-letsencrypt-http01.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-produccion
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: [email protected]      # avisos de caducidad y problemas
    privateKeySecretRef:
      name: letsencrypt-produccion-cuenta   # clave de la CUENTA ACME, no del certificado
    solvers:
      - http01:
          ingress:
            ingressClassName: nginx

Requisito imprescindible: el dominio debe resolver a tu Ingress y ser alcanzable desde internet en el puerto 80. Consecuencias directas: no funciona en minikube con dominios .example resueltos por /etc/hosts (por eso en dev usamos autofirmados); no funciona para servicios internos sin exposición pública; no puede emitir certificados comodín; y si rediriges todo HTTP a HTTPS debes excluir /.well-known/acme-challenge/ —los controladores modernos lo gestionan solos, pero con reglas propias es un fallo habitual.

Desafío DNS-01

Let's Encrypt pide un registro TXT en _acme-challenge.<dominio>. cert-manager lo crea usando la API del proveedor de DNS y lo borra después.

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-dns
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: [email protected]
    privateKeySecretRef:
      name: letsencrypt-dns-cuenta
    solvers:
      - dns01:
          cloudflare:
            email: [email protected]
            apiTokenSecretRef:
              name: cloudflare-token       # Secret con el token de la API del DNS
              key: token
        selector:
          dnsZones:
            - rutasnorte.example           # este solver solo para esta zona
HTTP-01 DNS-01
Qué se publica Un fichero en /.well-known/acme-challenge/ Un registro TXT
Requiere puerto 80 público No
Funciona con servicios internos No
Certificados comodín No Sí, obligatorio
Credenciales necesarias Ninguna Token de la API del DNS
Velocidad Segundos Minutos (propagación del DNS)
Riesgo Ninguno especial Un token con permiso para editar tu zona DNS

Regla práctica: HTTP-01 por defecto, y DNS-01 cuando necesites comodines o el servicio no sea público. Rutas Norte usaría HTTP-01 para sus dos dominios; si mañana quisiera *.rutasnorte.example para dar un subdominio a cada agencia, tendría que pasar a DNS-01.

  1. Emisión automática desde el Ingress

Se puede crear un Certificate explícito, útil cuando se quieren controlar la duración, el algoritmo de clave o la rotación:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: rutasnorte-tls
  namespace: rutas-norte-pro
spec:
  secretName: rutasnorte-tls          # el Secret que se creará
  duration: 2160h                     # 90 días
  renewBefore: 720h                   # renovar 30 días antes de caducar
  privateKey:
    algorithm: ECDSA
    size: 256
    rotationPolicy: Always            # clave nueva en cada renovación
  dnsNames: [www.rutasnorte.example, api.rutasnorte.example]
  issuerRef:
    name: letsencrypt-produccion
    kind: ClusterIssuer

Pero lo cómodo es la anotación en el Ingress: cert-manager la detecta y genera el Certificate por ti a partir del bloque tls:.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: rutas-norte
  namespace: rutas-norte-pro
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-produccion"
    # para un Issuer de namespace seria: cert-manager.io/issuer
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - www.rutasnorte.example
        - api.rutasnorte.example
      secretName: rutasnorte-tls      # cert-manager creará este Secret
  rules:
    # ... las mismas reglas de 04-04

Toda la gestión de certificados de Rutas Norte se reduce a una anotación: los dominios salen de tls.hosts y el destino, de tls.secretName.

kubectl apply -f k8s/entornos/pro/ingress-rutas-norte.yaml
kubectl get certificate,order,challenge -n rutas-norte-pro
certificate.cert-manager.io/rutasnorte-tls              False     rutasnorte-tls           12s
order.acme.cert-manager.io/rutasnorte-tls-1-2894732     pending                            11s
challenge.acme.cert-manager.io/rutasnorte-tls-1-...-0   pending   www.rutasnorte.example   10s

# ... unos segundos despues
certificate.cert-manager.io/rutasnorte-tls              True      rutasnorte-tls           47s

READY: True y los Order/Challenge desaparecidos: el certificado está en el Secret y el controlador de Ingress ya lo está sirviendo.

  1. El entorno de staging y los límites de emisión

Let's Encrypt impone límites estrictos: 50 certificados por dominio registrado y semana, 5 fallos de validación por cuenta, dominio y hora, 5 duplicados exactos por semana (mismo conjunto de dominios) y 100 nombres por certificado.

El de duplicados es el que muerde: cinco intentos con el mismo conjunto de dominios y quedas bloqueado una semana; depurando una configuración nueva se consumen en diez minutos y no hay forma de acelerarlo. Por eso existe el entorno de staging, con los mismos límites multiplicados por mucho y certificados emitidos por una CA de pruebas que los navegadores no reconocen.

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  acme:
    server: https://acme-staging-v02.api.letsencrypt.org/directory   # la unica diferencia
    email: [email protected]
    privateKeySecretRef:
      name: letsencrypt-staging-cuenta
    solvers:
      - http01:
          ingress:
            ingressClassName: nginx

El procedimiento correcto, sin excepciones: (1) configurar el Ingress con cert-manager.io/cluster-issuer: letsencrypt-staging; (2) comprobar kubectl get certificate hasta READY: True; (3) verificar con curl -k que el TLS funciona y que el emisor es (STAGING) Let's Encrypt; y (4) solo entonces pasar a producción:

kubectl annotate ingress rutas-norte -n rutas-norte-pro \
  cert-manager.io/cluster-issuer=letsencrypt-produccion --overwrite
kubectl delete secret rutasnorte-tls -n rutas-norte-pro
kubectl get certificate -n rutas-norte-pro -w

Ese kubectl delete secret es necesario: sin él, cert-manager ve un certificado válido y no lo reemplaza hasta que toque renovar.

  1. Renovación automática y comprobación del estado

cert-manager renueva cuando queda menos de renewBefore para caducar (por defecto un tercio de la vida; con 90 días, a los 60). El proceso es idéntico al de emisión y el Secret se actualiza en su sitio, sin cambiar de nombre. ¿Hay que reiniciar los pods? No. El controlador de Ingress observa el Secret y recarga solo. Si un pod montara el certificado como volumen, el kubelet propaga el cambio en aproximadamente un minuto, aunque la aplicación tendría que releerlo.

Comprobaciones

kubectl get certificate -A
kubectl describe certificate rutasnorte-tls -n rutas-norte-pro
Status:
  Conditions:
    Type:      Ready
    Status:    True
    Message:   Certificate is up to date and has not expired
  Not Before:  2026-07-30T09:14:23Z
  Not After:   2026-10-28T09:14:22Z
  Renewal Time: 2026-09-28T09:14:22Z
Events:
  Normal  Issued  21d  cert-manager-certificates-issuing  The certificate has been successfully issued

Not After y Renewal Time son los dos campos que hay que vigilar. Y el certificado servido de verdad:

echo | openssl s_client -connect www.rutasnorte.example:443 \
  -servername www.rutasnorte.example 2>/dev/null \
  | openssl x509 -noout -issuer -subject -dates
issuer=C=US, O=Let's Encrypt, CN=R11
subject=CN=www.rutasnorte.example
notBefore=Jul 30 09:14:23 2026 GMT
notAfter=Oct 28 09:14:22 2026 GMT

El -servername es imprescindible: sin SNI, el controlador devolvería su certificado por defecto y verías uno equivocado.

Diagnóstico cuando READY es False

Sigue la cadena hacia abajo hasta el Challenge, con kubectl describe sobre certificate, certificaterequest, order y challenge, y por último kubectl logs -n cert-manager -l app=cert-manager.

Mensaje en el Challenge Causa
Waiting for HTTP-01 challenge propagation: wrong status code '404' El Ingress temporal no es alcanzable, o el DNS público no apunta a tu clúster
connection refused / timeout El puerto 80 no está abierto desde internet
NXDOMAIN El dominio no existe en el DNS público
too many certificates already issued Límite semanal alcanzado. Espera o usa staging
Issuer not found Confusión entre Issuer y ClusterIssuer, o nombre mal escrito

Ese último merece atención: cert-manager.io/issuer busca un Issuer en el namespace del Ingress; cert-manager.io/cluster-issuer busca un ClusterIssuer global. Usar la anotación equivocada da un error que parece de nombre y es de tipo.

  1. Redirección a HTTPS y HSTS

Publicar HTTPS no basta: hay que impedir el HTTP, porque un cliente que escriba el dominio sin esquema irá por HTTP. En ingress-nginx la redirección está activada por defecto en cuanto el Ingress tiene bloque tls, y se controla con las anotaciones ssl-redirect (HTTP → HTTPS) y force-ssl-redirect (incluso detrás de otro proxy):

curl -sI http://www.rutasnorte.example/ | head -2
HTTP/1.1 308 Permanent Redirect
Location: https://www.rutasnorte.example/

Pero la redirección deja una ventana: la primera petición viaja en claro y puede ser interceptada. HSTS (HTTP Strict Transport Security) la cierra: una cabecera que ordena al navegador usar HTTPS para ese dominio durante un tiempo, sin preguntar y sin permitir saltarse el aviso.

metadata:
  annotations:
    nginx.ingress.kubernetes.io/hsts: "true"
    nginx.ingress.kubernetes.io/hsts-max-age: "31536000"          # 1 año
    nginx.ingress.kubernetes.io/hsts-include-subdomains: "true"
    nginx.ingress.kubernetes.io/hsts-preload: "false"

Advertencias serias sobre HSTS, porque es de las pocas cosas de esta lección que pueden dejarte fuera de tu propio dominio:

  • No se puede revocar rápidamente. Los navegadores recuerdan la directiva hasta que expire max-age. Si tu certificado falla, los usuarios no pueden entrar, ni aceptando el riesgo.
  • includeSubDomains afecta a TODOS los subdominios, incluidos los que aún no tienen HTTPS: un admin.rutasnorte.example interno por HTTP dejaría de ser accesible.
  • preload es prácticamente irreversible: entrar en la lista precargada de los navegadores es fácil, salir tarda meses. Empieza con max-age bajo (300) y súbelo progresivamente.

Y aquí toca recordar la advertencia inicial: la política de HSTS, las versiones de TLS aceptadas (ssl-protocols: "TLSv1.2 TLSv1.3") y los conjuntos de cifrado (ssl-ciphers), que son ajustes globales del ConfigMap del controlador y no anotaciones por Ingress, deben ser revisados por un profesional de seguridad antes de aplicarse en producción.

  1. Más allá: mTLS y mallas de servicio

Lo montado protege el tramo cliente ↔ Ingress; dentro del clúster, el tráfico entre el controlador y tienda-web, o entre api-reservas y postgres-reservas, sigue en claro. Dos mecanismos van más lejos. mTLS (TLS mutuo): además del servidor, el cliente presenta certificado, de modo que api-reservas no solo verifica con quién habla sino que demuestra quién es; es la base de la identidad criptográfica entre servicios y sustituye a la confianza basada en direcciones IP. Y las mallas de servicio (Istio, Linkerd, Cilium Service Mesh), que inyectan un proxy junto a cada pod —o lo integran en el kernel con eBPF— y establecen mTLS automáticamente entre todos los componentes, con certificados de corta vida y sin tocar el código.

Para Rutas Norte hoy es exagerado: cinco componentes en un clúster propio, con las políticas de red de 04-06 limitando quién puede hablar con quién. Se replantearía si la plataforma creciera a decenas de servicios o si un requisito normativo exigiera cifrado extremo a extremo. El análisis completo está en 08-04.

Errores Comunes y Consejos

  • Empezar directamente en producción de Let's Encrypt. Cinco fallos y una semana bloqueado. Siempre staging primero.
  • Certificado sin subjectAltName. Los navegadores ignoran el CN. Sin SAN, el certificado es inútil.
  • Secret TLS en otro namespace. No hay referencias cruzadas: el Secret vive donde vive el Ingress.
  • Confundir Issuer con ClusterIssuer en la anotación. El error dice "no encontrado" y es de tipo.
  • Cambiar el issuer sin borrar el Secret. cert-manager ve un certificado válido y no reemplaza nada.
  • Redirigir a HTTPS bloqueando /.well-known/acme-challenge/. El desafío HTTP-01 deja de funcionar y la renovación falla en silencio hasta que caduca.
  • Activar HSTS con preload desde el primer día. Prácticamente irreversible. Sube max-age por etapas.
  • Intentar HTTP-01 en minikube con dominios de /etc/hosts, o pedir un comodín con HTTP-01: lo primero no es alcanzable desde Let's Encrypt y lo segundo es imposible por diseño (exige DNS-01).
  • Consejo: vigila certmanager_certificate_expiration_timestamp_seconds en Prometheus (07-03) y alerta con 21 días de margen. La automatización también falla.
  • Consejo: un Certificate por dominio en lugar de uno con muchos SAN limita el daño: un fallo de validación en un dominio no impide renovar los demás.
  • Consejo: guarda el ClusterIssuer en Git, pero nunca el Secret con la clave privada de la cuenta ACME ni el token del DNS. Aplica lo de 03-02: Sealed Secrets, ESO o Vault.

Ejercicios

Ejercicio 1: HTTPS en desarrollo con certificado autofirmado

Genera con openssl un certificado válido para www.rutasnorte.example y api.rutasnorte.example, créalo como Secret TLS en rutas-norte-dev, añádelo al Ingress y comprueba que HTTPS funciona con -k y falla sin él. Explica exactamente qué garantía falta.

Ejercicio 2: Instalar cert-manager y crear los dos ClusterIssuers

Instala cert-manager, crea letsencrypt-staging y letsencrypt-produccion con desafío HTTP-01, y anota el Ingress de producción con el de staging. Sigue la cadena CertificateOrderChallenge y explica por qué en minikube el desafío no se completará.

Ejercicio 3: Diagnosticar un certificado que no se emite

Un Certificate lleva 20 minutos en READY: False. Diseña el procedimiento de diagnóstico completo, indicando qué comando usar en cada nivel y qué mensaje esperarías para tres causas distintas: dominio que no resuelve desde internet, puerto 80 cerrado y límite de emisión alcanzado.

Soluciones

Ejercicio 1

openssl req -x509 -nodes -newkey rsa:2048 -days 365 \
  -keyout /tmp/dev.key -out /tmp/dev.crt \
  -subj "/C=ES/O=Rutas Norte S.L./CN=www.rutasnorte.example" \
  -addext "subjectAltName=DNS:www.rutasnorte.example,DNS:api.rutasnorte.example"

kubectl create secret tls rutasnorte-tls \
  --cert=/tmp/dev.crt --key=/tmp/dev.key -n rutas-norte-dev

kubectl patch ingress rutas-norte -n rutas-norte-dev --type=merge -p '
spec:
  tls:
    - hosts: ["www.rutasnorte.example","api.rutasnorte.example"]
      secretName: rutasnorte-tls'

curl -k -s -o /dev/null -w 'con -k: %{http_code}\n' https://www.rutasnorte.example/
curl    -s -o /dev/null https://www.rutasnorte.example/ || echo "sin -k: fallo (esperado)"

Lo que falta es la autenticación, no el cifrado. El canal está igual de cifrado, pero nadie de confianza avala que ese certificado pertenezca a Rutas Norte: un atacante que secuestrara el DNS podría presentar su propio autofirmado y el cliente no notaría la diferencia. La cadena de confianza es lo que aporta una CA pública.

Ejercicio 2

kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.16.2/cert-manager.yaml
kubectl wait --for=condition=Available deploy --all -n cert-manager --timeout=180s
kubectl apply -f k8s/base/clusterissuer-letsencrypt-staging.yaml
kubectl apply -f k8s/base/clusterissuer-letsencrypt-produccion.yaml
kubectl get clusterissuer      # ambos deben aparecer con READY True

kubectl annotate ingress rutas-norte -n rutas-norte-pro \
  cert-manager.io/cluster-issuer=letsencrypt-staging --overwrite
kubectl describe challenge -n rutas-norte-pro | tail -12
Reason: Waiting for HTTP-01 challenge propagation: failed to perform self check
        GET request: Get "http://www.rutasnorte.example/.well-known/acme-challenge/xY3...":
        dial tcp: lookup www.rutasnorte.example: no such host

En minikube el dominio solo existe en tu /etc/hosts. Los servidores de Let's Encrypt no lo resuelven ni pueden alcanzar tu clúster, así que HTTP-01 no puede completarse por diseño. Es exactamente el motivo por el que en desarrollo se usan certificados autofirmados.

Ejercicio 3

NS=rutas-norte-pro
kubectl describe certificate rutasnorte-tls -n $NS | sed -n '/Status/,$p'  # nivel 1
kubectl describe certificaterequest -n $NS | grep -A3 "Conditions"         # nivel 2
kubectl describe order -n $NS | grep -A5 "Status"                          # nivel 3
kubectl describe challenge -n $NS | grep -A5 "Reason"       # nivel 4: el mensaje util
kubectl logs -n cert-manager -l app=cert-manager --tail=80 | grep -i error  # nivel 5
Causa Mensaje esperado Dónde aparece
Dominio que no resuelve dial tcp: lookup www.rutasnorte.example: no such host Challenge
Puerto 80 cerrado connection refused o context deadline exceeded tras el self check Challenge
Límite alcanzado 429 urn:ietf:params:acme:error:rateLimited: too many certificates already issued Order y logs de cert-manager

Para los dos primeros se arregla la infraestructura (DNS público, cortafuegos). Para el tercero no hay arreglo técnico: se espera a que pase la semana o se trabaja en staging.

Conclusión

Rutas Norte ya cifra su tráfico. Sabes por qué HTTPS no es opcional en una plataforma que maneja DNI, teléfonos y pagos, y que TLS aporta dos cosas distintas: cifrado e identidad. Conoces los tres modelos de terminación —en el Ingress, reencriptado y passthrough— y por qué la terminación en el Ingress es la elección correcta aquí. Dominas la pieza básica, el Secret de tipo kubernetes.io/tls con sus claves tls.crt y tls.key, y las tres reglas que lo gobiernan: mismo namespace que el Ingress, coincidencia entre los hosts del bloque tls y las SAN del certificado, y selección por SNI cuando hay varios.

Has generado un certificado autofirmado con openssl para desarrollo, con el detalle decisivo del subjectAltName que los navegadores validan e ignorando el CN, y has comprobado en tu propia terminal la diferencia entre "cifrado" y "verificable": funciona con -k, falla sin él, y esa diferencia es la razón de que los autofirmados nunca lleguen a producción.

Después has automatizado el problema con cert-manager, que es el patrón operador aplicado a certificados: seis CRDs, un controlador y un bucle de reconciliación. Sabes qué es un Issuer y en qué se diferencia de un ClusterIssuer, y cómo cert-manager encadena por debajo CertificateCertificateRequestOrderChallenge, que es también el orden en que se diagnostica cualquier fallo. Entiendes ACME y sus dos desafíos: HTTP-01, sencillo pero con el requisito de ser alcanzable desde internet en el puerto 80 y sin poder emitir comodines, y DNS-01, imprescindible para comodines y para servicios internos a cambio de confiar un token de la API del DNS. Y has reducido toda la gestión a una anotación en el Ingress, con la disciplina irrenunciable de empezar siempre en staging para no consumir los cinco intentos semanales de Let's Encrypt.

Sabes comprobar el estado con kubectl get certificate y openssl s_client -servername, que la renovación ocurre 30 días antes de caducar sin reiniciar nada, y cómo cerrar la puerta al HTTP con la redirección y con HSTS, midiendo bien sus riesgos porque no se revoca a voluntad. Y sabes qué queda fuera: el tráfico dentro del clúster sigue en claro, y eso son mTLS y mallas de servicio, materia de 08-04.

Nos queda un agujero de los grandes, el mismo que arrastramos desde el módulo 3: cualquier pod del clúster puede conectarse a postgres-reservas:5432. Un pod comprometido, una imagen con una dependencia maliciosa o un simple error de despliegue en rutas-norte-dev tienen acceso directo a la base de datos con los datos personales de todos los clientes. En 04-06, la última lección del módulo, construiremos el modelo de aislamiento de la plataforma: denegar todo por defecto, abrir DNS —el fallo clásico que tumba el clúster entero—, y autorizar una a una únicamente las conversaciones que Rutas Norte necesita.

Curso de Kubernetes

Módulo 1: Introducción a Kubernetes

Módulo 2: Componentes Principales de Kubernetes

Módulo 3: Gestión de Configuración y Secretos

Módulo 4: Redes en Kubernetes

Módulo 5: Almacenamiento en Kubernetes

Módulo 6: Conceptos Avanzados de Kubernetes

Módulo 7: Monitoreo y Registro

Módulo 8: Seguridad en Kubernetes

Módulo 9: Escalado y Rendimiento

Módulo 10: Ecosistema y Herramientas de Kubernetes

Módulo 11: Estudios de Caso y Aplicaciones del Mundo Real

Módulo 12: Preparación para la Certificación de Kubernetes

© Copyright 2026. Todos los derechos reservados