Skip to main content

Get Service Mesh Certified with Buoyant.

Enroll now!
close
Blog home

A 2026 Guide to Linkerd Trust Anchor Rotation with cert-manager

You'll never get a 2am call for an expired leaf certificate, because every Linkerd proxy gets a workload certificate that is valid for 24 hours and rotates it automatically before it expires. You can run for years and never think about them.

The trust anchor is a different story, though. It’s the root every certificate chains back to. Every proxy is configured to trust it, and nothing rotates it for you. Being long-lived, it slips off the radar until the day it expires, and when it does, it fails quietly: existing traffic keeps flowing, so nothing looks broken, but the next pod to restart can’t rejoin the mesh.

To avoid all that, you need to rotate it ahead of time, and you need to do it without dropping a request. It's all a matter of the order of operations. Get it wrong and you'll cause the outage you were avoiding. In this blog post, I'll cover the rotation on Linkerd with cert-manager and trust-manager, the failure modes to plan for, and everything you need to know to avoid that outage.

Environment prerequisites for trust anchor rotation

The Cilium flag that matters for any mesh is socketLB.hostNamespaceOnly: true. Without it, Cilium rewrites the ClusterIP at the socket level before traffic reaches the proxy, and east-west mTLS silently stops.

Visit Linkerd's getting started and Helm installation docs for more details on setup. Linkerd runs in externalCA mode. It reads its roots from cert-manager and trust-manager instead of generating its own.

identity:
  externalCA: true             # trust roots come from a trust-manager ConfigMap
  issuer:
    scheme: kubernetes.io/tls  # issuing CA read from a cert-manager Secret

cert-manager creates and renews the chain. A self-signed-type ClusterIssuer creates the root-blue trust anchor, a 1-year ECDSA P-256 root. The anchor signs the identity issuer, a 90-day intermediate that cert-manager renews on its own. Both lifetime values are sensible choices, not constants Linkerd requires.

# identity issuer (intermediate CA), signed by the anchor
spec:
  commonName: identity.linkerd.cluster.local
  duration: 2160h    # 90 days
  renewBefore: 720h  # 30 days
  issuerRef:
    name: root-blue  # repointed to root-green during rotation

trust-manager handles distribution. A Bundle is the list of trusted anchors. It writes them into the linkerd-identity-trust-roots ConfigMap that every proxy reads, starting with root-blue as the source. Most of the rotation is editing that list.

Traffic comes from emojivoto, Linkerd’s own demo app, and its built-in vote-bot, which sends a constant stream of meshed requests. When trying this on your own, keep in mind that emojivoto deliberately returns an error for votes on one specific emoji, so its overall success rate sits below 100%, whether it rotated or not.

One root to rule them all: Understanding the Linkerd identity hierarchy

Linkerd’s identity is a 3-tier hierarchy: a trust anchor, an identity issuer, and the workload certificates it signs. Each has a different job, lifetime, and storage, and the rotation touches them in a set order.

  • Trust anchor is the self-signed root, typically valid for a long time. Its only job is signing the issuer. Proxies validate peers with its public certificate, so the private key never has to reach them. cert-manager keeps that key in the cluster here for convenience. Don't do that in production, though. 
  • Identity issuer is the intermediate that signs every workload certificate. Its cert and key both live in the cluster because the identity service signs with them constantly. Keeping that key in the cluster is why the issuer is short-lived and renewed automatically, so a leaked Secret expires within months rather than years.
  • Workload certificate is the 24-hour leaf. The proxy generates the key at startup and keeps it in memory. It never touches the disk. The cert carries the workload’s service account as a SPIFFE ID.
Linkerd’s signing chain: the self-signed trust anchor signs the identity issuer, which signs the workload certificate.
Linkerd’s signing chain: the self-signed trust anchor signs the identity issuer, which signs the workload certificate.

With the exception of the in-memory workload certificate, each of these is verifiable against the cluster. The trust bundle is a ConfigMap, and it holds only the public certificates.

kubectl -n linkerd get cm linkerd-identity-trust-roots -o jsonpath='{.data}' | jq -c keys

["ca-bundle.crt"]

The issuer is a kubernetes.io/tls Secret. Cert and key together.

kubectl -n linkerd get secret linkerd-identity-issuer -o jsonpath='{.data}' | jq -c keys

["ca.crt","tls.crt","tls.key"]

The leaf is in neither. It only exists in proxy memory.

This is where each part lives. The trust-roots ConfigMap holds only the anchor’s public certificate. The identity-issuer Secret holds that same anchor public certificate (ca.crt) alongside the issuer’s certificate and private key. The proxy keeps its own workload certificate and key in memory, plus the anchor’s public certificate as its trust roots, set in the Pod spec as an env var at injection.
This is where each part lives. The trust-roots ConfigMap holds only the anchor’s public certificate. The identity-issuer Secret holds that same anchor public certificate (ca.crt) alongside the issuer’s certificate and private key. The proxy keeps its own workload certificate and key in memory, plus the anchor’s public certificate as its trust roots, set in the Pod spec as an env var at injection.

To read a certificate, decode it and inspect it with step.

kubectl -n linkerd get secret linkerd-identity-issuer \
  -o jsonpath='{.data.tls\.crt}' | base64 -d | step certificate inspect --short
  
X.509v3 Intermediate CA Certificate (ECDSA P-256) [Serial: 4666...5740]
  Subject:     identity.linkerd.cluster.local
  Issuer:      root.linkerd.cluster.local
  Valid from:  2026-06-29T10:07:07Z
          to:  2026-09-27T10:07:07Z

Two specific fields matter for the rotation. The Subject is who the certificate is for, and the Issuer is who signed it. When they’re equal, the certificate is self-signed, which is how you spot the anchor. Here they differ, so root signed identity. The first line also marks it a CA certificate using ECDSA P-256, the only key type Linkerd accepts. Repointing that Subject-to-Issuer link is the rotation.

Why trust anchor expiry triggers service mesh outages

An expired anchor doesn’t take the mesh down on its own. In the demo we let an anchor expire while the issuer and the workload certificates under it stayed valid. Existing traffic never noticed. vote-bot kept sending requests and the meshed pods kept serving mTLS after the anchor’s notAfter had passed.

What breaks is a proxy cold start. When we restarted a meshed deployment, the new pod hung in Init and its proxy never passed the startup probe. A brand-new pod did the same. The injector added the sidecar, but the proxy returned 503 and never became ready. Even restarting the identity controller failed.

The difference is consistent. A proxy that is already running keeps validating peers and serving traffic, because it already has its identity from before the anchor expired. A cold-starting proxy has to get a fresh certificate before it can become ready, and with the anchor expired, it never does.

So nothing alarms while the running pods keep running, and nothing flags it but linkerd check, which goes red on a single line: × trust anchors are within their validity period

Restarts happen all the time. A deploy, a drained node, an autoscaler, an OOM kill, you name it. After the anchor expires, each one leaves a pod that can’t rejoin. Rotate before expiry to avoid all of it.

Trust anchor rotation example

A few things should be in place before you start. linkerd check should be passing, the live anchor identified, and the tooling ready (step, kubectl, and the cert-manager and trust-manager installation from the setup). Keep real traffic running so you’ll notice if anything breaks.

The core idea of certificate rotation is for the certs to overlap. You can’t swap one anchor for another in a single step. Every proxy still holding the old anchor would reject certificates signed by the new one. So you widen the set of trusted anchors, move everything onto the new anchor, then drop the old. For the demo, we’ll call the current anchor blue and its replacement green. The bundle goes blue, then blue and green, then green.

The trust bundle ConfigMap across a rotation: it starts holding only the blue anchor (current), then holds both blue and green after green is added and the pods restart (overlap), then holds only green after blue is dropped and the pods restart again (rotated).
The trust bundle ConfigMap across a rotation: it starts holding only the blue anchor (current), then holds both blue and green after green is added and the pods restart (overlap), then holds only green after blue is dropped and the pods restart again (rotated).

1. Create the green anchor. 

A second self-signed root is created next to blue. Nothing trusts it yet, so the mesh doesn’t notice.

kubectl apply -f - <<'EOF'
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: root-green
  namespace: cert-manager
spec:
  isCA: true
  commonName: root.linkerd.cluster.local
  secretName: root-green
  duration: 8760h           # 1 year
  privateKey:
    algorithm: ECDSA        # Linkerd requires ECDSA P-256
    size: 256
  issuerRef:
    name: selfsigned
    kind: ClusterIssuer
    group: cert-manager.io
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: root-green
spec:
  ca:
    secretName: root-green
EOF

2. Publish both anchors, then restart. 

Add green to the bundle so the ConfigMap carries both. Then rolling-restart every meshed pod. This is the step people skip, and it’s the one that matters. Proxies read the bundle once at startup and bake the trust roots in as an environment variable. Until they restart, they still trust blue only. Skip the restart and step 3 would take the mesh down.

kubectl patch bundle linkerd-identity-trust-roots --type=merge -p \
  '{"spec":{"sources":[{"secret":{"name":"root-blue","key":"tls.crt"}},
                       {"secret":{"name":"root-green","key":"tls.crt"}}]}}'

The restart command:

for ns in linkerd linkerd-viz emojivoto; do kubectl rollout restart deploy -n $ns; done

3. Swap the issuer onto green, then restart. 

Repoint the issuer so cert-manager re-signs it under green. New workload certificates chain to green, while the ones already issued still chain to blue. Every proxy trusts both, so nothing breaks. Restart so each workload picks up a fresh certificate now, rather than waiting out the 24-hour lifetime.

kubectl patch certificate linkerd-identity-issuer -n linkerd --type=merge -p \
  '{"spec":{"issuerRef":{"name":"root-green","kind":"ClusterIssuer","group":"cert-manager.io"}}}'
for ns in linkerd linkerd-viz emojivoto; do kubectl rollout restart deploy -n $ns; done

That patch does the real work. cert-manager sees the changed issuerRef and re-signs the intermediate under green:

kubectl -n linkerd describe certificate linkerd-identity-issuer | sed -n '/Events:/,$p'

Issuing    Issuing certificate as Secret was previously issued by "ClusterIssuer.cert-manager.io/root-blue"
Issuing    The certificate has been successfully issued

The identity controller reloads it live, no restart. Its restartCount stays 0, and the log keeps showing fresh workload certificates:

kubectl -n linkerd logs deploy/linkerd-identity -c identity

level=info msg="issued certificate for web.emojivoto.serviceaccount.identity.linkerd.cluster.local until ..."
level=info msg="issued certificate for emoji.emojivoto.serviceaccount.identity.linkerd.cluster.local until ..."

4. Drop blue, then restart. 

Everything’s on green now. Remove blue from the bundle and restart once more. You are now back to a single trusted anchor.

kubectl patch bundle linkerd-identity-trust-roots --type=merge -p \
  '{"spec":{"sources":[{"secret":{"name":"root-green","key":"tls.crt"}}]}}'
for ns in linkerd linkerd-viz emojivoto; do kubectl rollout restart deploy -n $ns; done

We ran the full sequence against live traffic. linkerd check was successful at every step. web and voting report success below 100% throughout, but that’s emojivoto erroring its own votes, not the certificates. emoji, which has no built-in errors, held at 100% across the rotation, and every emojivoto edge stayed mTLS-secured:

linkerd viz edges deployment -n emojivoto

SRC        DST      SRC_NS      DST_NS      SECURED
vote-bot   web      emojivoto   emojivoto   √
web        emoji    emojivoto   emojivoto   √
web        voting   emojivoto   emojivoto   √

Both anchors are trusted throughout the swap, and each rolling restart keeps a ready replica serving, so no request reaches a proxy that doesn’t yet trust its peer’s certificate.

Common pitfalls during trust anchor rotation

Forgetting it exists. Likely the main cause of expired certificates. Nobody rotates the anchor, it expires on a date no one wrote down, and you land in the cold-start hole above. To avoid this, set an alert on the anchor's expiry date.

Mismatched lifetimes. If the issuer or the leaves outlast the anchor’s remaining time, they outlive their own root. Keep each tier’s lifetime well inside the one above it. Set renewBefore with room to spare before anything lapses.

No audit trail. Without a record of each rotation, a problem weeks later is pure guesswork. Keep the bundle and issuer manifests in version control, and log every rotation.

Retiring blue too early. If you drop the old blue anchor before every proxy is on the new green anchor, you’ve lost the ability to roll back. While both are in the bundle, rolling back is just repointing the issuer. Keep blue in the bundle until linkerd check passes and every workload certificate chains to green.

One warning looks like a problem but isn’t. linkerd check flags an issuer with under 60 days of validity left. cert-manager renews the issuer automatically, so when that warning shows up, it’s informational, not a risk.

Automating the rotation process

Most of the chain runs itself. cert-manager renews the issuer on schedule, and the identity controller hot-reloads it with no restart. Workload certificates rotate every 24 hours and trust-manager keeps the ConfigMap in sync with whatever roots you list. You can automate the restarts too by annotating the meshed workloads for Stakater Reloader, which watches the trust-roots ConfigMap and fires the rolling restarts on every bundle change.

But no tool can decide when to rotate and when to retire the old anchor. That's something you need to decide, and the only tool to help you with that is a calendar reminder.

Automating rotation with the Linkerd trust-rotation operator

Yes, Buoyant, the creators of Linkerd just shipped a trust-rotation operator that drives the whole sequence as part of Buoyant Enterprise for Linkerd (BEL). You can give it a try for free and see for yourself.

Turning trust anchor rotation into a routine operation

Rotating the trust anchor can (and should) be boring. Set a reminder with enough buffer to plan the rotation, run the sequence in the right order and you'll have no disruption to live traffic. Or automate everything with BEL. Do that, and the rotation is the least interesting thing you’ll do that day.

Sources

FAQ

What happens when a Linkerd trust anchor expires?

Existing traffic keeps flowing, so the mesh looks fine. What breaks is cold starts: any pod that restarts or is newly scheduled can't get a workload certificate and won't become ready. linkerd check is the only tool that flags it. 

How do you rotate a Linkerd trust anchor without dropping traffic?

Add the new anchor to the trust bundle alongside the old one, then rolling-restart all meshed pods. Once every proxy trusts both, repoint the identity issuer to the new anchor and restart again. Remove the old anchor only after linkerd check passes and the switch is complete. 

Why do you need to restart pods after updating the Linkerd trust bundle?

Proxies read the trust bundle once at startup and bake the trust roots in as an env var. Until a pod restarts, it still trusts only the old anchor. Skip the restart and removing the old anchor will break mTLS for any pod that hasn't picked up the new bundle yet. 

How is Linkerd trust anchor rotation different from workload certificate rotation?

Workload certificates rotate automatically every 24 hours. The proxy handles it with no input needed. The trust anchor doesn't rotate on its own. If it expires, new pods can't join the mesh, even though existing traffic looks fine. 

Can Linkerd trust anchor rotation be automated?

cert-manager renews the identity issuer automatically, and the identity controller hot-reloads it. The decision of when to rotate the anchor and retire the old one stays yours. Buoyant Enterprise for Linkerd ships a trust-rotation operator that drives the full sequence.