Kubernetes Security & Hardening›02 · cert-manager in production

Lesson 02 of 15 · Identity & Access

cert-manager in production

Automate TLS certificates inside Kubernetes with cert-manager: issuers for Let's Encrypt and your own private CA, Certificate resources, automatic renewal, and monitoring expiry before it bites.

Advanced
Key wordscert-managerIssuerClusterIssuerCertificateACMELet's EncryptHTTP-01DNS-01private CA

Why cert-manager

Ingresses, Gateways, webhooks, internal mTLS: a platform has many certificates, each with an expiry date. cert-manager turns certificates into Kubernetes resources: you declare what you need, and it issues, stores (as a Secret) and renews them automatically.

Instead of queuing at the office every year to renew everyone's ID card, you hire an assistant (cert-manager) with a list: "Asha's card, Ben's card, the library door card". The assistant fetches each card, puts it in the right locker, and renews it a month before it expires, without anybody asking.

The objects

Object Meaning
Issuer / ClusterIssuer Where certificates come from: Let's Encrypt (ACME), your CA, Vault, self-signed
Certificate What you want: names, duration, which issuer, which Secret to store it in
CertificateRequest, Order, Challenge Internal steps you inspect when something is stuck

Install

$ helm repo add jetstack https://charts.jetstack.io && helm repo update
$ helm install cert-manager jetstack/cert-manager \
    --namespace cert-manager --create-namespace --set crds.enabled=true
$ kubectl -n cert-manager get pods

(Older chart versions used --set installCRDs=true. Check the chart's documentation for your version.)

A private CA for internal services

Bootstrap a CA inside the cluster: a self-signed issuer creates a CA certificate, and a CA issuer signs everything else with it.

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: selfsigned-bootstrap
spec:
  selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: platform-ca
  namespace: cert-manager
spec:
  isCA: true
  commonName: platform-internal-ca
  secretName: platform-ca
  duration: 87600h            # 10 years
  privateKey:
    algorithm: ECDSA
    size: 256
  issuerRef:
    name: selfsigned-bootstrap
    kind: ClusterIssuer
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: platform-ca
spec:
  ca:
    secretName: platform-ca   # read from the cert-manager namespace for ClusterIssuers

Now any team can request a certificate:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: orders-api-tls
  namespace: shop
spec:
  secretName: orders-api-tls
  dnsNames:
    - orders-api.shop.svc
    - orders-api.shop.svc.cluster.local
  duration: 2160h             # 90 days
  renewBefore: 360h           # renew 15 days before expiry
  issuerRef:
    name: platform-ca
    kind: ClusterIssuer
$ kubectl -n shop get certificate orders-api-tls
NAME             READY   SECRET           AGE
orders-api-tls   True    orders-api-tls   12s
$ kubectl -n shop get secret orders-api-tls -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -noout -subject -enddate

Public certificates with Let's Encrypt (ACME)

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  acme:
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    email: platform-team@example.com
    privateKeySecretRef:
      name: letsencrypt-staging-account
    solvers:
      - http01:
          ingress:
            ingressClassName: traefik     # the class of your ingress controller
  • HTTP-01: Let's Encrypt fetches a token over HTTP from your domain. Simple, but needs the domain publicly reachable on port 80, and doesn't do wildcards.
  • DNS-01: cert-manager creates a TXT record through your DNS provider's API. Works for private services and wildcards.

With an issuer in place, annotate an Ingress and cert-manager creates the Certificate for you:

metadata:
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-staging
spec:
  tls:
    - hosts: [ "shop.example.com" ]
      secretName: shop-example-com-tls

Switch to https://acme-v02.api.letsencrypt.org/directory (production) once the staging flow works.

Try it: a private CA on kind

  1. Install cert-manager with Helm.
  2. Apply the three private-CA objects above and check kubectl get clusterissuers shows READY True.
  3. Request orders-api-tls in a namespace, then decode the Secret and check the SANs and expiry with openssl.
  4. Delete the Secret and watch cert-manager re-issue it within seconds.
  5. Set duration: 1h and renewBefore: 30m on a test certificate and watch kubectl get certificate -w renew it (look at the notAfter field in kubectl describe).

Going deeper: certificates at platform scale

  • Monitor with cert-manager's Prometheus metrics (certmanager_certificate_expiration_timestamp_seconds, certmanager_certificate_ready_status) and alert on certificates not Ready or expiring within 14 days.
  • Trust distribution: trust-manager (a cert-manager project) distributes CA bundles to namespaces as ConfigMaps, so apps can trust your internal CA.
  • For a real private PKI, keep the root CA offline and give cert-manager an intermediate (or use Vault/a cloud private CA as the issuer).
  • Stuck issuance? Follow the chain: Certificate → CertificateRequest → Order → Challenge. The first one with a failing condition tells you why.

Recap

  • cert-manager makes certificates declarative: Issuers/ClusterIssuers + Certificates → Secrets, renewed automatically.
  • Private CA: self-signed bootstrap → CA certificate → CA ClusterIssuer.
  • ACME: HTTP-01 (simple) or DNS-01 (private and wildcard); staging first.
  • Monitor readiness and expiry; debug along Certificate → CertificateRequest → Order → Challenge.

This site is a public version of my personal engineering knowledge hub. It intentionally excludes confidential company information and internal operational details.