En el Episode 4 dejé al cluster con MetalLB: los Services consiguen IPs de la LAN, y ya probé que el router le entrega tráfico a IPs que nunca asignó.
Pero una IP por servicio escala mal. Lo que quiero es lo que tiene cualquier hosting: un único punto de entrada que reparta por nombre de host — y con candado, que en 2026 un servicio sin HTTPS ni el navegador te deja abrir sin hacer ruido.
Acá confluyen dos problemas de lab que en la nube no existen: no tengo DNS para el 192.168.68.200 del cluster, y no puedo usar Let’s Encrypt porque no hay dominio público apuntando a mi LAN. Dos trucos de lab los resuelven: nip.io para el primero, una CA propia para el segundo.
Y una decisión de más fondo: por qué Traefik y no ingress-nginx — que Kubernetes retiró en marzo de 2026 y ya no es la respuesta por defecto que era.
El problema: HTTPS sin dominio, y nombres sin DNS
Para que un certificado valga algo, el nombre del certificado tiene que coincidir con el nombre con el que llegás. Y para que un nombre resuelva, hace falta DNS. En un lab casero no hay ni lo uno ni lo otro:
- Let’s Encrypt valida que controlás el dominio — con HTTP-01, conectándose a tu servidor desde internet. Un cluster en una LAN privada jamás pasa esa prueba. Y ni siquiera tengo dominio apuntando a la LAN.
- DNS: podría montar Pi-hole, o tocar el router, o editar
/etc/hostsen cada máquina. Todo por un host nuevo cada vez que expongo una app.
Las soluciones:
nip.io: un DNS público que resuelvewhoami.192.168.68.200.nip.ioa192.168.68.200. El nombre se arma solo con la IP: cero configuración, cero DNS propio. Cualquier host nuevo funciona al instante.- CA propia: en vez de pelear con una AC pública, ser mi propia autoridad certificante. Firmo un certificado por host, instalo la CA una sola vez en cada cliente, y listo por 10 años.
1. cert-manager (chart v1.21.2)
cert-manager es el componente que emite y renueva certificados dentro del cluster: le pedís un certificado para un host y él crea el Certificate, lo firma, lo guarda en un Secret y lo renueva antes de que venza.
helm repo add jetstack https://charts.jetstack.io && helm repo update
helm install cert-manager jetstack/cert-manager \
-n cert-manager --create-namespace --version v1.21.2 \
--set crds.enabled=true
kubectl -n cert-manager rollout status deploy/cert-manager-webhook
crds.enabled=true instala los CRDs desde el chart (Certificate, ClusterIssuer, etc.). Sin eso, el primer apply de un ClusterIssuer no encuentra el tipo.
La CA del lab
Acá viene la parte elegante. Para que una CA sea una CA, su certificado tiene que estar firmado… por una CA. El huevo y la polla se resuelve con una cadena de tres recursos (ingress/cluster-issuers.yaml):
# 1. Un ClusterIssuer autofirmado: solo existe para crear la CA.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: selfsigned
spec:
selfSigned: {}
---
# 2. El certificado de la CA: firmado por "selfsigned", con 10 años de vida.
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: lab-ca
namespace: cert-manager
spec:
isCA: true
commonName: lab-ca
secretName: lab-ca
duration: 87600h0m0s
privateKey:
algorithm: ECDSA
size: 256
issuerRef:
name: selfsigned
kind: ClusterIssuer
group: cert-manager.io
---
# 3. El ClusterIssuer que firma los certificados de las apps.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: lab-ca
spec:
ca:
secretName: lab-ca
Cada recurso depende del anterior: el Certificate necesita el issuer selfsigned, y el issuer lab-ca necesita que el Certificate exista. cert-manager ordena la cadena solo.
kubectl apply -f ingress/cluster-issuers.yaml
kubectl get clusterissuers # selfsigned y lab-ca en READY=True
kubectl -n cert-manager get certificate lab-ca
Gotcha: si el apply falla con error del webhook, cert-manager todavía está arrancando. Esperar y repetir — el webhook tiene que estar listo antes de validar cualquier recurso de cert-manager.
Confiar en la CA en cada cliente
Un certificado firmado por mi CA sólo vale si el cliente conoce a mi CA. La exporto:
kubectl -n cert-manager get secret lab-ca -o jsonpath='{.data.ca\.crt}' | base64 -d > lab-ca.crt
Y la instalo una sola vez por máquina:
- Linux/WSL:
sudo cp lab-ca.crt /usr/local/share/ca-certificates/ && sudo update-ca-certificates - Windows: doble clic en
lab-ca.crt→ Instalar certificado → “Entidades de certificación raíz de confianza”. - Firefox: usa su propio almacén, no el del SO: Ajustes → Certificados → Importar.
lab-ca.crt es público — es justamente lo que se distribuye. Pero el Secret completo no se sube a ningún lado: contiene la clave privada de la CA, y quien la tenga puede firmar certificados que mis clientes aceptarían.
2. Traefik (chart 41.6.0, Traefik v3.7.13)
La elección no es casual. ingress-nginx fue retirado por Kubernetes en marzo de 2026 — el proyecto que durante años fue el ingress por defecto ya no se mantiene. Traefik, en cambio, es el que Talos y Kubernetes dan como referencia en la mayoría de las guías actuales, y me da gratis el dashboard, el redirect HTTP→HTTPS declarativo y middleware (que va a servir en el post de Argo CD).
Los values (ingress/traefik-values.yaml):
service:
annotations:
metallb.io/loadBalancerIPs: 192.168.68.200 # IP fija del pool de MetalLB
ingressClass:
enabled: true
isDefaultClass: true
ports:
web:
http:
redirections:
entryPoint:
to: websecure
scheme: https
permanent: true
Tres decisiones acá:
- IP fija, no “la que salga”. La anotación le dice a MetalLB qué IP del pool darle al Service de Traefik. Sin ella, Traefik podría recibir cualquier IP del pool — y los hosts
nip.ioapuntan a una IP concreta. El ingress es el único punto de entrada del cluster: su IP no puede cambiar. isDefaultClass: true: los Ingress siningressClassNameexplícito caen en Traefik. Menos una línea por app.- El redirect es 301 permanente, configurado en Traefik mismo y no en un Ingress por app: todo lo que llegue por HTTP se va a HTTPS sin que las apps se enteren.
helm repo add traefik https://traefik.github.io/charts && helm repo update
helm install traefik traefik/traefik \
-n traefik --create-namespace --version 41.6.0 \
-f ingress/traefik-values.yaml
kubectl -n traefik rollout status deploy/traefik
kubectl -n traefik get svc traefik # EXTERNAL-IP = 192.168.68.200
kubectl get ingressclass # traefik (default)
Que EXTERNAL-IP diga 192.168.68.200 — la primera IP del pool del Episode 4 — es MetalLB cumpliendo la anotación: los dos episodios se están dando la mano.

3. Probar: whoami con su propio certificado
Una app de prueba con su Ingress (ingress/test-whoami.yaml), que pide su certificado a lab-ca para whoami.192.168.68.200.nip.io:
apiVersion: apps/v1
kind: Deployment
metadata:
name: whoami
spec:
replicas: 1
selector:
matchLabels:
app: whoami
template:
metadata:
labels:
app: whoami
spec:
containers:
- name: whoami
image: traefik/whoami
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: whoami
spec:
selector:
app: whoami
ports:
- port: 80
targetPort: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: whoami
annotations:
cert-manager.io/cluster-issuer: lab-ca
spec:
tls:
- hosts:
- whoami.192.168.68.200.nip.io
secretName: whoami-tls
rules:
- host: whoami.192.168.68.200.nip.io
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: whoami
port:
number: 80
La anotación cert-manager.io/cluster-issuer: lab-ca es todo lo que hace falta: cert-manager ve el Ingress con bloque tls, crea el Certificate, lo firma con la CA, y lo guarda en el Secret whoami-tls. Traefik lo levanta del Secret solo.
kubectl apply -f ingress/test-whoami.yaml
kubectl get certificate whoami-tls # READY=True
curl -I http://whoami.192.168.68.200.nip.io # 301 → https
curl --cacert lab-ca.crt https://whoami.192.168.68.200.nip.io
Ese último curl es la demo completa de la serie en una línea: DNS de nip.io resolviendo a la IP de MetalLB, Traefik repartiendo por nombre de host, y un certificado firmado por una CA que curl confía.
Limpiar:
kubectl delete -f ingress/test-whoami.yaml
kubectl delete secret whoami-tls # cert-manager no lo borra con el Ingress
Ese secret huérfano es fácil de olvidar: si mañana se re-crea el Ingress con el mismo secretName, el certificado viejo resucita y la confusión puede ser larga.
4. El día a día: exponer una app
Con todo montado, exponer una app nueva es sólo el Ingress — la anotación del issuer y el bloque tls:
metadata:
annotations:
cert-manager.io/cluster-issuer: lab-ca
spec:
tls:
- hosts: [miapp.192.168.68.200.nip.io]
secretName: miapp-tls
rules:
- host: miapp.192.168.68.200.nip.io
...
cert-manager crea el certificado, lo guarda en miapp-tls y lo renueva solo. Ni certificados vencidos, ni renovaciones manuales, ni una herramienta fuera del cluster.
Nombres sin nip.io
nip.io resuelve miapp.192.168.68.200.nip.io vía DNS público — elegante, pero es una dependencia externa. Si un día no responde (o el router decide bloquearlo), las alternativas:
/etc/hosts(oC:\Windows\System32\drivers\etc\hosts):192.168.68.200 whoami.lab- Un registro comodín
*.lab → 192.168.68.200en el DNS del router o en Pi-hole/AdGuard.
La segunda es la que termina gustándome más: un solo registro para siempre.
Opcional: Let’s Encrypt
Si el lab tuviera un dominio propio con DNS en un proveedor con API (Cloudflare, Route 53…), Let’s Encrypt es viable — pero sólo con desafío DNS-01: el HTTP-01 necesita que internet llegue al cluster, y el cluster no está expuesto. Se agrega como un ClusterIssuer más y se elige por app con la anotación. Doc: cert-manager DNS-01.
Problemas comunes
| Síntoma | Causa probable |
|---|---|
EXTERNAL-IP de Traefik en <pending> |
MetalLB no instalado, o 192.168.68.200 ya asignada a otro Service |
apply de los issuers falla con error de webhook |
cert-manager todavía arrancando: esperar y repetir |
Certificate en READY=False |
kubectl describe certificate <nombre> y kubectl get certificaterequests,orders -A |
nip.io no resuelve |
El router bloquea respuestas DNS con IPs privadas (DNS rebinding protection): usar /etc/hosts o DNS local |
| El navegador marca el certificado inválido | Falta importar lab-ca.crt en ese cliente (¡en Firefox aparte!) |
| 404 de Traefik | El host del Ingress no coincide con la URL, o falta ingressClassName y Traefik no es la clase por defecto |
Lo que me llevo de este episodio
- La CA es una cadena de tres recursos: issuer autofirmado → certificado
isCA→ issuer real. cert-manager ordena la polla y el huevo solo. - La IP del ingress es fija o es nada. Los hosts
nip.iocodifican la IP en el nombre: si Traefik cambia de IP, todo el cluster “cambia de nombre”. - El redirect va en Traefik, no en cada app. 301 permanente configurado una vez, para siempre.
- cert-manager no borra los Secrets al borrar el Ingress. Limpiar a mano, o sorpresa resucitada.
nip.iodepende de DNS público. Cómodo hasta que el router lo bloquea por “protección” — tener/etc/hostscomo plan B.
Próximo episodio
El cluster ya tiene storage, IPs y HTTPS. Los cuatro primeros episodios instalaron todo con helm install y kubectl apply — comandos que, si el cluster se pierde, existieron y no dejan rastro. Lo que falta es que lo que corre en él se declare en git: el post de Argo CD.
Mi lab ahora emite certificados que mis propios equipos confían. Ser mi propia autoridad, resultó ser esto. 🔏