Happy Dussehra! May good always win. Happy Diwali! Wishing you a bright year ahead. Merry Christmas! Warm wishes from Vizag. Happy New Year! Here’s to a great year ahead.

Archive · Kubernetes

Free TLS Certificates with cert-manager

Install cert-manager 1.6, configure Let's Encrypt issuers for HTTP-01 and DNS-01 challenges, issue certificates from Ingress annotations, and debug the request chain.

4 min read

  • cert-manager 1.6
  • Let's Encrypt
  • Kubernetes 1.23
  • Helm 3
  • Route 53

Manually buying and renewing certificates is how sites go down on a Sunday. cert-manager runs inside the cluster, requests certificates from an authority such as Let’s Encrypt (free, automated), stores them in Secrets, and renews them before they expire. Your Ingress just references the Secret.

Versions in use on this date. cert-manager 1.6 (October 2021) with Kubernetes 1.23; version 1.7 is expected shortly. Release 1.6 removed the long-deprecated v1alpha2, v1alpha3 and v1beta1 API versions, so everything below uses cert-manager.io/v1. Let’s Encrypt’s current chain is rooted in ISRG Root X1; the old DST Root CA X3 expired on 30 September 2021, which broke very old clients.

How a certificate is issued

flowchart TB
  ING[Ingress with annotation<br/>cert-manager.io/cluster-issuer] --> CERT[Certificate resource<br/>created by ingress-shim]
  CERT --> CR[CertificateRequest]
  CR --> ISS[ClusterIssuer letsencrypt-prod<br/>ACME account]
  ISS --> ORD[Order]
  ORD --> CH[Challenge]
  CH -->|HTTP-01: temp pod and ingress path| LE[Lets Encrypt validates over port 80]
  CH -->|DNS-01: TXT record in Route 53| LE
  LE -->|signed certificate| SEC[Secret app-tls]
  SEC --> NGX[ingress-nginx serves HTTPS]
  CERT -.->|renews at two thirds of lifetime<br/>about 30 days before expiry| CR

Install

helm repo add jetstack https://charts.jetstack.io && helm repo update
helm install cert-manager jetstack/cert-manager --namespace cert-manager --create-namespace \
  --version v1.6.1 --set installCRDs=true
kubectl -n cert-manager get pods
# optional smoke test
cat <<'EOF2' | kubectl apply -f -
apiVersion: v1
kind: Namespace
metadata: { name: cm-test }
---
apiVersion: cert-manager.io/v1
kind: Issuer
metadata: { name: selfsigned, namespace: cm-test }
spec: { selfSigned: {} }
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata: { name: test, namespace: cm-test }
spec: { secretName: test-tls, dnsNames: [test.example.com], issuerRef: { name: selfsigned, kind: Issuer } }
EOF2
kubectl -n cm-test get certificate; kubectl delete ns cm-test

Pin the chart version and manage CRDs deliberately in production (installCRDs=true is convenient; some teams apply CRDs separately so Helm upgrades never delete them).

Issuers: staging first

Let’s Encrypt has strict rate limits (for example, 50 certificates per registered domain per week and 5 duplicate certificates per week). Test with the staging endpoint, which issues untrusted certificates but has generous limits.

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata: { name: letsencrypt-staging }
spec:
  acme:
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    email: ops@example.com
    privateKeySecretRef: { name: letsencrypt-staging-account }
    solvers:
      - http01: { ingress: { class: nginx } }
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata: { name: letsencrypt-prod }
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: ops@example.com
    privateKeySecretRef: { name: letsencrypt-prod-account }
    solvers:
      - http01: { ingress: { class: nginx } }

Use an Ingress annotation; cert-manager creates the Certificate and Secret:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: shop
  namespace: shop
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-staging      # switch to letsencrypt-prod when it works
spec:
  ingressClassName: nginx
  tls:
    - hosts: [shop.example.com]
      secretName: shop-tls
  rules:
    - host: shop.example.com
      http:
        paths:
          - { path: /, pathType: Prefix, backend: { service: { name: web, port: { number: 80 } } } }
kubectl -n shop get certificate,certificaterequest,order,challenge
kubectl -n shop describe certificate shop-tls

When the staging certificate shows Ready=True, change the annotation to letsencrypt-prod, delete the shop-tls Secret so a new one is issued, and verify with curl -vI https://shop.example.com.

HTTP-01 or DNS-01?

HTTP-01 DNS-01
Proof File served at /.well-known/acme-challenge/ on port 80 TXT record _acme-challenge.<host>
Wildcards (*.example.com) No Yes
Needs public port 80 Yes No (works for internal hosts)
Credentials None DNS API access (Route 53, Cloudflare…)

For Route 53 on EKS, give cert-manager’s service account an IAM role (IRSA) allowing route53:ChangeResourceRecordSets on the hosted zone, then:

solvers:
  - dns01:
      route53: { region: ap-south-1, hostedZoneID: Z123EXAMPLE }
    selector: { dnsZones: ["example.com"] }

Debugging

Follow the chain in order: Certificate, CertificateRequest, Order, Challenge.

kubectl -n shop describe challenge
kubectl -n cert-manager logs deploy/cert-manager --tail=50
kubectl -n shop get ingress -o wide                   # the solver creates a temporary ingress
curl -v http://shop.example.com/.well-known/acme-challenge/test        # reachable from the internet?
dig +short TXT _acme-challenge.shop.example.com

Frequent causes: DNS not yet pointing to the ingress; port 80 blocked or redirected before validation (HTTP-01 follows redirects to HTTPS poorly with some setups); a NetworkPolicy blocking the solver pod; the wrong class; rate limits (error text says so); missing IAM permission for DNS-01.

Operate it

  • Alert before expiry: cert-manager exports certmanager_certificate_expiration_timestamp_seconds; alert at under 14 days.
  • Keep issuers and Certificates in Git; use Certificate resources directly for non-Ingress uses (mTLS, internal services with a private CA issuer).
  • Back up the ACME account Secrets, and watch for upgrade notes between versions.
  • Let’s Encrypt certificates last 90 days and are renewed automatically at about two-thirds of their life.