Contents

cert-manager: Automated TLS Certificates That Actually Renew

Let Kubernetes do the certificate paperwork so you never get the expiry email again

Contents

I once let a certificate expire on a Sunday morning. Not a test box — the box that mattered. The expiry email had arrived eleven days earlier with the subject line containing the word “expires,” and I had archived it the way you archive everything that isn’t on fire yet. By the time visitors started seeing browser interstitials, I was SSH’d into a server trying to remember which flags certbot wanted while my coffee went cold and the clock ran. TLS certificates expire on a schedule precisely engineered to be longer than your memory and shorter than your attention span, and the manual-renewal model fails in exactly this way: silently, then all at once.

cert-manager exists to end that. It’s a Kubernetes controller that treats certificates as first-class cluster resources. You declare the certificate you want as a manifest, and the controller obtains it, stores it as a secret, and — crucially — renews it before expiry, forever, without you doing anything. The “actually renews” in the title is doing real work, because renewal is the part everyone else’s automation quietly skips. A cron job that fetches a certificate is easy. A cron job that fetches it, notices when renewal fails, retries with backoff, and tells you before the certificate dies is not a cron job any more — it’s a controller, and cert-manager is the one most of the Kubernetes world already runs.

This matters more now than it did a year ago. Let’s Encrypt has been pushing certificate lifetimes down: 90 days is still the classic default, but short-lived six-day certificates went generally available in January 2026, and the classic profile is scheduled to drop to 64 days in early 2027 and 45 days in 2028. Shorter lifetimes mean more renewals, and more renewals mean manual processes break more often. Automation stops being a nicety and becomes the only sane option. If you are weighing up the broader effort of running your own stack, the renewal treadmill is one of those hidden costs I covered in the real cost of self-hosting — and it’s exactly the sort of toil worth automating away once and never thinking about again.

The mental model

Advertisement

cert-manager adds a handful of custom resources to your cluster. The two you’ll touch most are Issuer (or ClusterIssuer, its cluster-wide sibling) and Certificate. An Issuer describes how certificates are obtained — which certificate authority, which account, which challenge method. A Certificate describes what you want — the domain names, where to store the resulting secret, how long before expiry to renew.

cert-manager watches these, talks to the CA on your behalf, completes whatever proof-of-ownership dance is required, and drops a standard Kubernetes TLS secret into your namespace. Your ingress controller mounts that secret like any other. Nothing downstream needs to know cert-manager exists — and that decoupling is the design’s quiet genius. The thing that renews your certificate has no opinion about what serves it.

Installation is a single Helm chart or a manifest apply. The chart installs three deployments — the controller, the webhook, and the cainjector — plus the custom resource definitions. The webhook is the part people forget about: it validates your Issuer and Certificate manifests at admission time, which means a typo in your ACME server URL gets rejected immediately instead of failing mysteriously twenty minutes later. If you run Helm for your other workloads, this is one more reason to understand the tool properly; I went into when it earns its place and when it gets in the way in Helm charts demystified.

A Let’s Encrypt issuer

The overwhelmingly common case is Let’s Encrypt with an HTTP-01 challenge, where the CA proves you control a domain by fetching a token over HTTP. Here’s a ClusterIssuer pointed at Let’s Encrypt’s production endpoint:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: [email protected]
    privateKeySecretRef:
      name: letsencrypt-prod-account-key
    solvers:
      - http01:
          ingress:
            class: nginx

Two things I beg you to do. First, start against the staging endpoint (https://acme-staging-v02.api.letsencrypt.org/directory) until your config is right. Production Let’s Encrypt has rate limits — fifty certificates per registered domain per week is the one that bites homelabbers — and you can lock yourself out for days by hammering it with a misconfigured renewal loop. The staging CA has far looser limits and issues certificates signed by an untrusted root, which is perfect: your browser will complain, but you’re only proving the machinery works, not serving real traffic. Switch the server URL to production once the staging certificate issues cleanly. Second, that email is where expiry warnings go if cert-manager ever stops working entirely, so use a real address you actually read, not a placeholder.

If you want to opt into the new six-day short-lived certificates, you set the ACME profile on the Issuer rather than changing anything about how you request certificates — cert-manager handles the more frequent renewal cycle transparently. For most homelab setups the 90-day default is fine; the short profile is more interesting for environments where you want a stolen key to be useless within a week.

You usually don’t write Certificates by hand

Advertisement

You can request a Certificate resource explicitly, but in practice you let your ingress do it for you with annotations. Add one annotation and a tls block, and cert-manager notices, creates the Certificate behind the scenes, solves the challenge, and populates the secret:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: blog
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - blog.example.com
      secretName: blog-tls
  rules:
    - host: blog.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: blog
                port:
                  number: 80

Apply that and watch the machinery turn. cert-manager creates a temporary Ingress route to answer the challenge, Let’s Encrypt fetches the token, the certificate is issued, and blog-tls appears. From here on it’s hands-off — and you can watch the state with kubectl describe certificate blog-tls, which spells out exactly which stage it’s at if something stalls.

The renewal is the whole point

A Certificate from Let’s Encrypt lasts 90 days. cert-manager renews it at roughly the two-thirds mark — around 30 days before expiry by default — completely automatically. It re-runs the challenge, gets a fresh certificate, updates the secret in place, and your ingress controller picks up the new secret. You do nothing. You receive no email. You forget certificates exist, which is the correct relationship to have with them.

This is the difference between cert-manager and a cron job calling certbot. The cron job works until the day the renewal command silently fails and nobody notices for a month. cert-manager surfaces failures as events and conditions on the resource, retries with backoff, and integrates with monitoring so a stuck renewal can page you instead of ambushing you.

DNS-01 for the wildcard crowd

If you want a wildcard certificate, or your services aren’t reachable from the public internet for an HTTP challenge, you switch to a DNS-01 solver. cert-manager proves ownership by creating a TXT record (_acme-challenge.example.com) via your DNS provider’s API, waiting for it to propagate, and letting the CA verify it. It’s more setup — you’re handing cert-manager API credentials, which you should scope as tightly as your provider allows — but it’s the only way to get *.example.com, and it works for internal-only services that the public internet can never reach to answer an HTTP-01 challenge. Most managed DNS providers have a supported solver, either built in or as a community webhook.

1
2
3
4
5
6
7
8
9
solvers:
  - dns01:
      cloudflare:
        apiTokenSecretRef:
          name: cloudflare-api-token
          key: api-token
    selector:
      dnsZones:
        - "example.com"

The credential lives in a secret, never in the manifest. Scope the token to DNS edit on that single zone and nothing else — a leaked token that can only twiddle TXT records on one domain is a much smaller problem than one that can repoint your whole account.

When it goes wrong: troubleshooting

cert-manager is reliable until it isn’t, and when issuance stalls the failure is rarely where you first look. The single most useful command is kubectl describe, walked down the chain of resources cert-manager creates:

1
2
3
4
kubectl describe certificate blog-tls          # high-level state
kubectl describe certificaterequest <name>     # the CSR sent to the issuer
kubectl describe order <name>                   # the ACME order
kubectl describe challenge <name>               # the actual challenge attempt

Each layer points at the next. The four failures I hit most often:

  • Challenge stuck “pending” on HTTP-01. Almost always a routing problem: the temporary /.well-known/acme-challenge/ path isn’t reaching cert-manager’s solver pod. Check that your ingress class matches the issuer’s solvers.http01.ingress.class, and that no other rule is shadowing that path. Curl the challenge URL yourself from outside the network — if you can’t fetch it, neither can Let’s Encrypt.
  • DNS-01 timing out. The TXT record was created but the CA checked before it propagated, or split-horizon DNS is serving a different answer internally than externally. cert-manager retries, so give it a few minutes, but if it never clears, dig the _acme-challenge record from a public resolver to confirm what the world actually sees.
  • Rate-limited. You’ll see 429 errors and a tooManyCertificates message. This is what staging exists to prevent. Wait out the window — there is no override — and fix the loop that caused it before switching back to production.
  • Renewal silently not happening. Rare, but check the controller logs (kubectl logs -n cert-manager deploy/cert-manager) and confirm the controller pod is actually running. A cert-manager that’s been CrashLoopBackOff for a fortnight will happily let everything expire. This is the one thing worth a monitoring alert: a probe on certificate expiry that pages you regardless of whether cert-manager thinks it’s healthy.

That last point is the whole philosophy. Automate the renewal, but monitor the outcome, not the automation — because the failure mode that hurts is the one where your automation believes it’s fine.

Is it worth it?

For a single static box, honestly, plain certbot with its systemd timer is fine and cert-manager is overkill — you’d be installing three deployments and a clutch of CRDs to manage one certificate. But the moment you’re on Kubernetes with more than one or two TLS endpoints, cert-manager stops being optional. It’s the kind of infrastructure that earns its keep by being boring: you install it once, wire your ingress to it, and certificates stop being something you think about. I’ve had clusters running for years where I genuinely could not tell you when the certificates renew, because I have never once had to care.

Who is it for? Anyone running ingress on Kubernetes, full stop. If you’re self-hosting services worth protecting — a Vaultwarden password manager where a certificate warning would (rightly) terrify you, or a Mealie recipe box you want to reach cleanly from a phone — the marginal cost of doing TLS properly is one afternoon, and the payoff is never living that cold-coffee Sunday again. With lifetimes shrinking towards 45 days and short-lived profiles already here, the manual model has run out of road. Automate it, alert on the result, and forget it exists. That is exactly the relationship you should have with certificates.

Advertisement
Advertisement
Smarc
Written by Smarc

Founder and editor of vo.rs. A lifelong tinkerer who self-hosts far more than is sensible, hardens Linux boxes for fun, and prods the latest AI tools to see what they can really do. The how-to guides here are the notes Smarc wishes had existed the first time round.