Automatic TLS with cert-manager, Let's Encrypt, and Traefik IngressRoutes
Published: 2026-04-15
Every subdomain on this server gets a valid TLS certificate automatically. No manual renewals, no certbot cronjob, no wildcard cert. Here's the full picture from ClusterIssuer to browser padlock.
Two issuers: staging and production
yamlapiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: admin@example.com
privateKeySecretRef:
name: letsencrypt-staging
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@example.com
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
ingressClassName: traefik
The staging issuer doesn't count against Let's Encrypt rate limits (50 certs/domain/week). Always use staging first when setting up a new domain — if the HTTP-01 challenge fails (wrong DNS, port 80 blocked), you won't burn production attempts.
HTTP-01 challenge mechanics
The http01 solver proves domain ownership by serving a token at:
http://<domain>/.well-known/acme-challenge/<token>
cert-manager temporarily creates an Ingress object for this path. Traefik picks it up and routes the ACME server's validation request to cert-manager's solver pod.
Prerequisites:
- The domain's A record must point to the server's public IP before requesting the cert.
- Port 80 must be open. Traefik listens on 80 with
hostNetwork: trueand redirects to 443 — but the ACME challenge is served on 80 before the redirect fires.
Certificate object
yamlapiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: site-tls
namespace: default
spec:
secretName: site-tls
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- example.com
- www.example.com
- blog.example.com
- cv.example.com
- proxy.example.com
renewBefore: 720h # 30 days before expiry
All subdomains share one certificate with multiple SANs. cert-manager:
- Requests a single cert covering all names
- Stores the key and cert chain in the
site-tlsSecret - Automatically renews ~30 days before expiry
- Triggers a cert rotation if you add new entries to
dnsNames
IngressRoute wiring
Traefik uses its own CRD instead of the standard Ingress. The TLS section references the same Secret:
yamlapiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: site-main
spec:
entryPoints:
- websecure
routes:
- match: Host(`example.com`)
kind: Rule
services:
- name: site-backend
port: 8000
- match: Host(`www.example.com`)
kind: Rule
middlewares:
- name: www-redirect
services:
- name: site-backend
port: 8000
tls:
secretName: site-tls
Each subdomain gets its own route rule.
Subdomain → path prefix middleware
The FastAPI app serves everything under one router. Subdomains are rewritten to path prefixes by a Traefik Middleware:
yamlapiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: blog-prefix
spec:
addPrefix:
prefix: /weblog
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: www-redirect
spec:
redirectRegex:
regex: "^https://www\\.(.+)"
replacement: "https://${1}"
permanent: true
blog.example.com/ → app sees /weblog/. One pod, one Service, one Deployment handles all subdomains.
Checking cert status
bash# List all certificates
kubectl get certificate -A
# Check specific cert
kubectl describe certificate site-tls
# Check the ACME order (if cert is pending)
kubectl get orders -A
kubectl describe order <order-name>
# Check the challenge (if order is pending)
kubectl get challenges -A
kubectl describe challenge <challenge-name>
The Ready: True condition means the cert is issued and stored. If stuck in False, check the order → challenge chain for the specific failure.
Debug: manual HTTP-01 challenge verification
bash# Check that cert-manager's solver pod is running
kubectl get pods -n cert-manager
# Check that Traefik has picked up the temporary Ingress
kubectl get ingress -A | grep acme
# Verify port 80 is reachable
curl -v http://example.com/.well-known/acme-challenge/test
Certificate renewal
cert-manager renews automatically. To force immediate renewal:
bashkubectl delete secret site-tls
cert-manager detects the missing Secret, re-triggers the ACME flow, and creates a new cert. Traefik will briefly serve the old cert from memory until the Secret is recreated (usually under 60 seconds).
Rate limits
Let's Encrypt limits:
- 50 certs per registered domain per week
- 5 failures per account per hostname per hour
- Staging has no limits (but certs are not trusted by browsers)
If you hit a rate limit, check https://crt.sh/?q=example.com to see all issued certs.
Summary
- cert-manager + Let's Encrypt + Traefik provides fully automatic TLS with zero manual renewal
ClusterIssueris created once; eachCertificateresource references it by name — no per-app ACME config needed- HTTP-01 challenge works for individual subdomains; DNS-01 is required for wildcard certs
- Start with the Let's Encrypt staging issuer to avoid rate limits during setup; switch to production after first successful cert
- Check cert status with
kubectl describe certificate -n <ns>andkubectl get certificaterequest— most issues are visible there