Inhoudsopgave
- Leerdoel
- Vereisten
- Wat cert-manager doet
- cert-manager installeren met Helm
- ClusterIssuers aanmaken voor Let's Encrypt
- HTTP-01 challenge: de eenvoudige route
- DNS-01 challenge: wildcards en private clusters
- Certificaten uitgeven via Ingress annotations
- Certificaten uitgeven via de Certificate resource
- Gateway API integratie
- Certificaatvervaldatum monitoren
- Troubleshooting
- Wat je geleerd hebt
Leerdoel
Aan het eind van deze tutorial heb je cert-manager draaien op een Kubernetes-cluster, een staging en productie ClusterIssuer gekoppeld aan Let's Encrypt, minimaal een TLS-certificaat dat automatisch wordt uitgegeven en vernieuwd, en Prometheus-alerts die certificaatvervaldatums bewaken. Je begrijpt wanneer je HTTP-01 boven DNS-01 kiest, wanneer je Ingress annotations versus een expliciet Certificate resource gebruikt, en hoe je de hele ACME-levenscyclus debugt.
Vereisten
- Een Kubernetes-cluster met v1.32 of nieuwer (cert-manager v1.20 ondersteunt Kubernetes 1.32 tot en met 1.35)
kubectlgeconfigureerd met rechten om namespaces, CRDs en cluster-scoped resources aan te maken- Helm 3.x lokaal geinstalleerd
- Een publiek bereikbare domeinnaam (voor HTTP-01 challenges). DNS-01 werkt zonder publieke toegang, maar vereist API-credentials voor je DNS-provider.
- Bekendheid met Kubernetes Ingress resources of Gateway API. Beide artikelen behandelen de routinglaag waarmee cert-manager integreert; deze tutorial richt zich op de certificaatautomatisering.
Wat cert-manager doet
cert-manager is een Kubernetes-controller die Certificate, Issuer, ClusterIssuer, CertificateRequest, Order en Challenge als Custom Resource Definitions toevoegt. Het bewaakt deze resources, communiceert met certificate authorities (Let's Encrypt, HashiCorp Vault, interne CAs en meer), en slaat ondertekende certificaten op in Kubernetes Secrets.
De levenscyclus voor Let's Encrypt ziet er zo uit:
- Jij (of een Ingress-annotation) maakt een
Certificateresource aan. - cert-manager maakt een
CertificateRequestaan, daarna een ACMEOrder. - Per domein in de order maakt cert-manager een
Challengeaan (HTTP-01 of DNS-01). - Let's Encrypt valideert de challenge en geeft het certificaat uit.
- cert-manager slaat
tls.crtentls.keyop in het doelSecret. - Na ruwweg twee derde van de 90-daagse levensduur van het certificaat (rond dag 60) vernieuwt cert-manager automatisch.
Dat laatste punt is de kern van de waarde. Handmatig Let's Encrypt-certificaten vernieuwen, elke 90 dagen, over tientallen services heen, dat is precies het soort operationeel werk dat vergeten wordt tot er om 2 uur 's nachts iets breekt.
cert-manager installeren met Helm
cert-manager v1.20 (uitgebracht op 10 maart 2026) is de huidige stabiele release. Installeer het vanuit de OCI registry op quay.io/jetstack:
helm install cert-manager oci://quay.io/jetstack/charts/cert-manager \
--version v1.20.0 \
--namespace cert-manager \
--create-namespace \
--set crds.enabled=true
De flags die ertoe doen:
--version v1.20.0pint de chartversie. Zonder deze flag pakt Helm de laatste versie, en dat kan voor verrassingen zorgen tijdens een productiewijziging.--set crds.enabled=trueinstalleert de CRDs van cert-manager als onderdeel van de Helm release. De CRDs worden bewust bewaard bij deinstallatie sinds v1.15 om per ongeluk dataverlies te voorkomen.
Checkpoint. Controleer of de drie cert-manager pods draaien:
kubectl get pods -n cert-manager
Verwachte output (namen zullen afwijken):
NAME READY STATUS RESTARTS AGE
cert-manager-6d4b6d6c96-xk7gn 1/1 Running 0 42s
cert-manager-cainjector-74fb68c89b-2j4qp 1/1 Running 0 42s
cert-manager-webhook-5c8f4b6d67-rl9tn 1/1 Running 0 42s
Als je Gateway API wilt gebruiken in plaats van of naast Ingress, voeg dan --set config.enableGatewayAPI=true toe. Gateway API CRDs (v1.4.1 of nieuwer) moeten al geinstalleerd zijn in het cluster voordat je deze flag inschakelt.
Integreer cert-manager nooit als sub-chart van een andere Helm chart. cert-manager beheert cluster-scoped resources die conflicteren met Helm's ownership-model, en het project waarschuwt hier expliciet voor.
ClusterIssuers aanmaken voor Let's Encrypt
Een Issuer is namespace-scoped; een ClusterIssuer is cluster-scoped. Voor Let's Encrypt gebruik je ClusterIssuer, zodat elke namespace certificaten kan aanvragen zonder de issuerconfiguratie te dupliceren.
Maak er altijd twee aan: een voor staging, een voor productie. De staging-omgeving heeft ruimere rate limits en geeft certificaten uit van een niet-vertrouwde root ("(STAGING) Fake LE Root X1"). Browsers geven een waarschuwing, maar de ACME-flow is identiek aan productie. Dit is waar je fouten in de configuratie ontdekt.
# staging-clusterissuer.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
email: devops@jouwbedrijf.nl # Let's Encrypt stuurt vervalmeldingen hiernaartoe
server: https://acme-staging-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt-staging-account-key
solvers:
- http01:
ingress:
ingressClassName: nginx
# prod-clusterissuer.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
email: devops@jouwbedrijf.nl
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt-prod-account-key
solvers:
- http01:
ingress:
ingressClassName: nginx
Pas beide toe:
kubectl apply -f staging-clusterissuer.yaml
kubectl apply -f prod-clusterissuer.yaml
Checkpoint. Controleer of beide issuers klaar zijn:
kubectl get clusterissuer
Verwachte output:
NAME READY AGE
letsencrypt-staging True 15s
letsencrypt-prod True 10s
Als READY op False staat, inspecteer de events: kubectl describe clusterissuer letsencrypt-staging. De meest voorkomende oorzaak is een network policy die uitgaand HTTPS-verkeer naar acme-v02.api.letsencrypt.org blokkeert.
Het privateKeySecretRef Secret wordt automatisch aangemaakt bij de eerste registratie. Als je het kwijtraakt, kun je eerder uitgegeven certificaten niet meer intrekken (nieuwe uitgeven kan nog wel).
HTTP-01 challenge: de eenvoudige route
HTTP-01 is de standaard solver in de ClusterIssuers hierboven. cert-manager maakt een tijdelijke Pod en Ingress (of HTTPRoute) aan die een token serveert op http://<domein>/.well-known/acme-challenge/<token>. Let's Encrypt haalt dit op via gewoon HTTP op poort 80 om domeineigendom te verifieren.
Wanneer HTTP-01 goed werkt:
- Je domein resolveert naar de ingress controller van het cluster
- Poort 80 is bereikbaar vanaf het internet (Let's Encrypt volgt geen redirects van HTTP naar HTTPS voor challenge-validatie)
- Je hebt geen wildcardcertificaten nodig
Wanneer HTTP-01 niet werkt:
- Interne of private clusters zonder publieke ingress
- Wildcardcertificaten (
*.jouwbedrijf.nl). DNS-01 is het enige ACME challenge-type dat wildcardeigendom valideert. - Poort 80 is geblokkeerd door een firewall of cloud security group
Een veelgemaakte fout: geen ingressClassName specificeren in de solver. Zonder die instelling maakt cert-manager challenge Ingress resources aan zonder class-annotatie, waardoor elke ingress controller in het cluster het challenge-verkeer serveert. Op clusters met meerdere controllers leidt dat tot onverwachte load balancers en routingconflicten.
DNS-01 challenge: wildcards en private clusters
DNS-01 bewijst domeineigendom door een TXT-record aan te maken op _acme-challenge.<domein>. Let's Encrypt verifieert dit via DNS, daarna ruimt cert-manager het record op.
cert-manager heeft ingebouwde ondersteuning voor Route53, Cloudflare, Google Cloud DNS, Azure DNS, DigitalOcean, Akamai, ACMEDNS en RFC-2136. Meer dan 40 community webhook providers dekken de rest.
Route53 (AWS)
Maak een IAM policy aan die beperkt is tot TXT record-mutaties:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "route53:GetChange",
"Resource": "arn:aws:route53:::change/*"
},
{
"Effect": "Allow",
"Action": [
"route53:ChangeResourceRecordSets",
"route53:ListResourceRecordSets"
],
"Resource": "arn:aws:route53:::hostedzone/*",
"Condition": {
"ForAllValues:StringEquals": {
"route53:ChangeResourceRecordSetsRecordTypes": ["TXT"]
}
}
},
{
"Effect": "Allow",
"Action": "route53:ListHostedZonesByName",
"Resource": "*"
}
]
}
Voor authenticatie zijn EKS Pod Identity of IRSA de aanbevolen opties. Beide injecteren credentials automatisch zonder langlevende access keys op te slaan. Met Pod Identity of IRSA geconfigureerd is de ClusterIssuer minimaal:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
email: devops@jouwbedrijf.nl
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt-prod-account-key
solvers:
- dns01:
route53: {}
Een leeg route53: {} blok vertelt cert-manager om ambient credentials te gebruiken. De regio wordt afgeleid uit AWS_REGION, die Pod Identity en IRSA automatisch injecteren.
Ambient credentials (Pod Identity, IRSA, EC2 instance profile) werken alleen voor
ClusterIssuer, niet voor namespace-scopedIssuer. Dit bijt mensen op EKS die namespace-isolatie proberen met IRSA en dan merken dat DNS-01 challenges stilletjes falen.
Cloudflare
Maak een API Token aan (niet de legacy Global API Key) met rechten: Zone > DNS > Edit en Zone > Zone > Read.
Sla het op in een Secret:
apiVersion: v1
kind: Secret
metadata:
name: cloudflare-api-token-secret
namespace: cert-manager
type: Opaque
stringData:
api-token: cfl_je_daadwerkelijke_api_token_hier
Verwijs ernaar in de ClusterIssuer solver:
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token-secret
key: api-token
Solvers combineren
Een enkele ClusterIssuer kan DNS-01 voor specifieke zones en HTTP-01 als fallback combineren:
solvers:
- selector:
dnsZones:
- "internal.jouwbedrijf.nl"
dns01:
route53: {}
- http01:
ingress:
ingressClassName: nginx
cert-manager evalueert selectors van boven naar beneden. De eerste matching solver wint; als er geen matcht, fungeert de laatste solver zonder selector als catch-all.
Certificaten uitgeven via Ingress annotations
cert-manager bevat een ingress-shim component dat Ingress resources bewaakt op specifieke annotations. Wanneer het er een vindt, maakt het automatisch een Certificate resource aan.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: dashboard
namespace: team-alpha
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx
tls:
- hosts:
- dashboard.jouwbedrijf.nl
secretName: dashboard-tls # cert-manager slaat het cert hier op
rules:
- host: dashboard.jouwbedrijf.nl
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: dashboard
port:
number: 8080
De annotation cert-manager.io/cluster-issuer: letsencrypt-prod activeert de shim. cert-manager leest het tls-blok, maakt een Certificate aan voor dashboard.jouwbedrijf.nl, en slaat het resultaat op in Secret dashboard-tls in namespace team-alpha.
Checkpoint. Controleer na het toepassen van de Ingress of het Certificate is aangemaakt en gereed is:
kubectl get certificate -n team-alpha
Verwachte output binnen 1–2 minuten:
NAME READY SECRET AGE
dashboard-tls True dashboard-tls 90s
Als er geen Certificate verschijnt, controleer dan: (1) de annotation-naam is exact (typos zijn stille fouten), (2) het tls-blok bestaat met secretName en hosts, en (3) de cert-manager controller-logs tonen geen fouten: kubectl logs -n cert-manager deploy/cert-manager --since=5m.
Voor de volledige lijst van ondersteunde annotations (duration, renewal window, key algorithm), zie de ingress-shim documentatie.
Certificaten uitgeven via de Certificate resource
De Ingress annotation-aanpak is handig voor eenvoudige gevallen. Gebruik een expliciet Certificate resource als je nodig hebt:
- Wildcardcertificaten (
*.jouwbedrijf.nl) - Certificaten voor niet-HTTP services (gRPC, databases, mTLS)
- Volledige controle over key algorithm, duration of renewal timing
- Ontkoppelde levenscyclus van Ingress resources
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-jouwbedrijf-nl
namespace: team-alpha
spec:
secretName: wildcard-jouwbedrijf-nl-tls
duration: 2160h # 90 dagen (Let's Encrypt maximum)
renewBefore: 720h # vernieuwen 30 dagen voor verval
dnsNames:
- "*.jouwbedrijf.nl"
- jouwbedrijf.nl # wildcard dekt het apex-domein niet
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
privateKey:
algorithm: ECDSA
size: 256
Zet *.jouwbedrijf.nl tussen aanhalingstekens in YAML om parserproblemen met het wildcardkarakter te voorkomen. Het apex-domein (jouwbedrijf.nl) heeft een eigen entry nodig, want een wildcardcertificaat dekt het kale domein niet.
Het resulterende Secret bevat:
| Key | Inhoud |
|---|---|
tls.crt |
Ondertekende certificaatketen (leaf + intermediates) |
tls.key |
Private key |
ca.crt |
Uitgevende CA-certificaat (leeg voor Let's Encrypt) |
Checkpoint. Verifieer de uitgifte:
kubectl describe certificate wildcard-jouwbedrijf-nl -n team-alpha
Kijk naar Status: True op de Ready-conditie en een Certificate issued successfully event.
Key rotation
cert-manager v1.20 staat standaard op rotationPolicy: Always, wat betekent dat elke vernieuwing een verse private key genereert. Applicaties die TLS-keys in het geheugen cachen, moeten Secret-updates detecteren en herladen. De Reloader controller of een sidecar als configmap-reload kan dit automatiseren.
Gateway API integratie
cert-manager integreert met Kubernetes Gateway API sinds v1.15 (beta). In plaats van Ingress resources te annoteren, annoteer je de Gateway zelf:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: production-gateway
namespace: infra
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
gatewayClassName: envoy
listeners:
- name: https
hostname: app.jouwbedrijf.nl
port: 443
protocol: HTTPS
tls:
mode: Terminate
certificateRefs:
- name: app-jouwbedrijf-nl-tls
kind: Secret
cert-manager maakt een Certificate aan met de naam app-jouwbedrijf-nl-tls in namespace infra voor DNS-naam app.jouwbedrijf.nl. De listener moet tls.mode: Terminate hebben (Passthrough wordt niet ondersteund) en een niet-lege hostname.
Deze aanpak past bij het Gateway API rollenmodel: platform engineers beheren de Gateway en de TLS-configuratie, terwijl applicatieteams hun HTTPRoutes onafhankelijk beheren.
Beperking: Certificaten kunnen alleen in dezelfde namespace als de Gateway worden aangemaakt. Cross-namespace secret references worden niet ondersteund.
Een opmerking over ingress-nginx
ingress-nginx bereikte einde levensduur in maart 2026. cert-manager's Ingress annotations werken er nog steeds mee en met andere actief onderhouden controllers (Traefik, Contour, HAProxy). Voor nieuwe deployments is Gateway API het overwegen waard. Voor bestaande ingress-nginx clusters is er geen directe noodzaak om werkende configuraties te herschrijven, maar plan wel een migratiepad.
Certificaatvervaldatum monitoren
cert-manager exposeert Prometheus-metrics op poort 9402. De belangrijkste:
certmanager_certificate_expiration_timestamp_seconds geeft de Unix-timestamp wanneer elk certificaat verloopt, gelabeld met name, namespace, issuer_name, issuer_kind en issuer_group.
Prometheus scraping inschakelen
Voeg deze values toe aan je cert-manager Helm release:
prometheus:
enabled: true
podmonitor:
enabled: true # maakt een PodMonitor aan voor de Prometheus Operator
PrometheusRule alerts
Deze vier regels dekken de belangrijkste faalscenario's (gebaseerd op Philip Schmid's community ruleset):
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: cert-manager-alerts
namespace: monitoring
spec:
groups:
- name: cert-manager
interval: 30s
rules:
- alert: CertManagerAbsent
expr: absent(up{job="cert-manager"})
for: 1h
labels:
severity: critical
annotations:
summary: "cert-manager is verdwenen uit Prometheus service discovery"
- alert: CertManagerCertExpireSoon
expr: |
certmanager_certificate_expiration_timestamp_seconds - time() < (31 * 24 * 3600)
unless on(name, namespace)
certmanager_certificate_ready_status{condition!="True"} == 1
for: 24h
labels:
severity: warning
annotations:
summary: >-
Certificate {{ $labels.namespace }}/{{ $labels.name }}
verloopt over {{ $value | humanizeDuration }}
- alert: CertManagerCertNotReady
expr: |
max by (name, namespace, condition) (
certmanager_certificate_ready_status{condition!="True"} == 1
)
for: 24h
labels:
severity: critical
annotations:
summary: "Certificate {{ $labels.namespace }}/{{ $labels.name }} is niet ready"
- alert: CertManagerACMEErrors
expr: |
sum by (host, status, method) (
rate(certmanager_http_acme_client_request_count{status!~"2.."}[1h])
) > 0
for: 4h
labels:
severity: warning
annotations:
summary: "cert-manager kan het ACME-endpoint niet bereiken"
Handmatige inspectie met cmctl
cmctl is de officiele CLI voor cert-manager. Twee commando's die ik regelmatig gebruik:
# Volledige status van een certificaat, inclusief gerelateerde CertificateRequest, Order en Challenge
cmctl status certificate dashboard-tls -n team-alpha
# Onmiddellijke vernieuwing forceren (omzeilt het 2/3-levensduurschema)
cmctl renew dashboard-tls -n team-alpha
Troubleshooting
Wanneer een certificaat vastzit, loop de resource-keten van boven naar beneden af:
Certificate → CertificateRequest → Order → Challenge
# Stap 1: is het Certificate ready?
kubectl get certificate -n team-alpha
kubectl describe certificate dashboard-tls -n team-alpha
# Stap 2: wat zegt de CertificateRequest?
kubectl get certificaterequest -n team-alpha
kubectl describe certificaterequest <naam> -n team-alpha
# Stap 3: voor ACME, inspecteer Order en Challenge
kubectl get order -n team-alpha
kubectl describe challenge <naam> -n team-alpha
# Stap 4: cert-manager controller logs (laatste redmiddel)
kubectl logs -n cert-manager deploy/cert-manager --since=15m
Veelvoorkomende fouten
| Symptoom | Waarschijnlijke oorzaak |
|---|---|
| Challenge blijft hangen op "pending" | HTTP-01: poort 80 geblokkeerd, of ingressClassName niet ingesteld. DNS-01: verkeerde credentials of TXT record propageert niet. |
| "too many certificates already issued" | De Let's Encrypt rate limit is bereikt: 5 certificaten per exacte domeincombinatie per 7 dagen. Workaround: voeg een SAN toe of verwijder er een om de set te wijzigen. |
| Geen Certificate aangemaakt na Ingress annotation | Missende cert-manager.io/cluster-issuer annotation (exacte key, geen typos) of ontbrekend tls-blok in de Ingress spec. |
| DNS-01 faalt op EKS met IRSA | Je gebruikt een namespace Issuer in plaats van ClusterIssuer. Ambient credentials werken alleen voor ClusterIssuers. |
Let's Encrypt rate limits
Valideer altijd eerst met de staging issuer. Productielimieten waar teams het vaakst tegenaan lopen:
| Limiet | Waarde |
|---|---|
| Certificaten per geregistreerd domein | 50 per 7 dagen |
| Certificaten per exacte identifier set | 5 per 7 dagen |
| Nieuwe orders per account | 300 per 3 uur |
Als je deze overschrijdt, moet je wachten tot het refill-venster. Er is geen handmatige reset.
Wanneer escaleren
Voordat je een issue opent of contact zoekt met support, verzamel:
- cert-manager versie (
helm list -n cert-manager) - Kubernetes versie (
kubectl version) - Volledige output van
cmctl status certificate <naam> -n <namespace> - cert-manager controller logs voor het relevante tijdvenster
kubectl describeoutput voor de ClusterIssuer, Certificate, CertificateRequest, Order en Challenge
Wat je geleerd hebt
- cert-manager automatiseert de hele ACME-levenscyclus: registratie, challenge-validatie, certificaatuitgifte, Secret-opslag en vernieuwing.
- Twee ClusterIssuers (staging + productie) beschermen je tegen rate limits tijdens setup en debugging.
- HTTP-01 is de eenvoudige route voor publiek bereikbare services. DNS-01 is vereist voor wildcardcertificaten en private clusters.
- Ingress annotations (
cert-manager.io/cluster-issuer) zijn handig voor standaard HTTP-workloads. Het expliciete Certificate resource geeft volledige controle en is vereist voor wildcards, niet-HTTP services en Gateway API. certmanager_certificate_expiration_timestamp_secondsis de Prometheus-metric om op te alerten. Stel een waarschuwing in op 31 dagen, kritiek wanneer het Certificate 24 uur niet ready is.- Troubleshoot door de resource-keten af te lopen: Certificate, CertificateRequest, Order, Challenge.
cmctl status certificatetoont het complete plaatje in een commando.