Kubernetes 1.37 Pod Certificates and Cluster Trust Bundles GA: Workload Identity Guide

Kubernetes 1.37 Pod Certificates and Cluster Trust Bundles GA: Workload Identity Guide

Kubernetes Pod Certificates and Cluster Trust Bundles in 1.37: A Workload Identity Guide

For a decade the identity your pod carried into the outside world was a bearer token. A projected service account JWT proves who you are to anyone who holds a copy, and every peer you authenticate to holds a copy by definition. Time, audience and object binding shrink the blast radius, but none of them removes the fundamental property: possession of the token is the identity. Kubernetes 1.37 changes the foundations. Kubernetes Pod Certificates, together with the ClusterTrustBundle object that distributes trust anchors, went generally available in the 1.37 release, and they put X.509 issuance for TLS and mutual TLS (mTLS) directly into core Kubernetes.

That matters now because the three things teams currently bolt on to get pod-level certificates, a service mesh, a SPIFFE/SPIRE deployment or cert-manager with a CSI driver, each carry their own control plane to run and upgrade. A first-party API will not replace them overnight, and there is an important catch we will spell out: core Kubernetes ships no production signer yet. But the contract between kubelet, the API server and a signer is now stable, and that is the part everyone else can build against.

This guide explains the mechanism at the API-field level, shows where it overlaps with SPIFFE and cert-manager, and ends with a migration plan you can run without betting production traffic on a young ecosystem.

What this covers: the GA status and its limits, the issuance and rotation flow, the PodCertificateRequest and ClusterTrustBundle APIs, working YAML, a comparison with SPIFFE/SPIRE and cert-manager csi-driver-spiffe, failure modes, and a staged adoption checklist.

Context and Background

Kubernetes has always had two credential stories. Inside the cluster, pods authenticate to the API server with a service account token. Outside the cluster, the same token is federated: the large cloud providers’ pod-to-cloud authentication schemes consume service account JWTs, and many other systems accept anything that speaks JSON Web Tokens. The release blog for 1.37, written by Taahir Ahmed, credits that ubiquity as the reason JWTs have done so much work, and then names their weakness plainly: they are bearer tokens.

The standard cure for bearer-token risk is proof of possession. Instead of sending the credential to the peer, the holder proves it controls a private key. Request-signing schemes such as AWS SigV4, JWT DPoP and RFC 9421 do this at the HTTP layer. The most widely deployed approach by far is X.509 in TLS, where the private key never leaves the workload and the peer verifies a certificate chain against a trust anchor it already holds. That split, a key generated inside the workload and a certificate signed by an authority, is exactly what Pod Certificates automate.

Teams that wanted this before 1.37 had four options, all out-of-tree. A service mesh such as Istio or Linkerd issues short-lived workload certificates through a sidecar or node proxy and terminates mTLS for you; we compared the sidecar and sidecarless trade-offs in our Istio ambient versus Linkerd analysis. SPIRE, the reference implementation of the SPIFFE specification, attests nodes and workloads and serves identities over a local Workload API, as described in our SPIFFE and SPIRE architecture walkthrough. cert-manager’s csi-driver and csi-driver-spiffe mount per-pod certificates through a Container Storage Interface volume. And teams with a regulated PKI wrote bespoke controllers around the older CertificateSigningRequest API, which was designed for humans and nodes, not for thousands of short-lived pod credentials.

The Kubernetes documentation records the graduation history. The ClusterTrustBundle API first appeared in v1.27, its projected volume source in v1.29, and PodCertificateRequest together with the podCertificate projected volume source in v1.34. All are marked stable since v1.37 and enabled by default, so there is no feature gate to flip. The upstream announcement is on the Kubernetes blog, and the authoritative field reference lives in the projected volumes documentation and the certificate signing requests page.

One premise deserves correction before we go deeper. “GA” here means the API contract and the kubelet machinery are stable. It does not mean Kubernetes will hand your pods a certificate out of the box. The blog states that the project does not yet ship any Pod Certificate signers in core, and that to try the feature you install a third-party signer. Hold that thought, because it shapes every recommendation below.

How Kubernetes Pod Certificates Work

In short: your pod spec declares a podCertificate projected volume source naming a signer. Kubelet generates a private key on the node, files a PodCertificateRequest addressed to that signer, waits for the signer’s controller to fill in a certificate chain, writes key and chain into the container filesystem, and repeats the process before expiry. The application only reads files.

That paragraph hides five distinct responsibilities, and the design only makes sense once you see who owns each one. The figure below shows the three parties and the order of operations.

Kubernetes Pod Certificates issuance flow between kubelet, kube-apiserver and a signer controller

Figure 1: Kubernetes Pod Certificates issuance flow, from pod spec to rotated credential bundle.

Read the diagram left to right. The pod spec is declarative; it states a signer name, a key type and a maximum lifetime, nothing more. Kubelet is the only component that ever touches key material, which is the property that makes this a proof-of-possession scheme rather than a certificate-shaped bearer token. The API server enforces who may ask for what. The signer controller decides whether to issue, and it is the only place where policy about identity content lives.

The podCertificate projected volume source

The user-facing surface is small. Each podCertificate projection supports these fields, per the upstream documentation:

  • signerName names the signer you want. Signers have their own access rules and may refuse your pod.
  • keyType selects the private key algorithm: ED25519, ECDSAP256, ECDSAP384, ECDSAP521, RSA3072 or RSA4096.
  • maxExpirationSeconds is the longest lifetime you will accept. It defaults to 86,400 (24 hours), must be at least 3,600 and at most 7,862,400, which is 91 days. A signer may issue a shorter certificate than you ask for.
  • credentialBundlePath writes one PEM file whose first block is a PKCS#8 PRIVATE KEY and whose remaining blocks are the certificate chain, leaf first.
  • keyPath and certificateChainPath write the key and chain to separate files.
  • userAnnotations passes domain-prefixed key and value pairs to the signer.

A complete pod using it looks like this. The signer name is the placeholder used in the Kubernetes documentation, not a real signer.

apiVersion: v1
kind: Pod
metadata:
  namespace: payments
  name: ledger-api
spec:
  serviceAccountName: ledger-api
  containers:
  - name: main
    image: registry.example.com/ledger-api:1.8.2
    volumeMounts:
    - name: x509
      mountPath: /var/run/x509
      readOnly: true
    - name: trust
      mountPath: /var/run/trust
      readOnly: true
  volumes:
  - name: x509
    projected:
      defaultMode: 0640
      sources:
      - podCertificate:
          keyType: ECDSAP256
          signerName: coolcert.example.com/foo
          maxExpirationSeconds: 14400
          credentialBundlePath: credentialbundle.pem
  - name: trust
    projected:
      sources:
      - clusterTrustBundle:
          signerName: coolcert.example.com/foo
          labelSelector:
            matchLabels:
              env: prod
          path: roots.pem

Two choices in that manifest are deliberate. The credential bundle goes into a single file because the documentation recommends it for most applications: kubelet writes projected files using a symlink-based atomic strategy, so a single open sees either the old or the new content, whereas reading key and chain from two files can catch a rotation between reads and load a mismatched pair. And maxExpirationSeconds is set to four hours instead of the 24-hour default, which tightens exposure at the price of more frequent refresh traffic.

The PodCertificateRequest object

Kubelet never exposes the request to the workload. It creates a PodCertificateRequest, an API object the documentation describes as similar to a CertificateSigningRequest but with a simpler format enabled by the narrower use case. Its spec carries the signer name, the pod name and UID, the service account name and UID, the node name and UID, maxExpirationSeconds, a stubPKCS10Request and unverifiedUserAnnotations.

The stub PKCS#10 request is minimal on purpose. It carries the public key, and the API server checks the request signature so that the signer does not have to. Kubelet’s requests include no other attributes. Everything the signer should believe about identity comes from the other spec fields, which the API server has already validated against reality. The pod, service account and node identities in the spec are not self-asserted claims; the NodeRestriction admission plugin, when enabled, ensures a node can only create requests that correspond to a pod currently running on that node.

That is the structural difference from the classic CSR flow. With a CSR, anyone with create permission can put any subject in the request and then depends on an approver to catch lies. With a PodCertificateRequest, the facts that matter are bound by the API server to a scheduled pod on a specific node before a signer sees them.

Unlike CSRs, there is also no separate approval phase. Once the request exists, the signer’s controller directly decides to issue, deny or mark it failed. Those outcomes are recorded as mutually exclusive conditions named Issued, Denied and Failed, each with status True, and once one is set the status becomes immutable. On issuance the controller writes the chain to status.certificateChain, denormalizes the validity window into status.notBefore and status.notAfter for debugging, and sets status.beginRefreshAt to tell kubelet when to start renewing.

Authorization on the signer side

A signer is just a controller with permissions. To act on requests it needs update on podcertificaterequests/status and the custom sign verb on the signers resource in the certificates.k8s.io group, scoped by resource name to <signerNameDomain>/<signerNamePath> or <signerNameDomain>/*. This reuses the namespacing convention of CSR signers. A cluster administrator can therefore delegate one signer name to one team’s controller by RBAC alone, and a controller for payments.example.com/* cannot answer requests for another domain.

The documentation also notes a hygiene rule: a kube-controller-manager controller deletes PodCertificateRequests older than 15 minutes, and all issuance flows are expected to complete within that window. Treat this as a hard design constraint for any signer that waits on an external CA or a human.

Cluster Trust Bundles: The Other Half of mTLS

A certificate is only half of a TLS credential. The other half is knowing which authorities to trust, and that was the unglamorous gap in Kubernetes for years. Teams distributed CA bundles through ConfigMaps copied into every namespace, baked them into images, or mounted them from Secrets, and then discovered during a CA rotation that nothing guaranteed every pod saw the new bundle at the same time. ClusterTrustBundle is the first-class object that fixes the distribution problem.

Cluster Trust Bundles signer-linked and signer-unlinked distribution to pods through projected volumes

Figure 2: Cluster Trust Bundles feed trust anchors from signer controllers and administrators into pods through the clusterTrustBundle projected source.

A ClusterTrustBundle is cluster-scoped and holds a trustBundle field of one or more DER-serialized X.509 certificates, each wrapped in a PEM CERTIFICATE block. Validation is strict: every certificate must parse, and the exotic PEM features, such as data between blocks or headers inside them, are rejected or ignorable by consumers. The documentation explicitly allows consumers to reorder the certificates with their own stable ordering, and kubelet takes that licence, so applications must never depend on the position of a root in the file.

Signer-linked versus signer-unlinked

The object has two modes, and the distinction is where the security model lives.

A signer-linked bundle sets spec.signerName. These are meant to be maintained by the controller for that signer, so creating or updating one requires the custom attest verb on the signers resource in certificates.k8s.io, again scoped to <signerNameDomain>/<signerNamePath> or a wildcard. The object name must carry a prefix derived from the signer name, with slashes replaced by colons and a final colon appended. The signer example.com/mysigner therefore owns names beginning example.com:mysigner:. This naming rule is an authorization mechanism in disguise: nobody else can squat on a name inside a signer’s namespace.

A signer-unlinked bundle leaves spec.signerName empty. These are for cluster configuration, each one an independent object rather than a member of a signer’s group, and they have no attest requirement; you control them with ordinary RBAC. Their names must not contain a colon, which keeps the two namespaces disjoint.

World-readable by design

The documentation states that ClusterTrustBundle objects should be considered world-readable within the cluster. Under RBAC, all service accounts have a default grant to get, list and watch them. If you run a custom authorization mechanism and enable the API, you need an equivalent public-read rule or the projected volumes will fail. Trust anchors are public information, so this is the correct default, but it does mean you must never put anything other than public certificates in these objects. Validation helps, because the field accepts only certificates, but an operator pasting the wrong PEM file is still a plausible incident.

The clusterTrustBundle projected source

Pods consume bundles through the clusterTrustBundle projected source. You select either a single object by name, or a set by signerName with an optional labelSelector. With a signer name and no label selector, every bundle for that signer is selected. Kubelet merges the selected objects, deduplicates certificates, normalizes the PEM representation by discarding comments and headers, reorders, and writes the result to the named path, keeping the file updated as the underlying objects change.

By default the pod will not start if a named bundle is missing or a signer and selector match nothing. Setting optional: true lets the pod start with an empty file instead. Use that sparingly. An empty trust file in a client that treats “no roots” as “use system roots” silently widens trust, while a client that fails closed will fail every handshake. Neither surprise is welcome at 3 a.m.

The selector-plus-signer pairing is what makes graceful CA rotation tractable. A signer controller can publish the next CA’s certificate as a second bundle object labelled rotation: next well before it starts issuing from it. Workloads selecting the whole signer receive both roots, validate chains from either, and the old bundle is retired only after every certificate issued under it has expired. With a 24-hour maximum lifetime, “every certificate” is a bounded, boring wait.

Rotation, Lifetimes, and Refresh in Practice

Issuance is the visible part of a certificate system. Rotation is where it lives or dies. This section works through the timing and the application-side obligations that the GA announcement says plainly belong to you.

Lifetime ceilings

The numbers are crisp. The upstream blog says any signers eventually shipped in core Kubernetes will issue certificates with a maximum lifetime of 24 hours, and that the maximum lifetime allowed for other signers is 91 days. The projected source enforces a floor of one hour for maxExpirationSeconds. So the workload author sets a ceiling, the signer may undercut it, and the platform clamps both between one hour and 91 days.

A short lifetime replaces revocation. There is no Certificate Revocation List or Online Certificate Status Protocol in this design; if a pod is compromised you delete it, and the credential expires within hours on its own. That is a legitimate security strategy, and it is the same one SPIFFE implementations and service meshes have used for years, but it moves your risk onto the issuance path. If the signer is down for longer than the remaining lifetime, workloads begin failing handshakes. Lifetime is therefore a trade between the exposure window after compromise and the outage tolerance of your signer.

Who decides when to refresh

The signer sets status.beginRefreshAt, and kubelet starts the issuance process again when that time passes. A sensible signer picks a point well before expiry, commonly around two-thirds to four-fifths of the lifetime, so that a handful of failed attempts still leaves headroom. The upstream documents do not prescribe a fraction, so treat any specific percentage as signer policy rather than platform behavior. Because refresh creates a new PodCertificateRequest each time, signer load scales with pod count divided by refresh interval.

Illustrative arithmetic, not a measurement: a cluster with 5,000 pods, each holding one certificate with a 24-hour lifetime refreshed at roughly 70 percent of lifetime, produces about 5,000 requests per day per refresh cycle, or just over three per minute on average. Shorten the lifetime to one hour and the same cluster produces roughly 5,000 requests per 42 minutes, close to 120 per minute. That is trivial for a controller doing local signing and meaningful for one that calls out to a rate-limited external CA, which is why a signer’s backing CA belongs in capacity planning, not an afterthought.

What the application must do

Kubelet writes new files; it does not signal your process. The documentation states that applications must reload promptly using inotify or polling. For trust bundles the same obligation applies. This is the sharpest edge in the whole feature, because many frameworks load certificates once at startup. Here is a minimal Go pattern that reloads a combined credential bundle on each TLS handshake with a cheap modification-time check:

package main

import (
    "crypto/tls"
    "os"
    "sync"
    "time"
)

type reloader struct {
    path  string
    mu    sync.RWMutex
    cert  *tls.Certificate
    mtime time.Time
}

func (r *reloader) get(*tls.ClientHelloInfo) (*tls.Certificate, error) {
    fi, err := os.Stat(r.path)
    if err != nil {
        return nil, err
    }
    r.mu.RLock()
    fresh := r.cert != nil && !fi.ModTime().After(r.mtime)
    c := r.cert
    r.mu.RUnlock()
    if fresh {
        return c, nil
    }
    // Bundle holds PRIVATE KEY first, then CERTIFICATE blocks.
    // tls.LoadX509KeyPair accepts the same file for both arguments.
    nc, err := tls.LoadX509KeyPair(r.path, r.path)
    if err != nil {
        return nil, err
    }
    r.mu.Lock()
    r.cert, r.mtime = &nc, fi.ModTime()
    r.mu.Unlock()
    return &nc, nil
}

func serverConfig(path string) *tls.Config {
    r := &reloader{path: path}
    return &tls.Config{GetCertificate: r.get, MinVersion: tls.VersionTLS13}
}

The symlink-swap write strategy means the modification time of the resolved file changes atomically, so a stat per handshake is safe and costs microseconds. For production, prefer a file watcher with a polling fallback, because some container filesystems and some network-backed volumes do not deliver inotify events reliably.

Pod certificate rotation timeline showing refresh before expiry and trust bundle overlap

Figure 3: Rotation sequence for a pod certificate, with the signer deciding the refresh moment and the application reloading files.

Pod Certificates versus SPIFFE/SPIRE versus cert-manager

It is tempting to frame this as a replacement story. It is better framed as a layering story, and we cover the head-to-head in depth in our comparison of Pod Certificates, SPIFFE/SPIRE and cert-manager. Here is the short, mechanism-level version.

What Pod Certificates standardize

Pod Certificates standardize the node-local and API-level parts: key generation in kubelet, the request object, the authorization verbs, the refresh hint, the on-disk format and the trust distribution API. They deliberately do not standardize what goes inside the certificate. The release blog says that the X.509 ecosystem is more varied than the JWT ecosystem, with different extensions for different purposes, so the machinery is common and the issuer is pluggable, allowing several kinds of certificate to be issued in one cluster at once. The blog’s author says he expects Kubernetes to eventually offer at least two built-in providers, one issuing server TLS certificates for the DNS names of Kubernetes Services and one issuing SPIFFE client certificates in the role service account JWTs fill today. That is a stated expectation, not a shipped feature.

What SPIFFE and SPIRE add

SPIFFE defines the identity format: a URI such as spiffe://trust-domain/ns/payments/sa/ledger-api carried as a URI subject alternative name in an X509-SVID, plus the Workload API and federation of trust bundles between trust domains. SPIRE is an implementation that adds node and workload attestation with selectors beyond Kubernetes, so a VM, a bare-metal process and a pod can share one trust domain. The SPIRE documentation describes a server acting as signing authority with a registry of registration entries and agents on every node exposing the Workload API. None of that is replicated by the Kubernetes API. If you need a single identity namespace across clusters, clouds and non-Kubernetes hosts, SPIRE still earns its operational cost.

The blog nevertheless points at convergence. The sample signer it offers, Tinycert, includes one signer that issues SPIFFE-compatible certificates naming the pod’s namespace and service account, and the post asks readers to review the draft SPIFFE Filesystem Delivery standard, which defines how SVIDs and bundles are laid out in a directory. A SPIRE deployment could become a Pod Certificates signer; the API supports that shape, though no such integration is documented in the sources we reviewed.

What cert-manager csi-driver-spiffe does today

cert-manager’s csi-driver-spiffe is the closest existing analogue. Its documentation describes a DaemonSet CSI driver that generates a private key locally in a tmpfs, uses the pod’s own token via CSI token requests to derive the SPIFFE ID, and creates a cert-manager CertificateRequest signed by a configured issuer, with a separate approver enforcing a single URI SAN, a trust domain match and a default one-hour duration. The architecture is strikingly similar to kubelet plus PodCertificateRequest. The differences are where the logic lives: in a third-party DaemonSet and CRDs versus in kubelet and a core API, and in cert-manager’s issuer ecosystem, which supports many certificate authorities through existing integrations.

Comparison architecture of Pod Certificates, SPIRE and cert-manager csi-driver-spiffe

Figure 4: Where each approach generates keys, where policy lives and how credentials reach the pod.

Decision matrix

The matrix below reflects the documented behavior of each approach as of October 2026. Cells say “not documented” where the primary sources reviewed do not state a fact.

Dimension Pod Certificates SPIRE cert-manager csi-driver-spiffe
Key generation Kubelet, on the node Agent or workload, per SPIRE configuration CSI driver, local tmpfs
Request object PodCertificateRequest, core API SPIRE registration and attestation cert-manager CertificateRequest
Identity content Defined by the signer you install SPIFFE ID in an X509-SVID SPIFFE ID as a single URI SAN
Works outside Kubernetes No Yes, via other attestors No
Ships a production signer No, third-party only Yes Yes, via cert-manager issuers
Extra control plane to run Only the signer SPIRE Server and agents Driver, approver, cert-manager
Delivery mechanism Projected volume Workload API socket CSI volume
Default lifetime 24 hours ceiling unless set Configurable One hour enforced by the approver

Read the matrix with one caveat in mind. The Pod Certificates column describes a platform contract, while the other two columns describe finished products. The honest comparison is “a stable foundation with no first-party signer” against “complete systems with their own operational burden”.

A Walk-through: Building a Signer and Wiring a Workload

The fastest way to understand the design is to follow a request through a concrete setup. Because core ships no signer, the blog’s author provides Tinycert, a toy signer that he describes as not a full production solution but a base for experimenting, with two signers named ahmedtd.github.io/tinycert-service and ahmedtd.github.io/tinycert-spiffe. We use that shape to illustrate the steps; the RBAC and manifests below are our own illustration of the documented verbs, not Tinycert’s published manifests.

Step one: authorize the signer controller

The signer’s controller needs the sign and status-update permissions described earlier, plus the attest verb if it also publishes signer-linked trust bundles. For a hypothetical signer signers.example.com/workload:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: workload-signer
rules:
- apiGroups: ["certificates.k8s.io"]
  resources: ["podcertificaterequests"]
  verbs: ["get", "list", "watch"]
- apiGroups: ["certificates.k8s.io"]
  resources: ["podcertificaterequests/status"]
  verbs: ["update"]
- apiGroups: ["certificates.k8s.io"]
  resources: ["signers"]
  resourceNames: ["signers.example.com/workload"]
  verbs: ["sign", "attest"]
- apiGroups: ["certificates.k8s.io"]
  resources: ["clustertrustbundles"]
  verbs: ["get", "list", "watch", "create", "update"]
- apiGroups: [""]
  resources: ["pods", "serviceaccounts"]
  verbs: ["get"]

The last rule is optional and policy-dependent. The documentation notes that a signing controller is free to consider information beyond the request, such as loading the pod to read annotations or performing a SubjectAccessReview on the service account. Every extra read widens what a compromised signer can see, so grant only what your issuance policy actually consults.

Step two: decide what the certificate says

This is the step that makes or breaks the design. A signer for service identity might issue a URI SAN of the form spiffe://prod.example.com/ns/<namespace>/sa/<serviceaccount> using the namespace and service account names from the request spec. A signer for server TLS might instead issue DNS SANs for each Kubernetes Service that selects the pod; the blog says Tinycert’s service signer does exactly that. Both derive identity from API-server-validated fields rather than from anything the pod controls.

The one pod-controlled input is unverifiedUserAnnotations, and its name is a warning. The documentation says signers must not inherently trust it and should deny requests with keys they do not recognize. A tempting but dangerous pattern is letting an annotation choose a SAN or a lifetime. Resist it. If a team needs a custom DNS name, verify it against an authoritative source, such as an allowlist keyed by namespace, before issuing.

Step three: publish the roots

Once the signer’s CA exists, its controller creates a signer-linked ClusterTrustBundle named with the colon-prefixed convention, labelled so workloads can select the active generation. Anything a pod will verify against the signer’s chain must be delivered this way, or the pod has an identity nobody can check.

Step four: mount both volumes and handshake

The pod manifest shown earlier mounts both a podCertificate source and a clusterTrustBundle source. A server then configures its TLS stack to present the leaf and require client certificates verified against the bundle; a client does the mirror image. For SPIFFE-style authentication, verification must go beyond chain validation. Chain validity says the certificate came from your CA; it says nothing about whether the caller is the service you intended. The server must also check the peer’s URI SAN against an allowlist, and a library such as the Tinycert spiffefsd helper, which the blog says loads SVIDs and bundles from a SPIFFE Filesystem Delivery folder and configures Go TLS, exists for that reason.

Here is the authorization check in miniature, using Go’s verification callback:

func allowPeer(allowed map[string]bool) func([][]byte, [][]*x509.Certificate) error {
    return func(_ [][]byte, chains [][]*x509.Certificate) error {
        leaf := chains[0][0]
        for _, u := range leaf.URIs {
            if allowed[u.String()] {
                return nil
            }
        }
        return errors.New("peer identity not in allowlist")
    }
}

Pair it with ClientAuth: tls.RequireAndVerifyClientCert and a ClientCAs pool loaded from the projected roots.pem. Without the allowlist, every pod in the trust domain can call every service, which is mTLS in name only.

Verifying it works

Three observations confirm a healthy flow. kubectl get podcertificaterequests should show short-lived objects reaching the Issued condition, and they disappear within about 15 minutes. The status.notBefore and status.notAfter fields give you the validity window without parsing the PEM. And openssl x509 -in on the leaf block of the bundle shows the SAN, issuer and expiry. If requests sit with no condition, check the signer’s RBAC first; if they reach Denied, read status.conditions[].message, which the API reserves for human-readable explanation.

Trade-offs, Gotchas, and What Goes Wrong

The feature is well designed and still young. These are the failure modes worth planning for, drawn from the documented semantics and from general certificate-operations experience.

The signer is a new tier-zero component. It can mint identities for any pod under its signer name. Compromise of the signer’s controller or of its CA key is equivalent to compromise of every workload identity it issues. Run it with minimal RBAC, keep CA keys in an HSM or a cloud KMS where the signer supports it, and treat its namespace like the control plane.

Applications that never reload. The most common production failure will be a service that loads its certificate once and then serves an expired one a day later. Make reload a launch-readiness test: set maxExpirationSeconds to 3,600 in staging and watch whether handshakes survive several rotations.

Mismatched key and certificate. Using separate keyPath and certificateChainPath files invites a race at rotation time. Prefer credentialBundlePath.

subPath mounts do not update. The projected volumes documentation notes that a container using a projected source as a subPath mount receives no updates. A subPath mount of the certificate file is a certificate that never rotates.

Signer stalls and the 15-minute window. A signer that blocks on a slow upstream CA can exceed the cleanup window and have its request deleted. Design signers to issue locally, or to fail fast and let kubelet retry.

Empty or missing trust. The default behavior of refusing to start a pod without its bundle is a feature. Overriding it with optional: true trades a loud failure for a quiet one.

Static pods and bootstrap ordering. Kubelet requests the certificate on behalf of a scheduled pod, so the signer must be running before workloads that need its certificates. The signer itself cannot depend on a Pod Certificate from its own signer. Plan a bootstrap path, such as a statically provisioned certificate for control-plane-adjacent components, and do not discover the circularity during a cold-start outage.

Identity is not authorization. A certificate proves which service account a pod runs as. It does not say what that pod may do. You still need policy at the receiving service, whether application-level checks, a mesh authorization policy or a network policy, as we discuss in our zero trust network architecture implementation guide.

Ecosystem maturity. At the time of writing, the project’s own announcement points readers to a toy signer. Mature signers from the vendors and projects named above may appear, but we have not seen primary-source confirmation of any specific production release, so verify before committing a roadmap to one.

Practical Recommendations

Start by deciding what problem you are solving, because the right answer differs sharply by situation. If you run SPIRE today and it works, keep it; the benefit of switching is a smaller footprint, not new capability, and the missing first-party signer makes the migration premature. If you run cert-manager with csi-driver-spiffe, you already have a working per-pod SPIFFE issuance; the interesting experiment is a cert-manager-backed signer behind the Pod Certificates API, if and when one exists. If you have neither and your goal is mTLS between services, a mesh remains the lowest-effort path for most teams, because it also removes the application-side reload burden.

Pod Certificates earn their place in three scenarios: platform teams that want to build an internal signer tied to an existing PKI, environments that cannot tolerate a CSI driver or sidecar dependency, and organizations preparing for the day core ships SPIFFE and service-DNS signers. In an industrial or edge context, where nodes are constrained and a lean footprint matters, the absence of an extra agent is attractive; our zero trust architecture guide for industrial OT and IoT covers the surrounding threat model.

A staged plan that limits risk:

  • Confirm every cluster is on 1.37 or later, and that kubelet, API server and signer versions agree.
  • Inventory what your pods use service account JWTs for, and keep those uses; Pod Certificates complement them, they do not remove them.
  • Stand up one non-production signer, using Tinycert or your own controller, with a 1-hour lifetime to force rotation bugs early.
  • Publish signer-linked ClusterTrustBundles and mount them with optional: false.
  • Add file reload to two pilot services and test a forced rotation and a signer outage.
  • Enforce peer identity allowlists, not just chain validation.
  • Alert on PodCertificateRequests stuck without a condition and on certificates within 20 percent of expiry.
  • Revisit core signer availability each release before retiring any existing mechanism.

Frequently Asked Questions

Are Pod Certificates generally available in Kubernetes 1.37?

Yes. The Kubernetes documentation marks the PodCertificateRequest API, the podCertificate projected volume source, the ClusterTrustBundle API and the clusterTrustBundle projected source as stable since v1.37 and enabled by default. They first appeared as early releases in v1.34, v1.34, v1.27 and v1.29 respectively. Stable means the API and kubelet behavior are settled; it does not mean core Kubernetes issues certificates by itself, because no built-in signer ships yet.

Do Pod Certificates replace service account tokens?

No. Service account JWTs remain the mechanism for authenticating to the API server and for federating to cloud providers, many of which consume them directly. The release blog presents Pod Certificates as a proof-of-possession alternative for workload-to-workload TLS and mTLS, and expects a future built-in SPIFFE signer to fill a similar role to JWTs. Today they run side by side, and most clusters should keep both.

Do I still need SPIRE or cert-manager?

It depends on what you need. Pod Certificates provide the issuance plumbing but no signer, so you need something to sign, whether your own controller or a third-party project. SPIRE still offers cross-platform attestation and federation that Kubernetes does not. cert-manager still provides a mature issuer ecosystem. Think of Pod Certificates as a standard hook those systems could plug into, not as a feature that removes them.

How long are pod certificates valid and how do they rotate?

The workload sets maxExpirationSeconds, defaulting to 24 hours, with a minimum of one hour and a maximum of 91 days. Signers may issue shorter lifetimes, and the blog says any built-in signers will cap at 24 hours. The signer sets beginRefreshAt, and kubelet re-runs issuance then, rewriting the files atomically. Your application must reload them with inotify or polling.

What is a ClusterTrustBundle and who can write to it?

It is a cluster-scoped object that holds PEM-encoded X.509 root certificates. Signer-linked bundles carry a signer name, must follow a colon-delimited naming prefix, and require the attest verb for that signer. Signer-unlinked bundles have no signer and are governed by plain RBAC. Under RBAC all service accounts can read them by default, so only public certificates belong there.

Can a compromised node steal other pods’ certificates?

The design limits this. Kubelet creates requests, and the NodeRestriction admission plugin, when enabled, only allows a node to create PodCertificateRequests for pods actually running on it. The private key is generated on the node for that pod. A compromised node can still obtain credentials for pods scheduled there, so node compromise remains serious, but it does not extend to pods on other nodes through this API.

Further Reading

By Riju — about

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *