cert-manager: The X.509 Controller That Made TLS a Kubernetes Resource

Jetstack built cert-manager in 2017 to fix what kube-lego could not: certificates, issuers, and challenges as real Kubernetes resources. The v0.11 API rename then made every upgrading cluster uninstall and reinstall to get there.

Sources

The default answer to "how do you get TLS certificates into Kubernetes" started as a London consultancy's second attempt at the problem. The first attempt, kube-lego, watched Ingress annotations and asked Let's Encrypt for certificates — and kept its state where nobody could inspect it. The second attempt, cert-manager, made the opposite bet: certificates, issuers, orders, and challenges would all be first-class Kubernetes resources, watchable with kubectl describe and automatable with the same machinery that runs the rest of the cluster. The bet required breaking the project's own API so badly in 2019 that the official upgrade path was "back up, uninstall, reinstall" — and it still ended in a CNCF graduation, three corporate acquisitions of the founding company, and a tool that most clusters now install without thinking about any of this history.

The Origin: kube-lego's Limits, a Consultancy's Pain, and a CRD Bet (2016–2017)

kube-lego: the annotation-driven predecessor (2016)

The thing cert-manager replaced was also a Jetstack-era project. kube-lego, whose repository was created on and is archived today, automated the obvious 2016 problem: you annotated a Kubernetes Ingress with the domains you wanted, and kube-lego asked Let's Encrypt for a certificate and wrote it into a Secret. For a year this was the state of the art. Its ceiling was architectural: the process held issuance state in memory, exposed nothing as an API object, and offered one issuer type — Let's Encrypt, one challenge at a time. cert-manager's own README is the primary source on the lineage: the project "is loosely based upon the work of kube-lego and has borrowed some wisdom from other similar projects e.g. kube-cert" — a sentence preserved today on the Go module docs page. kube-lego was formally closed on August 26, 2021, when PR #346 updated its README "to say that the project is no longer maintained" — four years after cert-manager had taken over its user base.

The gap between the two designs is the whole story of this project, so it is worth stating plainly: kube-lego was a client of the Kubernetes API that happened to run in a cluster; cert-manager was designed as a native extension of the Kubernetes API. That difference — state in a process versus state in etcd, annotations versus resources — is what everything else in this tale follows from.

The first commit, and an apiVersion that never shipped (July 2017)

Jetstack, a London Kubernetes consultancy founded in 2015, created the repository on under the path jetstack-experimental/cert-manager — the name survives in the v0.1.0 release notes, which link their own quick-start guide at that old URL. The root commit landed on , authored by James Munnelly with the message initial commit: 36308ae8. The scaffold is a time capsule: a single controller at cmd/controller/main.go, API types at pkg/apis/certmanager/v1alpha1/types.go, and — most revealing of all — a design document posing as an example manifest at docs/certificate.yaml.

# Example ACME certificate using DNS
apiVersion: certmanager.kubernetes.io/v1
kind: Certificate
metadata:
  name: my-certificate
spec:
  domains:
  - www.google.com
  acme:
    challenge: dns-01
    url:  https://...
    email: test@example.com
    dns:
      clouddns: {}
status:
  state: Pending
---
# Example ACME certificate using Ingress
apiVersion: certmanager.kubernetes.io/v1
kind: Certificate
metadata:
  name: my-certificate
spec:
  domains:
  - www.google.com
  acme:
    challenge: http-01
    url:  https://...
    email: test@example.com
    ingressRef:
      name: something
status:
  state: Pending

Two things about that file deserve a slow read. First, the apiVersion is certmanager.kubernetes.io/v1 — a group under kubernetes.io, and a v1 that never shipped: the public releases opened at v1alpha1, in a certmanager.k8s.io group. Ambition outran both the API plan and the namespace in the first week. Second, the design is monolithic: the Certificate type embeds its ACME configuration — the release-era types.go defines CertificateSpec as Domains []string plus a ProviderConfig wrapper whose only inhabitant is ACME *ACME. There is no Issuer kind anywhere in the root tree. The separation of what you want (a Certificate) from how it gets signed (an Issuer) — the idea the entire project would eventually be built on — did not exist on day one. It arrived by v0.1.0, alongside the ClusterIssuer, in October 2017.

cert-manager/cert-manager  (GitHub repo ID 92313258, created 2017-05-24)
├── root  36308ae8  "initial commit"                     (James Munnelly, 2017-07-19)
│            — scaffold: cmd/controller/main.go,
│              pkg/apis/certmanager/v1alpha1/types.go, docs/certificate.yaml
└── path  jetstack-experimental/cert-manager (2017, per the v0.1.0 release notes)
           → jetstack/cert-manager        (same repo object, renamed)
           → cert-manager/cert-manager    (org move completed 2022-01-05, issue #3451)

jetstack/kube-lego  (repo created 2016-03-06; 2.1k stars; archived)
└── last act  PR #346  "Update README to say that the project is no
              longer maintained"  (merged 2021-08-26) — the predecessor
              cert-manager says it was "loosely based upon"

v0.1.0: the release that admitted everything (October 2017)

The first release, published , opens with a sentence vendors do not usually ship: "It is currently still not in a production ready state, and features are subject to change." What v0.1.0 did contain was already the project's core differentiator set: ACME DNS-01 challenges with Google Cloud DNS, AWS Route53, and Cloudflare providers; HTTP-01 challenges "compatible with all ingress controllers following ingress spec (GCE & nginx tested)"; simple CA-based issuance from a Secret-held keypair; cluster-wide issuers ("aka ClusterIssuer"); and the line that separated it from kube-lego forever — "Backed by CRDs," with events written to the Kubernetes API and a status block used for state. The release notes end with a community pitch, not a roadmap: "open some issues, comment or pick something up and get started." The 38 contributors credited by v0.3.0 six months later suggest that pitch worked.

The Timeline: From a Niche CRD Controller to the Default TLS Layer

Crisis Points & Architectural Pivots

Pivot one: annotations in, resources underneath (2018)

cert-manager's most-used interface today is still the one it inherited from kube-lego: annotate an Ingress, get a certificate. The difference is what the annotation now means. In May 2018, v0.3.0 merged ingress-shim — until then a separate tool — into the main controller (#502, @munnerz), and renamed its flags into the process (--ingressShim.defaultIssuerName and friends; today the Helm chart values ingressShim.defaultIssuerName, ingressShim.defaultIssuerKind, ingressShim.defaultIssuerGroup). The annotation is now a thin front-end for the resource model: ingress-shim reads it, creates or updates a Certificate resource, and the normal issuance machinery does the rest. The kube-lego interaction was kept alive deliberately — as a shim over a real API — which is why the migration off kube-lego never required anyone to change a single Ingress manifest.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    # add an annotation indicating the issuer to use.
    cert-manager.io/cluster-issuer: nameOfClusterIssuer
  name: myIngress
  namespace: myIngress
spec:
  rules:
  - host: example.com
    http:
      paths:
      - pathType: Prefix
        path: /
        backend:
          service:
            name: myservice
            port:
              number: 80
  tls: # placing a host in the TLS config determines what ends up in the cert's subjectAltNames
  - hosts:
    - example.com
    secretName: myingress-cert # cert-manager will store the created certificate in this secret.

Pivot two: ACME state becomes API objects (2019)

The second pivot is the one that made cert-manager debuggable. In January 2019, v0.6.0 introduced the Order and Challenge resource types and, in the release's own words, "re-implemented ACME Issuer to be completely driven by CRDs" (#788, @munnerz). Before this, an in-flight ACME authorization was controller state; after it, every step — the order taken against the ACME server, the challenge it demanded, the solver pod or DNS TXT record standing behind it — became an object with events and a status you could read with kubectl describe. The operational significance is hard to overstate: a failed HTTP-01 challenge now fails in the same place you debug everything else, instead of inside a controller log you have to know to fetch. It also created the surface that 2026's high-severity advisory would later land on (see the security arc below): state in the API is inspectable by everyone, including actors you did not intend.

Pivot three: CertificateRequest — the issuer contract (2019)

In July 2019, v0.9.0 introduced the CertificateRequest resource type, described in the release notes as "a step towards where we want to be for 1.0, defining an API specification for Certificates and allowing anyone to implement their own issuers and CAs as first class citizens." The architecture that resulted — Certificate describes what you want, CertificateRequest carries the X.509 CSR to whichever issuer can sign it — did two things at once. It let new issuer types (the v0.11 notes name the Smallstep step-issuer, an experimental CSI driver for mounting certs into pods, an Istio node-agent integration, and "retrieving certificates without giving cert-manager access to your private keys") slot in without touching the core controller. And it produced a vendor ecosystem: the org's webhook-example repo — the sample for building third-party ACME DNS01 solver webhooks — counts 534 forks as of 2026-11-07. Issuers for Vault, Venafi, Google CA Service, AWS Private CA, and every regional DNS provider on earth exist because this contract exists.

2017 — v0.1: one resource, one controller, ACME embedded in the Certificate

    Ingress ──► cert-manager controller ──► Let's Encrypt (ACME)
                     (in-memory state)  └──► Secret (kubernetes.io/tls)


2019+ — the five-resource model (Order/Challenge v0.6, CertificateRequest v0.9)

    Certificate (cert-manager.io/v1)   created by: ingress-shim | Gateway API shim
         │  spec.issuerRef                 | CSI driver | your manifests
         ▼
    Issuer / ClusterIssuer   type: acme | ca | selfsigned | vault | venafi | external
         │  controller reconciles the Certificate into...
         ▼
    CertificateRequest (the CSR — the issuer contract any CA can implement)
         │ [acme path]                        │ [external issuer path]
         ▼                                    ▼
    Order (acme.cert-manager.io)         any controller watching
         ▼                                CertificateRequests
    Challenge
      ├─ http01: solver pod + service, or edit-in-place Ingress
      └─ dns01:  TXT record via in-tree provider or a DNS01 webhook
         ▼
    Secret (kubernetes.io/tls) ──► Ingress / Gateway / pod mounts (CSI) /
                                   CA injection targets (cainjector)

The v0.11 migration: the breaking change that cleared the road to v1

October 2019 is the crisis this project is built on. v0.11.0 renamed the API group from certmanager.k8s.io to cert-manager.io, bumped the version from v1alpha1 to v1alpha2, removed fields deprecated in v0.8, renamed every annotation prefix, and switched the status subresource on — all "in preparation for moving into v1beta1 and eventually v1," as the release put it. The cost was stated without apology: "It's recommended that you backup and completely uninstall cert-manager before re-installing the v0.11 release," followed by a manual rewrite of every manifest's apiVersion:

Kind               | Old apiVersion                 | New apiVersion
-------------------|-------------------------------|--------------------------------------
Certificate        | certmanager.k8s.io/v1alpha1   | cert-manager.io/v1alpha2
Issuer             | certmanager.k8s.io/v1alpha1   | cert-manager.io/v1alpha2
ClusterIssuer      | certmanager.k8s.io/v1alpha1   | cert-manager.io/v1alpha2
CertificateRequest | certmanager.k8s.io/v1alpha1   | cert-manager.io/v1alpha2
Order              | certmanager.k8s.io/v1alpha1   | acme.cert-manager.io/v1alpha2
Challenge          | certmanager.k8s.io/v1alpha1   | acme.cert-manager.io/v1alpha2

The retrospective framing came a year later, in the v1.0.0 release notes: "In the past 16 releases many bugs got fixed, and things that needed to be broken were broken." That is the honest summary of the v0.x era — eight minor lines in two years, at least four of them carrying action-required migrations. The cure was architectural, not procedural: the conversion webhook. v0.15.0 (May 2020) made the webhook a required component and introduced v1alpha3 — "This change is required in order to support the upcoming changes to our API versions, as we introduce v1alpha3, v1beta1 and v1 over the coming months!" — with a feedback plea for firewall-rule configurations that break API-server-to-webhook traffic. From that point, API versions changed through conversion rather than uninstall rituals: v1beta1 arrived in v0.16.0 (#3038), and v1.0.0 (September 2020) shipped cert-manager.io/v1 — the apiVersion still in every manifest today, six years and ten minor versions later, exactly as the release's "compatibility promise with our v1 API" committed.

apiVersionFirst shippedWhat it cost you
certmanager.k8s.io/v1alpha1v0.1.0 (Oct 2017)Certificate carried ACME config inline; 16 releases of action-required upgrades followed
cert-manager.io/v1alpha2v0.11.0 (Oct 2019)Back up, uninstall, rewrite every manifest, reinstall
cert-manager.io/v1alpha3v0.15.0 (May 2020)Webhook becomes a required component — clusters that block webhook traffic now need firewall work
cert-manager.io/v1beta1v0.16.0 (Jul 2020)Nothing — the conversion webhook absorbs it
cert-manager.io/v1v1.0.0 (Sep 2020)Nothing — the compatibility promise; still current as of v1.21.3 (2026)

What you write today: the v1 API in practice

Two resources still cover the common case — a Certificate and a Let's Encrypt issuer. What changed since 2017 is the expressiveness: renewal windows on a cron schedule, key algorithm and encoding, PKCS#12/JKS keystores with named profiles, subject and name constraints. The samples below are the current documented forms.

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: example-com
  namespace: sandbox
spec:
  secretName: example-com-tls
  dnsNames:
    - example.com
  issuerRef:
    name: ca-issuer
    kind: Issuer
  duration: 2160h # 90d
  renewBefore: 360h # 15d
  renewal:          # v1.21: renewal policies — cron-scheduled renewal windows
    policy: RenewBefore
    windows:
      - cron: "0 2 * * 1-5"
        timezone: America/Denver
        windowDuration: 2h
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  acme:
    email: user@example.com
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      # Secret that will store the ACME account key — "your identity with
      # your ACME provider"; lose it and you cannot revoke prior certificates
      name: example-issuer-account-key
    solvers:
    - http01:
        ingress:
          ingressClassName: nginx
      # for split-horizon DNS / NAT hairpin environments, v1.21 added:
      # waitInsteadOfSelfCheck: 30s   # skip cert-manager's self-check and wait instead
# full issuance state: Certificate + CertificateRequest + events in one view
cmctl status certificate example-com

# force an immediate renewal (e.g. after a CA compromise or mis-issuance)
cmctl renew example-com

The security arc: from "no CVEs" to a graduated-era high

The project's TAG Security self-assessment (June 2024) contains a sentence that reads oddly today: "There haven't been any CVEs or notable security reports made about cert-manager." It was true — the audit-era findings were the Ada Logics report's eight issues (the most interesting: an out-of-memory denial of service from a crafted certificate subject in an alpha feature, and servers missing timeouts), all fixed across v1.12.8, v1.13.4, and v1.14.3, with the audit also folding cert-manager into Google's OSS-Fuzz and deleting three low-scoring dependencies outright (pkg/errors #6793, go-ldap/ldap #6761, gomodules/jsonpatch #6455). The audit announcement called it "the last major blocker for cert-manager's journey to graduation."

The graduated era has been more eventful. GHSA-r4pg-vg54-wxx4 (November 2024, low) flagged a parsing DoS on crafted PEM input. GHSA-gx3x-vq4p-mhhv (February 2026, medium) was a controller DoS via specially crafted DNS responses. And GHSA-8rvj-mm4h-c258 (June 2026, high) is the instructive one: directly created ACME Challenge resources could bypass an Issuer's DNS01 solver policy and use ClusterIssuer DNS credentials — a privilege-boundary flaw enabled by the very design pivot (challenge state as API objects) that made the project debuggable in 2019. The fix was not a revert but a tightening of the cert-manager-edit aggregate ClusterRole, shipped simultaneously to v1.19.6, v1.20.3, and v1.21.0 — a three-branch coordinated patch of a kind the project had never needed before. The design lesson generalizes: every resource you add to the API is a capability you grant to everyone with create permissions.

Worth crediting: v1.21.0's own release notes carry a known-issues section naming a controller crash-loop triggered by renewal.policy: Disabled (#9031 — "do not set renewal.policy: Disabled on any Certificate until this is fixed"), cosmetic log spam from non-cert-manager Secret events (#8994), and an Issuer that can stick at Ready: False, Reason: InvalidSolver until a 10-hour informer resync or a spec edit (#9036). Shipping your own toil list in the release notes is the anti-hype pattern this site wishes every project copied.

Community Engine & Corporate Influence: Who Actually Built It

The cast is smaller than Helm's merged-two-projects origin, and the corporate story is stranger. cert-manager was built at Jetstack, the London Kubernetes consultancy founded in 2015 by Matt Bates (CTO) and Josh Barker. James Munnelly — the root commit's author — carried the project architecturally from the 2017 scaffold through the v0.6 ACME rewrite, the v0.9 CertificateRequest contract, and the v0.11 migration, and the release notes' @munnerz attribution recurs on exactly those pivot PRs. The generation after him — Maël Valais and Ashley Davis authored the 2024 TAG Security self-assessment — carried the project through graduation.

The ownership chain cert-manager's maintainers rode through is four companies deep: Jetstack → Venafi (announced May 14, 2020; TechCrunch reported the deal expected to close that June, with both co-founders joining Venafi's executive team) → CyberArk (completed its ~$1.54B acquisition of Venafi from Thoma Bravo on October 1, 2024 — while the graduation process was mid-flight) → Palo Alto Networks (completed its acquisition of CyberArk on February 11, 2026). The CNCF's own graduation press release had to compress this into one clause: "created in 2017 at Jetstack, which is now a part of Venafi, a CyberArk company."

What kept a four-acquisition chain from destabilizing the project is the same move Helm made, executed faster: foundation ownership, timed deliberately. The Sandbox donation landed in November 2020 — six months after the Venafi acquisition, and the org-move issue (#3451) opened the same day the donation was accepted. The graduation announcement's thank-you list is candid about the funding asymmetry: "Venafi for sponsoring the bulk of the maintainer time required to see this process through," and the audit announcement repeats it — "Venafi who sponsored maintainer time to respond to and fix the findings." The maintainer you can see doing that work today signs his trust-manager commits as wallrj-cyberark. One company funds most of the engineering; the foundation owns the project. That arrangement — not neutrality — is what cert-manager, Helm (see our Helm tale), and most of the graduated portfolio actually run on.

Scale, for the record: 601 contributors (GitHub contributors endpoint including anonymous, 2026-11-07), 14,151 stars, 2,487 forks, roughly 11,600 commits, and more than 200 releases since October 2017 — against a maintainer group small enough to fit in one Slack channel, which is the graduation blog's honest subtext when it asks readers to "reach out on Slack or join one of our regular meetings."

The sub-project family

cert-manager's architecture review increasingly happens in its satellite repos, each created to keep a concern out of the core: istio-csr (2020) signs Istio workload identities using any cert-manager issuer; approver-policy (2021) gates CertificateRequests behind approvable policy — the answer to "who may request a certificate for that name"; trust-manager (2021) distributes CA trust bundles cluster-wide — issuance and trust were split because they are different problems; cmctl (2024) became the standalone CLI at the v1.14 boundary; and the webhook-example template (2019) is where five hundred-plus DNS-provider integrations were born. The commercial layer — Jetstack Secure, launched February 2021, renamed TLS Protect for Kubernetes, now CyberArk Certificate Manager for Kubernetes — sells multi-cluster visibility and enterprise support on top of exactly this open-source core, the classic open-core split with the foundation owning the core.

Current Trajectory: The Verdict

Late 2026 cert-manager shows the metabolism of a mature graduated project: minors land roughly every four months (v1.12 May 2023 through v1.21 July 2026, with a drift to ~5-month spacing in 2026), patches flow on at least two lines simultaneously (v1.21.3 on October 23 and v1.20.5 on October 28), and the June 2026 security fix shipped across three. The feature direction is issuer-protocol maturity, not new surface: ACME Renewal Information support (RFC 9773, behind the ACMEUseARI gate) means a Let's Encrypt mass-revocation or CA key rollover can push renewals instead of waiting for clients' clocks; renewal policies add cron-scheduled windows; the Modern2026 PKCS#12 profile replaces 3DES/RC2 legacy encodings for FIPS 140-3 environments; and a webhook fix now recovers missed renewals within a minute of system suspend — a detail only a project with laptop-cluster users would think to ship.

The most current evidence that this ecosystem earns its keep is an incident: the eMudhra CA compromise of late 2026. When the compromised roots hit the news, trust-manager v0.26.0 (October 27) shipped within days removing the eMudhra roots from the default CA bundle, rebuilt its trust-package images, and moved to a Go toolchain that distrusts the compromised intermediates (CVE-2026-111943) — while documenting bluntly that the v0.25.0 chart's default bundle still trusted them. Issuance automation (cert-manager) plus trust automation (trust-manager) is what made that response days instead of a quarter-long certificate audit. The graduation blog's forward commitments — post-quantum adoption "when standards are ready," trust-manager growth, machine-identity scale — map cleanly onto what the project actually shipped next.

Who should skip it? Teams whose TLS terminates before the cluster: if your certificates live on a managed load balancer (AWS ACM, Google-managed certificates, Cloudflare edge TLS), in-cluster issuance has nothing to issue for. Teams with locked-down platforms that prohibit admission webhooks or third-party CRDs — the conversion webhook has been required since v0.15, so this is not optional software; that firewall exception to the apiserver is part of the deployment. Teams who need workload identity rather than ingress TLS: SPIFFE/SPIRE is the right primitive, and istio-csr is the bridge if you run Istio. And teams that need trust distribution rather than issuance — pushing CA bundles to every namespace is trust-manager's job, a distinction people learn the hard way.

Verdict: cert-manager is the rare tool whose founding architectural bet — everything as a Kubernetes resource — survived contact with reality so completely that the bet is now invisible. The 2019 breaking migration is the reason the API you write today is cert-manager.io/v1 and has not changed in six years; the 2020 foundation donation is the reason four corporate acquisitions never became a governance event; the 2024 audit is the reason the 2026 high-severity fix rolled out in days across three branches instead of becoming a community fight. For platform teams the operational toil is real — debugging failed orders is an events-and-status ceremony, the webhook is a hard dependency, and the RBAC surface needs review (don't let anyone create Challenge resources) — but the alternative is the world cert-manager replaced: certificates as untracked state, expiring on Friday nights. Install it with the Helm chart (see our Helm tale for the packaging history), pair it with trust-manager for bundles, monitor the Ready condition rather than expiry dates, and let the ingress-shim annotations keep doing what kube-lego started in 2016 — now with a real API underneath. (For the ingress machinery the HTTP-01 solver leans on, see our Ingress NGINX tale.)