Kubernetes TLS met cert-manager: geautomatiseerd certificaatbeheer

cert-manager automatiseert het uitgeven en vernieuwen van TLS-certificaten op Kubernetes via Let's Encrypt. Deze tutorial loopt door elke stap: cert-manager installeren met Helm, ClusterIssuers aanmaken voor staging en productie, HTTP-01 en DNS-01 challenges configureren, certificaten uitgeven voor Ingress en Gateway API resources, en certificaatvervaldatum monitoren met Prometheus.

Inhoudsopgave

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)
  • kubectl geconfigureerd 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:

  1. Jij (of een Ingress-annotation) maakt een Certificate resource aan.
  2. cert-manager maakt een CertificateRequest aan, daarna een ACME Order.
  3. Per domein in de order maakt cert-manager een Challenge aan (HTTP-01 of DNS-01).
  4. Let's Encrypt valideert de challenge en geeft het certificaat uit.
  5. cert-manager slaat tls.crt en tls.key op in het doelSecret.
  6. 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.0 pint de chartversie. Zonder deze flag pakt Helm de laatste versie, en dat kan voor verrassingen zorgen tijdens een productiewijziging.
  • --set crds.enabled=true installeert 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-scoped Issuer. 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 describe output 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_seconds is 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 certificate toont het complete plaatje in een commando.

Terugkerende server- of deploymentproblemen?

Ik help teams productie betrouwbaar maken met CI/CD, Kubernetes en cloud—zodat fixes blijven en deploys geen stress meer zijn.

Bekijk DevOps consultancy

Doorzoek deze site

Begin met typen om te zoeken, of blader door de kennisbank en blog.