Skip to content

Linkerd Security ​

Last Updated: September 11, 2026 · Linkerd edge-26.9.1 · cert-manager examples checked against 1.21.1

Linkerd provides workload authentication, transport encryption and inbound authorization for traffic handled by its proxies. Enrollment, policy, certificate lifecycle and application security still need explicit design. Use the supported Kubernetes/Gateway API combination in the installation guide; the examples here assume that installation and existing application workloads.

Security Architecture ​

Logical signing chain and control-plane roles. The root signs an issuer; the Identity service uses that issuer to sign workload certificates. The drawing does not imply that the root private key must be stored in the cluster.

Logical signing chain and control-plane roles. The root signs an issuer; the Identity service uses that issuer to sign workload certificates. The drawing does not imply that the root private key must be stored in the cluster.Open full screen ↗

Automatic mTLS ​

Linkerd automatically uses mTLS for eligible TCP traffic between meshed Pods. Both proxies must participate, trust the certificate chain and receive the traffic. Skip ports bypass the proxy; UDP is outside this TCP mechanism. Traffic to or from unmeshed endpoints does not acquire Linkerd mTLS merely because one endpoint has a proxy.

For an application speaking plain HTTP, the outbound proxy authenticates the destination proxy and encrypts the network hop; the receiving proxy authenticates the caller and forwards HTTP to its local application. Application-originated TLS can remain encrypted through the mesh: Linkerd does not automatically decrypt every external or opaque TLS stream.

PropertyMeaning and boundary
Transparent encryptionNo application TLS implementation is required for the eligible proxy-to-proxy hop
Mutual authenticationThe proxies authenticate workload identities, not end users
TLS 1.3The selected release's mesh TLS protocol
Automatic leaf renewalProxies normally renew their short-lived workload certificates
Root/issuer lifecycleSeparate credentials that still need rotation and monitoring

By default, Linkerd accepts plaintext from unmeshed sources. Authorization policy can reject it. “mTLS enabled” is therefore different from “all inbound access requires an authenticated mesh identity.” Network policy and admission controls must also cover paths that bypass or omit the proxy.

Observe encryption and identity ​

bash
linkerd check --proxy
linkerd viz edges deploy -n production
linkerd viz tap deploy/api -n production --method GET
linkerd identity -n production -l app=api
kubectl -n production get pods -l app=api \
  -o custom-columns=NAME:.metadata.name,SERVICEACCOUNT:.spec.serviceAccountName

viz edges reports observed resource edges and their security state; it is not an inventory of every possible or idle connection. tap shows supported observed traffic, not a complete packet/security audit. Its display is not the same interface as Prometheus TLS label values. Check both accepted and deliberately denied traffic from the intended client identities.

linkerd identity retrieves public certificates from selected Pods through port forwarding. Inspect their SANs, issuer and validity. This avoids assuming that an issued leaf is available at a fixed file path inside the proxy image.

Workload Identity ​

For the standard Kubernetes identity path, Linkerd uses this DNS-form identity:

text
<service-account>.<namespace>.serviceaccount.identity.<control-plane-namespace>.<trust-domain>

web.production.serviceaccount.identity.linkerd.cluster.local
api.production.serviceaccount.identity.linkerd.cluster.local

The examples use control-plane namespace linkerd and trust domain cluster.local. The root certificate's common name is not itself the workload trust-domain setting. This is not the Istio-style spiffe://.../ns/.../sa/... URI previously shown here. Multiple Pods with the same ServiceAccount share an authorization identity, although their private keys/certificates are separate.

The proxy generates its key and CSR, and sends the CSR with its projected ServiceAccount token to Identity. Identity validates the token using Kubernetes TokenReview, checks the requested identity and signs with the issuer's key. The root signs the issuer; it does not sign every proxy request. The private key is not derived from the ServiceAccount token.

Default workload certificates last about 24 hours and refresh before expiry. A certificate request does not generate a new Kubernetes ServiceAccount, and a renewal is not proof that all keys rotate on every refresh. See the architecture guide for the lifecycle.

Authorization Policy ​

These resources belong to Linkerd's policy.linkerd.io API. AuthorizationPolicy is not a Gateway API resource; it was introduced in Linkerd 2.12. It can target routes that use Gateway API definitions.

ResourceRole
ServerSelects a declared inbound port on matching Pods in its namespace
HTTPRoute/GRPCRoute attached to ServerSelects a subset of inbound requests
MeshTLSAuthenticationDescribes allowed mesh identities
NetworkAuthenticationDescribes allowed client IP networks; does not supply mTLS
AuthorizationPolicyGrants access to a target when its authentication requirements match
ServerAuthorizationOlder Server-only grant; supported as v1beta1 in the selected CRDs

ServerAuthorization and AuthorizationPolicy are alternative grant mechanisms, not a sequential pipeline. Multiple grants can broaden access; multiple requiredAuthenticationRefs within one AuthorizationPolicy must all match. A namespace-targeted AuthorizationPolicy covers policy targets defined in that namespace, not an automatic policy for every undeclared port.

Servers must not select overlapping Pod/port pairs. Declare the application port in the Pod specification. A Server defaults to denying unmatched traffic even if the namespace's default policy is permissive. accessPolicy: audit can help observe unmatched traffic during preparation, but it allows that traffic and is not enforcement.

Default policy ​

This annotation configures newly created proxies in an enrolled namespace:

yaml
apiVersion: v1
kind: Namespace
metadata:
  name: production
  annotations:
    linkerd.io/inject: enabled
    config.linkerd.io/default-inbound-policy: deny

Changing the namespace annotation does not retrofit the initialized default into existing proxies. Coordinate workload-specific rollouts and verify readiness. Dynamic policy CRDs are a separate mechanism and can update policy without replacing every Pod.

The cluster-wide Helm value is proxy.defaultInboundPolicy, not policyController.defaultPolicy. Merge it into the complete installation values, preserving CA configuration and release ownership:

yaml
proxy:
  defaultInboundPolicy: deny
DefaultMeaning
all-unauthenticatedAllows traffic without requiring mesh authentication; installation default
all-authenticatedRequires authenticated mesh clients, including appropriately trusted multicluster clients
cluster-authenticatedRequires authenticated clients from the same cluster
cluster-unauthenticatedAllows clients in the configured cluster network scope without requiring mesh authentication
denyDenies unmatched traffic, subject to explicit policy and documented probe handling
auditAllows unmatched traffic while recording audit evidence

Cluster scope is not an end-user identity or an application authorization boundary. Verify configured networks and the source addresses visible at the proxy.

Microservice example ​

For these separate example resources, prepare meshed frontend/API/PostgreSQL workloads in production, with app: frontend/api/postgres, the declared ports below, and their corresponding ServiceAccounts. Prepare the meshed ingress workload with ServiceAccount edge-gateway in namespace ingress; this name alone does not install or authenticate a gateway.

yaml
apiVersion: policy.linkerd.io/v1beta3
kind: Server
metadata:
  name: frontend-http
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: frontend
  port: 8080
  proxyProtocol: HTTP/1
  accessPolicy: deny
---
apiVersion: policy.linkerd.io/v1alpha1
kind: AuthorizationPolicy
metadata:
  name: frontend-from-gateway
  namespace: production
spec:
  targetRef:
    group: policy.linkerd.io
    kind: Server
    name: frontend-http
  requiredAuthenticationRefs:
  - kind: ServiceAccount
    name: edge-gateway
    namespace: ingress
---
apiVersion: policy.linkerd.io/v1beta3
kind: Server
metadata:
  name: api-http
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: api
  port: 8080
  proxyProtocol: HTTP/1
  accessPolicy: deny
---
apiVersion: policy.linkerd.io/v1alpha1
kind: AuthorizationPolicy
metadata:
  name: api-from-frontend
  namespace: production
spec:
  targetRef:
    group: policy.linkerd.io
    kind: Server
    name: api-http
  requiredAuthenticationRefs:
  - kind: ServiceAccount
    name: frontend
    namespace: production
---
apiVersion: policy.linkerd.io/v1beta3
kind: Server
metadata:
  name: database-tcp
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: postgres
  port: 5432
  proxyProtocol: opaque
  accessPolicy: deny
---
apiVersion: policy.linkerd.io/v1alpha1
kind: AuthorizationPolicy
metadata:
  name: database-from-api
  namespace: production
spec:
  targetRef:
    group: policy.linkerd.io
    kind: Server
    name: database-tcp
  requiredAuthenticationRefs:
  - kind: ServiceAccount
    name: api
    namespace: production

The intended call chain is gateway → frontend → API → database. A ServiceAccount name in YAML is not sufficient: the caller must present the authenticated identity of that account. Verify that no broader namespace/Server grant also permits unwanted callers.

Linkerd normally adds authorizations for declared HTTP health/readiness probes when no explicit route is attached to the Server. Once HTTPRoute/GRPCRoute resources attach, those default probe grants are not created; explicitly model required probe routes and their limited access. Do not grant unauthenticated access to an entire business port merely to make one probe succeed.

For reference, this alternative legacy grant is equivalent to the API's frontend-client grant. It does not need to be combined with the AuthorizationPolicy above:

yaml
apiVersion: policy.linkerd.io/v1beta1
kind: ServerAuthorization
metadata:
  name: api-from-frontend-legacy
  namespace: production
spec:
  server:
    name: api-http
  client:
    meshTLS:
      serviceAccounts:
      - name: frontend
        namespace: production

The selected release does not serve ServerAuthorization/v1beta2; do not infer a resource's API version from Server's version. client.unauthenticated:true permits clients without mesh authentication, whereas meshTLS.identities:["*"] still requires a meshed identity and grants it very broadly.

Metrics ports and verification ​

For an explicitly declared application metrics port 9091 on the API Pod, an example grant is:

yaml
apiVersion: policy.linkerd.io/v1beta3
kind: Server
metadata:
  name: api-app-metrics
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: api
  port: 9091
  proxyProtocol: HTTP/1
  accessPolicy: deny
---
apiVersion: policy.linkerd.io/v1alpha1
kind: AuthorizationPolicy
metadata:
  name: metrics-from-prometheus
  namespace: production
spec:
  targetRef:
    group: policy.linkerd.io
    kind: Server
    name: api-app-metrics
  requiredAuthenticationRefs:
  - kind: ServiceAccount
    name: prometheus
    namespace: monitoring

This is different from the proxy's own admin port, normally 4191. The proxy-init configuration exempts the admin/control ports from ordinary inbound interception. A Server on 4191 therefore does not make that endpoint an mTLS-protected application port. Use the actual cluster/network controls and restricted access paths for management endpoints.

bash
kubectl -n production get servers,authorizationpolicies,serverauthorizations
kubectl -n production get server api-http -o yaml
# Set this to an actual selected API Pod.
api_pod=api-example-pod
linkerd diagnostics policy -n production "pod/$api_pod" 8080 -o json
linkerd viz authz deploy/api -n production

Known HTTP policy rejection normally produces HTTP 403; opaque/TCP traffic can be rejected at connection level. A changed policy may interrupt existing connections. Kubernetes Forbidden events are not an automatic per-request record of proxy authorization denials. Use policy diagnostics and the appropriate HTTP/TCP authorization metrics.

Certificate Management ​

CredentialPurposeDefault/manual ownership considerations
Trust anchor certificate bundlePublic roots accepted by the meshNormally ConfigMap linkerd-identity-trust-roots, key ca-bundle.crt
Identity issuer certificate/keyIntermediate CA used by Identity to sign workload certificatesSecret linkerd-identity-issuer; key names depend on the issuer scheme
Workload certificate/keyPer-proxy TLS credentialShort-lived leaf, automatically refreshed by the proxy

The default CLI-generated root and issuer expire after one year; workload leaves normally last 24 hours. A manually chosen ten-year root is possible, not a universal recommendation or the installation default. Choose lifetimes and renewal lead time from the CA policy and recovery process, and track every certificate in the chain.

Linkerd's supplied root/issuer credentials require ECDSA P-256. The installation guide includes explicit generation parameters and local private-key handling. Keep the root signing key separate from the public trust bundle; a public ConfigMap must never contain that key.

Read the effective public credentials ​

bash
set -euo pipefail
umask 077
# Public trust bundle: ConfigMap data is not base64-encoded.
kubectl -n linkerd get configmap linkerd-identity-trust-roots -o json \
  | jq -er '.data["ca-bundle.crt"] | select(length > 0)' > current-trust.pem
# Select only public certificate data from the issuer Secret, never its key.
kubectl -n linkerd get secret linkerd-identity-issuer -o json \
  | jq -er '(.data["tls.crt"] // .data["crt.pem"]) | select(length > 0)' \
  | base64 -d > current-issuer.pem

# Show every certificate in a multi-root bundle, not only its first entry.
openssl crl2pkcs7 -nocrl -certfile current-trust.pem \
  | openssl pkcs7 -print_certs -text -noout
openssl x509 -in current-issuer.pem -noout -subject -issuer -dates
# Nonzero exit means expiration is within this window or parsing failed.
openssl x509 -in current-issuer.pem -noout -checkend 86400

Under the default linkerd.io/tls scheme the issuer Secret uses crt.pem/key.pem; kubernetes.io/tls uses tls.crt/tls.key. The command selects public certificate data only. Check the configured scheme and resource owner before changing anything.

Inspect every root in a bundle. openssl x509 by itself only examines the first certificate; it is not a complete multi-root expiry audit. Verify the issuer chain against the intended trust anchors as well as dates, and provide intermediate certificates when the chain requires them. Parsing or API-read failure must be reported as failure, not “certificate healthy.”

Issuer renewal with an unchanged trust anchor ​

Update the issuer through its owner: complete Helm/CLI certificate values for a Linkerd-owned Secret, or the certificate controller for a managed Secret. Identity watches its mounted issuer files, validates the replacement and reloads a valid issuer; a blanket Identity Deployment restart is not a required step for every renewal.

bash
kubectl -n linkerd get events --field-selector reason=IssuerUpdated
kubectl -n linkerd get events --field-selector reason=IssuerUpdateSkipped
kubectl -n linkerd logs deployment/linkerd-identity -c identity --tail=100
linkerd check --proxy
linkerd identity -n production -l app=api

IssuerUpdated confirms that Identity accepted an update. Investigate IssuerUpdateSkipped or validation errors. Existing proxy leaves can remain signed by the previous issuer until their normal refresh; that is expected while both chains remain valid. Immediate replacement of every leaf is a separate coordinated workload operation.

Trust anchor rotation ​

Replacing a root needs a staged transition. The healthy-root procedure is not a guaranteed recovery method for a root that has already expired.

  1. Inventory the active root bundle, issuer chain, managed resources and every consumer, including control-plane proxies, workloads, external workloads and linked clusters. Confirm capacity/readiness for the planned rollout.
  2. Generate the new root and retain the public old+new bundle. Update the bundle through its actual owner.
  3. Distribute that overlap bundle to all consumers before switching the issuer. Proxies receive trust through installation/injection configuration; a ConfigMap write alone does not prove that existing processes have reloaded it.
  4. Verify distribution with linkerd check --proxy and workload/cross-cluster checks. Then issue and load an issuer signed by the new root.
  5. Allow or deliberately coordinate leaf renewal, and verify that all relevant clients/servers use the new chain. A fixed sleep or only a successful controller rollout is insufficient.
  6. Remove the old root through the bundle owner, propagate the final bundle to all consumers, and verify connections and trust again.

Preserve rollback material and monitor each stage. Restart only reviewed meshed workload controllers with workload-appropriate readiness/disruption handling; an all-namespace Deployment loop misses other workload types and can disrupt unrelated workloads. This document does not claim that an untested rotation is zero downtime.

External Certificate Management ​

cert-manager issuer renewal ​

This example assumes an existing, validated CA certificate and ECDSA P-256 signing key in the linkerd-trust-anchor Secret in namespace linkerd. A cert-manager CA Issuer keeps that signing key in the cluster; choose a different CA integration if that does not fit the trust model. The selected cert-manager version must support the cluster's Kubernetes version.

yaml
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: linkerd-trust-anchor
  namespace: linkerd
spec:
  ca:
    secretName: linkerd-trust-anchor
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: linkerd-identity-issuer
  namespace: linkerd
spec:
  secretName: linkerd-identity-issuer
  duration: 8760h
  renewBefore: 720h
  issuerRef:
    name: linkerd-trust-anchor
    kind: Issuer
    group: cert-manager.io
  commonName: identity.linkerd.cluster.local
  isCA: true
  privateKey:
    algorithm: ECDSA
    size: 256
    rotationPolicy: Always
  usages:
  - cert sign
  - crl sign
  - server auth
  - client auth

The issuer is a CA because it signs workload leaves. rotationPolicy: Always makes key rotation explicit. Here 8760h is 365 days and renewBefore:720h means renewal 30 days before expiry, not every 30 days. Ensure that the parent CA remains valid long enough: the CA Issuer does not automatically enforce every chain-lifetime/path-length constraint, and updating its CA Secret does not automatically reissue all dependent certificates.

bash
kubectl -n linkerd get issuer linkerd-trust-anchor
kubectl -n linkerd get certificate linkerd-identity-issuer
kubectl -n linkerd describe certificate linkerd-identity-issuer
# Inspect public certificate contents and effective issuer loading as above.

The Certificate must be Ready, its Secret must have the expected keys/chain, and Identity must accept it before this is a functioning integration.

Choose trust-bundle ownership explicitly ​

Option A: cert-manager owns the issuer; Helm owns the public trust bundle. Save this as managed-issuer-values.yaml and supply the root bundle through the complete reviewed chart values:

yaml
identity:
  externalCA: false
  issuer:
    scheme: kubernetes.io/tls
bash
# Merge into the complete reviewed values from the installation guide.
# In this option, Helm owns the public trust bundle; cert-manager owns the issuer.
helm template linkerd-control-plane linkerd-edge/linkerd-control-plane \
  --version 2026.9.1 -n linkerd \
  -f reviewed-values.yaml -f managed-issuer-values.yaml \
  --set-file identityTrustAnchorsPEM=ca.crt > reviewed-control-plane.yaml

With kubernetes.io/tls, the chart expects the issuer Secret to exist instead of creating a Linkerd-format one. With externalCA:false, Helm still creates the public trust ConfigMap. Review the rendered objects and existing ownership before a deployment through the installation workflow.

Option B: an external controller also owns the trust ConfigMap. In that different ownership model:

yaml
identity:
  externalCA: true
  issuer:
    scheme: kubernetes.io/tls

identity.externalCA:true means the chart does not create linkerd-identity-trust-roots. An external controller such as trust-manager must supply that ConfigMap in the control-plane namespace with ca-bundle.crt. Merely passing identityTrustAnchorsPEM while omitting the external ConfigMap does not complete this setup.

For managed root rotation, retain the previous public certificate in the overlap bundle, coordinate issuer renewal and consumer rollouts, and then retire it. Do not copy an entire CA Secret just to retain its public certificate. cert-manager/trust-manager do not make all workload restarts and trust transitions automatic.

Vault integration boundary ​

Vault can participate in the CA design, but an ordinary PKI sign/<role> leaf-signing recipe is not a complete Linkerd issuer workflow. Linkerd requires an actual intermediate CA certificate; setting isCA:true on a Certificate resource alone does not demonstrate that the Vault endpoint grants that capability.

Verify the selected integration's signing endpoint and request/response mapping. Vault documents privileged root/sign-intermediate and issuer-specific intermediate-signing endpoints; permission to use them grants CA issuance capability and needs a deliberately restricted role/policy. Also validate ECDSA P-256, the returned chain, issuer lifetime, Vault server trust and renewal behavior.

For cert-manager authentication, prefer the documented short-lived ServiceAccount token flow where appropriate, with the required TokenRequest RBAC, Vault Kubernetes/JWT auth configuration and audiences. A Secret called vault-token is not sufficient by itself. The former YAML omitted these prerequisites and a proven intermediate-CA issuance path, so it is not presented as a tested deployment recipe.

Application Security and Monitoring ​

ResponsibilityLinkerd contributionAdditional controls
Network hopEligible proxy-to-proxy mTLSTLS for other hops, network restrictions and endpoint exposure
Workload authenticationServiceAccount-derived mesh identityEnd-user/API-client authentication and token validation
Service accessInbound authorization policyApplication roles, tenant and object authorization
Data handlingDoes not validate business inputInput validation, output handling and data protection

A permitted frontend identity does not prove that its caller is an administrator. Applications must validate user credentials and business permissions as well as inputs.

Meaningful security alerts ​

The following rules require Prometheus Operator and a Prometheus selecting this PrometheusRule, plus scrapes retaining the shown namespace/deployment and proxy TLS identity labels. Review target and cluster scope for shared backends.

yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: linkerd-security-alerts
  namespace: monitoring
spec:
  groups:
  - name: linkerd-security
    rules:
    - alert: LinkerdWorkloadCertificateExpiring
      expr: identity_cert_expiration_timestamp_seconds{namespace="production"} - time() < 3600
      for: 10m
      labels:
        severity: warning
      annotations:
        summary: Proxy workload certificate has less than one hour remaining
    - alert: LinkerdIssuerCertificateExpiring
      expr: issuer_cert_ttl_seconds{job="linkerd-controller",component="identity"} < 86400
      for: 10m
      labels:
        severity: warning
      annotations:
        summary: Identity issuer has less than one day remaining
    - alert: LinkerdInboundHTTPWithoutMeshIdentity
      expr: |-
        ((sum(rate(response_total{namespace="production",deployment="api",direction="inbound"}[5m])) - (sum(rate(response_total{namespace="production",deployment="api",direction="inbound",tls="true",client_id!=""}[5m])) or vector(0))) / sum(rate(response_total{namespace="production",deployment="api",direction="inbound"}[5m])) > 0.10)
        and on() (sum(rate(response_total{namespace="production",deployment="api",direction="inbound"}[5m])) > 0)
      for: 5m
      labels:
        severity: warning
      annotations:
        summary: More than 10% of observed API HTTP responses lack authenticated mesh client identity
    - alert: LinkerdInboundHTTPAuthorizationDenied
      expr: sum(rate(inbound_http_authz_deny_total{namespace="production",deployment="api"}[5m])) > 0
      for: 5m
      labels:
        severity: warning
      annotations:
        summary: API inbound HTTP authorization denials observed

identity_cert_expiration_timestamp_seconds measures a proxy leaf's absolute expiration time. A seven-day warning would always match a healthy default 24-hour leaf. The controller's issuer_cert_ttl_seconds is already a remaining duration; do not subtract time() from it. Its selector uses the default Viz controller job/component labels; that scrape does not add a namespace label. Adapt the selector if a custom collector changes those labels. Tune thresholds to the configured credential lifetimes and expected refresh interval, and separately monitor the public roots and scrape availability.

For the selected proxy, TLS labels include true, no_identity, disabled and opaque; the original tls="false" query did not match the intended series. tls="true" alone can also lack a client identity. The example compares completed inbound API HTTP responses with those carrying both TLS and a nonempty authenticated client_id, using rates and a positive-traffic guard.

This ratio is not a percentage of all network bytes or all plaintext traffic. It does not cover bypassed paths or opaque TCP, and it depends on retaining identity labels. Expected probes or deliberately unauthenticated routes need their own scope/baseline. For all-authenticated traffic without an unauthenticated series, the underlying ratio is zero and this alert does not fire. No traffic or missing data does not prove safety.

HTTP authorization-denial counters are distinct from application login failures. Use the TCP authorization counters for opaque connections, and do not infer “no denials” from a missing scrape. Audit mode logs/metrics record permissive unmatched traffic rather than enforced rejections.

Next Steps and References ​