cert-manager — Automatic TLS
cert-manager automates TLS certificate issuance and renewal from Let's Encrypt. Certificates are stored as Kubernetes Secrets and automatically rotated before expiry.
How it works
Browser → Traefik (:443) → Service
↑
cert-manager
↑
Let's Encrypt ACME
↑
HTTP-01 challenge via Traefik (:80)- You create a
Certificateresource referencing aClusterIssuer - cert-manager creates an ACME order with Let's Encrypt
- Let's Encrypt sends an HTTP-01 challenge to
http://<domain>/.well-known/acme-challenge/<token> - cert-manager deploys a temporary solver pod; Traefik routes the challenge to it
- Let's Encrypt validates the domain → issues the certificate
- cert-manager stores the certificate in a Kubernetes Secret
- Traefik reads the Secret and serves HTTPS
Certificates are automatically renewed ~30 days before expiry.
Helm install
helm upgrade --install cert-manager jetstack/cert-manager \
--version "${CERT_MANAGER_VERSION}" \
--namespace cert-manager \
--create-namespace \
--set crds.enabled=true
crds.enabled=trueinstalls the CRDs (Certificate,ClusterIssuer, etc.) as part of the Helm release so they are versioned and upgradeable.
ClusterIssuers
Two ClusterIssuer resources are defined in your consuming repo's platform/ directory (or managed via ArgoCD):
Staging (for testing)
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: ${EMAIL}
privateKeySecretRef:
name: letsencrypt-staging-account-key
solvers:
# DNS-01 via Cloudflare — for ${DOMAIN} and all its subdomains
# Works even when apex domain points to Vercel or another provider
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token-secret
key: api-token
selector:
dnsZones:
- "${DOMAIN}"
# HTTP-01 fallback — for any domain not covered by the dns01 selector
- http01:
ingress:
ingressClassName: traefikProduction
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-production
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: ${EMAIL}
privateKeySecretRef:
name: letsencrypt-production-account-key
solvers:
# DNS-01 via Cloudflare — for ${DOMAIN} and all its subdomains
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token-secret
key: api-token
selector:
dnsZones:
- "${DOMAIN}"
# HTTP-01 fallback — for any domain not covered by the dns01 selector
- http01:
ingress:
ingressClassName: traefik
${EMAIL}and${DOMAIN}are substituted at deploy time from.envviaenvsubst.
Staging vs Production
| Staging | Production | |
|---|---|---|
| Rate limits | None | Strict |
| Browser-trusted | ❌ | ✅ |
| Use case | Testing pipeline | Real workloads |
Always test with staging first. Production is rate-limited to 5 duplicate certificates per week per domain.
Using a certificate in an IngressRoute
# 1. Request the certificate
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: my-app-tls
namespace: apps
spec:
secretName: my-app-tls # Secret where the cert is stored
issuerRef:
name: letsencrypt-production
kind: ClusterIssuer
dnsNames:
- app.example.com
---
# 2. Reference it in the IngressRoute
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: my-app
namespace: apps
spec:
entryPoints:
- websecure
routes:
- match: Host(`app.example.com`)
kind: Rule
services:
- name: my-app-svc
port: 80
tls:
secretName: my-app-tls # Must match Certificate.spec.secretNameCheck certificate status
# List all certificates
kubectl get certificate -A
# Describe a certificate (shows ACME order status)
kubectl describe certificate my-app-tls -n apps
# Check cert-manager logs
kubectl logs -n cert-manager deploy/cert-manager --tail=50Expected output when ready:
NAME READY SECRET AGE
my-app-tls True my-app-tls 5mHTTP-01 challenge gotcha
⚠️ Do not configure a global HTTP→HTTPS redirect on Traefik's
webentrypoint.
Let's Encrypt sends the HTTP-01 challenge to port 80. If Traefik redirects all HTTP traffic to HTTPS before the challenge solver can respond, the validation fails and the certificate is never issued.
This is why traefik-values.yaml has no redirectTo on the web entrypoint. Use redirectScheme middleware on individual routes instead.
DNS-01 via Cloudflare (recommended)
Use DNS-01 when:
- The apex domain (
example.com) DNS points elsewhere (e.g. Vercel) — HTTP-01 would reach the wrong server - You need wildcard certificates (
*.example.com) — Let's Encrypt only issues these via DNS-01 - The cluster is not publicly reachable on port 80
cert-manager → Cloudflare API → creates TXT _acme-challenge.example.com
Let's Encrypt → DNS lookup → validates TXT → issues certificateSetup
Create a Cloudflare API Token with:
- Permission:
Zone - DNS - Edit - Permission:
Zone - Zone - Read - Zone Resources:
Include - All Zones
- Permission:
Store the token in Vault and sync via ESO:
# In your infra repo
make vault-seed-cloudflare # seeds secret/cert-manager/cloudflare
make vault-apply-externalsecrets # syncs cloudflare-api-token-secret → cert-manager namespace- The
ClusterIssuermanifests already include a DNS-01 solver for${DOMAIN}:
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token-secret
key: api-token
selector:
dnsZones:
- "${DOMAIN}"
- http01:
ingress:
ingressClassName: traefik # fallback for other domains
${DOMAIN}is substituted at deploy time from your.envviaenvsubst.
Wildcard certificate example
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-tls
namespace: apps
spec:
secretName: wildcard-tls
issuerRef:
name: letsencrypt-production
kind: ClusterIssuer
dnsNames:
- "*.example.com"
- "example.com"