Linkerd How-to Guides¶
Scope
Task recipes for running open-source Linkerd from edge releases (commands checked against the 2-edge docs for Linkerd 2.20, 2026-09). Distributions such as Buoyant Enterprise for Linkerd (BEL) have their own install and upgrade instructions. For defaults, ports and CRD versions, see the Reference. For background, see the Explanation.
Install the CLI¶
# Latest edge CLI (pin a version with LINKERD2_VERSION=edge-26.9.3)
curl --proto '=https' --tlsv1.2 -sSfL https://run.linkerd.io/install-edge | sh
export PATH=$HOME/.linkerd2/bin:$PATH
linkerd version --client
Check the Kubernetes compatibility table first. Linkerd 2.20 needs Kubernetes 1.31 or later.
Install the Gateway API CRDs¶
Since 2.19, Linkerd no longer installs the Gateway API types by default. Check what is already there, then install a version in Linkerd's supported range (1.2.1 to 1.5.1 for 2.20).
Gateway API version skew
Gateway API CRDs are cluster-scoped, so every controller in the cluster shares one version. Envoy Gateway v1.9 bundles Gateway API v1.6.1, outside Linkerd 2.20's documented range. If both run in one cluster, install the CRDs once, deliberately, and check each project's compatibility table before upgrading either. See the Envoy Gateway topic.
# Prints the bundle version, or NotFound if the Gateway API is absent
kubectl get crds/httproutes.gateway.networking.k8s.io \
-o "jsonpath={.metadata.annotations.gateway\.networking\.k8s\.io/bundle-version}"
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.5.1/standard-install.yaml
Warning
An incompatible Gateway API version on the cluster causes hard-to-debug Linkerd problems. When you upgrade from Gateway API 1.1.1 to 1.2.0 or later and use GRPCRoute, read the Gateway API 1.2.0 release notes first.
Install the Control Plane with the CLI¶
This is the quickest path, suitable for evaluation. The CLI generates a trust anchor that is valid for one year.
linkerd check --pre # validate the cluster
linkerd install --crds | kubectl apply -f - # Linkerd CRDs first
linkerd install | kubectl apply -f - # control plane
linkerd check # wait for healthy
For production, add --ha (see Enable High Availability) and bring your own trust anchor (next section).
Install with Helm and Your Own Trust Anchor¶
-
Generate a long-lived trust anchor and an issuer with the
stepCLI:# Root (trust anchor). Example: 10 years instead of the default 1 year. step certificate create root.linkerd.cluster.local ca.crt ca.key \ --profile root-ca --no-password --insecure --not-after=87600h # Issuer (intermediate CA), 1 year step certificate create identity.linkerd.cluster.local issuer.crt issuer.key \ --profile intermediate-ca --not-after 8760h --no-password --insecure \ --ca ca.crt --ca-key ca.key -
Install the charts from the edge Helm repo:
helm repo add linkerd-edge https://helm.linkerd.io/edge helm repo update helm install linkerd-crds linkerd-edge/linkerd-crds \ -n linkerd --create-namespace helm install linkerd-control-plane -n linkerd \ --set-file identityTrustAnchorsPEM=ca.crt \ --set-file identity.issuer.tls.crtPEM=issuer.crt \ --set-file identity.issuer.tls.keyPEM=issuer.key \ linkerd-edge/linkerd-control-planeAdd
--set cniEnabled=trueto both commands if you use the CNI plugin. For HA, add-f values-ha.yamlfrom the fetched chart (helm fetch --untar linkerd-edge/linkerd-control-plane).
Keep ca.key offline. Every cluster you plan to link for multicluster must use the same trust anchor.
Automate Issuer Rotation with cert-manager¶
This follows the official "automatically rotating control plane TLS credentials" guide. cert-manager issues the trust anchor and a short-lived issuer. trust-manager publishes the trust bundle ConfigMap that Linkerd reads.
kubectl create namespace linkerd
kubectl label namespace linkerd linkerd.io/is-control-plane=true
helm repo add jetstack https://charts.jetstack.io --force-update
helm install cert-manager jetstack/cert-manager -n cert-manager --create-namespace --set crds.enabled=true
helm install trust-manager jetstack/trust-manager -n cert-manager --set app.trust.namespace=cert-manager --wait
# 1. Self-signed root issuer and the trust anchor certificate (cert-manager namespace)
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: linkerd-trust-root-issuer
namespace: cert-manager
spec:
selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: linkerd-trust-anchor
namespace: cert-manager
spec:
issuerRef:
kind: Issuer
name: linkerd-trust-root-issuer
secretName: linkerd-trust-anchor
isCA: true
commonName: root.linkerd.cluster.local
duration: 8760h0m0s # 1 year
renewBefore: 7320h0m0s # renew ~2 months before expiry
privateKey:
rotationPolicy: Always
algorithm: ECDSA
---
# 2. ClusterIssuer backed by the trust anchor, and the short-lived Linkerd issuer
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: linkerd-identity-issuer
spec:
ca:
secretName: linkerd-trust-anchor
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: linkerd-identity-issuer
namespace: linkerd
spec:
issuerRef:
name: linkerd-identity-issuer
kind: ClusterIssuer
secretName: linkerd-identity-issuer
isCA: true
commonName: identity.linkerd.cluster.local
duration: 48h0m0s
renewBefore: 25h0m0s
privateKey:
rotationPolicy: Always
algorithm: ECDSA
---
# 3. trust-manager Bundle writes the linkerd-identity-trust-roots ConfigMap
apiVersion: trust.cert-manager.io/v1alpha1
kind: Bundle
metadata:
name: linkerd-identity-trust-roots
spec:
sources:
- secret:
name: "linkerd-trust-anchor"
key: "tls.crt"
- secret: # previous anchor, kept during a rotation
name: "linkerd-previous-anchor"
key: "tls.crt"
target:
configMap:
key: "ca-bundle.crt"
namespaceSelector:
matchLabels:
linkerd.io/is-control-plane: "true"
The official guide creates linkerd-previous-anchor as a copy of the current anchor before it applies the Bundle. Then install Linkerd so that it uses the external issuer:
linkerd install --crds | kubectl apply -f -
linkerd install \
--set identity.externalCA=true \
--set identity.issuer.scheme=kubernetes.io/tls \
| kubectl apply -f -
Note
cert-manager rotates the issuer every ~2 days in this setup with no proxy restarts. Rotating the trust anchor still needs a staged process: bundle the old and new anchors, restart the control plane, restart workloads, then drop the old anchor. BEL 2.20 automates trust anchor rotation.
Enable High Availability¶
linkerd install --ha | kubectl apply -f - # new install
linkerd upgrade --ha | kubectl apply -f - # existing install
linkerd install --ha --controller-replicas=2 | kubectl apply -f - # override replicas
linkerd viz install --ha | kubectl apply -f - # viz extension
HA mode runs 3 replicas of critical components, sets resource requests and anti-affinity, and makes the injector webhook fail closed. With a fail-closed injector, annotated pods cannot start while the injector is down. Always leave kube-system unannotated.
Use the CNI Plugin Instead of linkerd-init¶
Use this where pods may not have CAP_NET_ADMIN:
linkerd install-cni | kubectl apply -f -
linkerd install --crds | kubectl apply -f -
linkerd install --linkerd-cni-enabled | kubectl apply -f -
On Cilium, install Cilium with cni.exclusive=false so that chained plugins can write their configuration. Also set socketLB.hostNamespaceOnly=true so that Linkerd sees ClusterIPs.
Mesh Workloads¶
# Annotate a namespace, then restart workloads so the injector sees them
kubectl annotate namespace myapp linkerd.io/inject=enabled
kubectl -n myapp rollout restart deploy
# Or add the annotation to specific manifests
kubectl get deploy -n myapp -o yaml | linkerd inject - | kubectl apply -f -
# Verify proxies are present and healthy
linkerd check --proxy -n myapp
Since 2.20 the proxy is injected as a native sidecar. To opt a workload out (for example on a platform with sidecar quirks), set the following annotation on the namespace or pod template, or set proxy.nativeSidecar=false globally:
For an ingress controller that cannot target Service IPs, use linkerd.io/inject: ingress (linkerd inject --ingress). Make the ingress strip any client-supplied l5d-dst-override header, or it becomes an open relay.
Declare Protocols and Opaque Ports¶
Avoid protocol-detection surprises (Linkerd 2.18+):
apiVersion: v1
kind: Service
metadata:
name: backend
spec:
ports:
- name: http
port: 8090
targetPort: 8090
protocol: TCP
appProtocol: http # skip detection, treat as HTTP
For server-speaks-first protocols that are not in the default list, mark the ports opaque. The value replaces the default list.
Split Traffic for a Canary¶
Use a Gateway API HTTPRoute whose parent is the Service (GAMMA pattern). This replaces SMI TrafficSplit.
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: myapp-canary
namespace: myapp
spec:
parentRefs:
- name: myapp # the Service clients call
kind: Service
group: core # Linkerd docs use "core"; "" is the Gateway API spelling
port: 8080
rules:
- backendRefs:
- name: myapp-v1
port: 8080
weight: 90
- name: myapp-v2
port: 8080
weight: 10
Shift weights step by step and watch linkerd viz stat-outbound between steps. Progressive delivery tools such as Flagger or Argo Rollouts can automate the steps through the Gateway API.
Configure Retries and Timeouts¶
Retries and timeouts are annotations on an HTTPRoute, a GRPCRoute, or a whole Service (2.16+). They run on the client proxy, so the caller must be meshed.
kubectl -n booksapp annotate httproutes.gateway.networking.k8s.io/books-create \
retry.linkerd.io/http=5xx retry.linkerd.io/limit=3 retry.linkerd.io/timeout=300ms
kubectl -n booksapp annotate httproutes.gateway.networking.k8s.io/books-create \
timeout.linkerd.io/request=15s
linkerd viz -n booksapp stat-outbound deploy/webapp # shows RETRIES and TIMEOUTS columns
Only retry idempotent requests. If a ServiceProfile exists for the Service, these annotations are ignored. Delete the profile first.
Enable Circuit Breaking¶
kubectl annotate svc/backend -n myapp \
balancer.linkerd.io/failure-accrual=consecutive \
balancer.linkerd.io/failure-accrual-consecutive-max-failures=5 \
balancer.linkerd.io/failure-accrual-consecutive-min-penalty=5s \
balancer.linkerd.io/failure-accrual-consecutive-max-penalty=1m
The outbound_http_balancer_endpoints metric reports endpoints marked "pending", which includes those with tripped breakers.
Handle Rate-Limited Backends¶
These are experimental 2.20 features. Apply them to the Service that returns 429 or RESOURCE_EXHAUSTED:
# Load Biaser: treat 429s as slow so EWMA steers away
kubectl annotate svc/quota-api -n myapp \
balancer.alpha.linkerd.io/penalize-failures=true \
balancer.alpha.linkerd.io/load-biaser-penalty=5s
# Unified circuit breaker: trip on success rate (429 counts as failure) or consecutive failures
kubectl annotate svc/quota-api -n myapp \
balancer.linkerd.io/failure-accrual=unified \
balancer.alpha.linkerd.io/failure-accrual-success-rate-threshold=0.8 \
balancer.alpha.linkerd.io/failure-accrual-success-rate-window=10s
Restrict Access with Authorization Policy¶
Start in audit mode, watch the logs and metrics, then switch to deny.
apiVersion: policy.linkerd.io/v1beta3
kind: Server
metadata:
name: backend-http
namespace: production
spec:
podSelector:
matchLabels:
app: backend
port: http # named container port
proxyProtocol: HTTP/2
accessPolicy: audit # change to deny once vetted
---
apiVersion: policy.linkerd.io/v1alpha1
kind: MeshTLSAuthentication
metadata:
name: frontend-only
namespace: production
spec:
identities:
- "frontend.production.serviceaccount.identity.linkerd.cluster.local"
---
apiVersion: policy.linkerd.io/v1alpha1
kind: AuthorizationPolicy
metadata:
name: backend-from-frontend
namespace: production
spec:
targetRef:
group: policy.linkerd.io
kind: Server
name: backend-http
requiredAuthenticationRefs:
- name: frontend-only
kind: MeshTLSAuthentication
group: policy.linkerd.io
# Require mTLS for everything in a namespace (applies to pods created after this)
kubectl annotate ns production config.linkerd.io/default-inbound-policy=all-authenticated
# Find audit hits: proxy logs carry authz.name=audit and metrics carry authz_name="audit"
kubectl -n production logs deploy/backend -c linkerd-proxy | grep 'authz.name=audit'
linkerd viz authz -n production deploy/backend
Probes and routes
Once you attach any HTTPRoute to a Server, Linkerd stops authorizing kubelet probes automatically. Add a route and policy (for example a NetworkAuthentication for the node CIDR) that cover /ready and /live.
Legacy ServerAuthorization example (pre-2.12 style)
apiVersion: policy.linkerd.io/v1beta1
kind: ServerAuthorization
metadata:
name: backend-authz
namespace: production
spec:
server:
name: backend-http
client:
meshTLS:
identities:
- "frontend.production.serviceaccount.identity.linkerd.cluster.local"
AuthorizationPolicy. It can also target routes and namespaces.
Rate-Limit Inbound Traffic¶
apiVersion: policy.linkerd.io/v1alpha1
kind: HTTPLocalRateLimitPolicy
metadata:
name: web-rlpolicy
namespace: emojivoto
spec:
targetRef:
group: policy.linkerd.io
kind: Server
name: web-http
total:
requestsPerSecond: 100 # per pod (local), GCRA with 1s tolerance
Per-client fairness and per-identity overrides are also available. See the HTTPLocalRateLimitPolicy reference in the Linkerd docs.
Control Egress Traffic¶
-
Observe first. Create an
EgressNetworkthat allows everything, and enable hostname labels on the client: -
Lock down, then allowlist. Switch to
Denyand attach Gateway API routes to theEgressNetwork:kubectl patch egressnetwork -n egress-test all-egress-traffic \ -p '{"spec":{"trafficPolicy": "Deny"}}' --type=mergeapiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: httpbin-get namespace: egress-test spec: parentRefs: - name: all-egress-traffic kind: EgressNetwork group: policy.linkerd.io namespace: egress-test port: 80 rules: - matches: - path: value: "/get"HTTPS traffic needs a
TLSRoute(SNI match) to be allowed. Denied HTTP requests get403.
Observe Traffic with Viz¶
linkerd viz install | kubectl apply -f -
linkerd viz check
linkerd viz dashboard &
linkerd viz stat deploy -n myapp # golden metrics per workload
linkerd viz stat-inbound deploy/web -n myapp # server-side, by route
linkerd viz stat-outbound deploy/web -n myapp # client-side, by route and backend
linkerd viz top deploy/web -n myapp # live top paths
linkerd viz tap deploy/web -n myapp # live request stream
linkerd viz edges deploy -n myapp # who talks to whom, with mTLS identity
linkerd viz routes reports per-route metrics only for ServiceProfile routes. For Gateway API routes, use stat-inbound and stat-outbound. The old top-level linkerd stat, linkerd top and linkerd edges commands moved under linkerd viz in 2.10.
Verify mTLS¶
linkerd viz edges deploy -n myapp # SECURED column shows mTLS per edge
linkerd identity -n myapp <pod-name> # print the pod's proxy certificate
linkerd viz tap deploy/web -n myapp | grep 'tls=true'
linkerd check --proxy # includes certificate validity checks
Link Clusters (Multicluster)¶
These steps are for the hierarchical (gateway) mode. Both clusters must share a trust anchor.
# 1. Install the extension in both clusters
for ctx in west east; do
linkerd --context=${ctx} multicluster install | kubectl --context=${ctx} apply -f -
done
# 2. Tell west to run a controller for a link named "east"
cat <<EOF > values.yaml
controllers:
- link:
ref:
name: east
EOF
linkerd --context=west multicluster install -f values.yaml | kubectl --context=west apply -f -
# 3. Generate the Link resource and credentials from east, apply them in west (GitOps-friendly)
linkerd --context=east multicluster link-gen --cluster-name east | kubectl --context=west apply -f -
linkerd --context=west multicluster check
linkerd --context=west multicluster gateways
# 4. Export a service from east; it appears in west as podinfo-east
kubectl --context=east -n test label svc/podinfo mirror.linkerd.io/exported=true
linkerd multicluster link-gen (2.18+) replaces the older linkerd multicluster link workflow in current docs. Commit its output to Git instead of piping it straight to kubectl.
Federate a Service Across Clusters¶
Federated services use flat networking (pod-to-pod reachability), not gateways.
for ctx in west east north; do
linkerd --context ${ctx} multicluster install --gateway=false | kubectl --context ${ctx} apply -f -
done
linkerd --context east multicluster link-gen --cluster-name=east --gateway=false | kubectl --context west apply -f -
linkerd --context north multicluster link-gen --cluster-name=north --gateway=false | kubectl --context west apply -f -
# Mark each cluster's copy as a federation member
for ctx in west east north; do
kubectl --context ${ctx} -n mc-demo label svc/bb mirror.linkerd.io/federated=member
done
kubectl --context west -n mc-demo get svc bb-federated
linkerd --context west diagnostics endpoints bb-federated.mc-demo.svc.cluster.local:8080
Upgrade Linkerd¶
The order is CLI, then CRDs and control plane, then extensions, then data plane. Keep the data plane within one major version of the control plane.
linkerd check && linkerd check --proxy # start healthy
curl --proto '=https' --tlsv1.2 -sSfL https://run.linkerd.io/install-edge | sh
linkerd version --client
linkerd upgrade --crds | kubectl apply -f -
linkerd upgrade | kubectl apply -f -
linkerd prune | kubectl delete -f - # remove resources dropped in the new version
linkerd check
linkerd viz install | kubectl apply -f - # extensions: re-run install
linkerd viz prune | kubectl delete -f -
linkerd multicluster install | kubectl apply -f -
kubectl -n myapp rollout restart deploy # data plane picks up the new proxy
linkerd check --proxy
With Helm, run helm upgrade linkerd-crds linkerd-edge/linkerd-crds and then helm upgrade linkerd-control-plane linkerd-edge/linkerd-control-plane --reset-values -f values.yaml --atomic. Always read the edge release notes: edge releases are not semantically versioned and may carry breaking changes.
Use ServiceProfiles (Legacy)¶
ServiceProfiles still work but receive no new features. They also disable HTTPRoute retries and circuit breaking for their Service. Keep this only for existing setups.
apiVersion: linkerd.io/v1alpha2
kind: ServiceProfile
metadata:
name: backend.myapp.svc.cluster.local
namespace: myapp
spec:
routes:
- name: GET /api/health
condition:
method: GET
pathRegex: /api/health
isRetryable: true
- name: POST /api/orders
condition:
method: POST
pathRegex: /api/orders
timeout: 30s
retryBudget:
retryRatio: 0.2
minRetriesPerSecond: 10
ttl: 10s
Troubleshooting¶
| Symptom | Diagnose | Likely fix |
|---|---|---|
| Pods not injected | linkerd check --proxy, kubectl get pod -o yaml (look for linkerd-proxy) |
Annotate the namespace or workload and restart the pods. Check that the injector is running. HA mode blocks scheduling when it is down. |
linkerd check warns about certificates |
linkerd check identity section |
Rotate the issuer (or automate it with cert-manager). Plan trust anchor rotation well before expiry. |
| Meshed calls fail after cert expiry | Proxy logs show TLS or identity errors | Replace the expired issuer or anchor with linkerd upgrade --identity-trust-anchors-file=... or with Helm values, then restart workloads. |
| HTTP features missing (no route metrics, retries) on a port | linkerd diagnostics proxy-metrics, protocol detection metrics |
Set appProtocol on the Service port, or fix opaque ports. Remove any ServiceProfile that blocks the new features. |
| Server-speaks-first protocol hangs about 10 s on connect | Connection timing | Add the port to config.linkerd.io/opaque-ports |
| No per-request balancing or routes with Cilium | linkerd viz stat-outbound shows no routes |
Set Cilium socketLB.hostNamespaceOnly=true |
| Pods fail readiness after adding an HTTPRoute to a Server | Proxy logs show 403 on probe paths | Explicitly authorize the probe routes |
| Job pods never complete | Pod shows proxy still running | Use native sidecars (default in 2.20). On older versions, enable them with the annotation. |
| Gateway API conflicts with another project | CRD bundle-version annotation | Pin one Gateway API version within Linkerd's supported range. Do not let two charts install it. |
| High proxy latency or memory | linkerd viz top, pod metrics |
Set proxy resource annotations. Check destination controller memory on high-churn clusters (improved in 2.20). |