102

Workload identity before secrets: short-lived, audience-bound credentials instead of stored keys

#
  • practice
  • established
  • config

Scope. Workload identity moves the root of trust, it does not remove it: the issuer, its signing keys and the attestation that decides which workload gets which identity become the most sensitive part of the platform. Audience binding helps only where verifiers actually check it, and a short-lived bearer token can still be replayed within its lifetime.

Versions. Kubernetes v1.22+ mounts short-lived TokenRequest tokens by default; the projected-volume fields and the 600-second minimum are as documented for the current release (v1.37).

Conflicts with: Short credential TTL ↔ static stability of the data plane

Primary sources: SPIFFE — Concepts (identity, SVIDs, Workload API), SPIFFE — JWT-SVID specification (aud and exp requirements, replay), Kubernetes Documentation — Service Accounts (short-lived tokens; long-lived Secret tokens not recommended)

References: Kubernetes Documentation — Projected Volumes: serviceAccountToken, Kubernetes Documentation — Configure Service Accounts for Pods (token projection, kubelet refresh, api-audiences), Kubernetes Documentation — Managing Service Accounts (bound tokens, pod deletion invalidates), SPIFFE — X509-SVID specification

A service should prove what it is with a credential the platform issues to an attested workload — short-lived, rotated automatically, and bound to the audience that will verify it — rather than with a long-lived key someone copied into a secret store. Identity answers “who is calling”; whether the caller may do something is a separate decision made by policy. Secrets that cannot be avoided are then fetched with that identity, not shipped alongside it.

Authentication is not authorization. SPIFFE deliberately stops at identity, and a Kubernetes service account that authenticates successfully can do nothing until RBAC grants it something. “Holds a valid token, therefore allowed” is the bug: every verifier still maps identity to the narrowest permission (33) at its own trust boundary (32). The audience is what stops lateral replay. A bearer token accepted by any verifier lets every service that receives it impersonate the caller to every other service. Binding the token to one audience, and having verifiers reject tokens without theirs, confines a leaked token to one recipient; the kube-apiserver itself only accepts tokens bound to its own configured audiences.

Code example (config)
config
Kubernetes
apiVersion: v1
kind: ServiceAccount
metadata:
  name: billing-exporter
  namespace: billing
automountServiceAccountToken: false   # no API-server token unless a pod asks for one
---
apiVersion: v1
kind: Pod
metadata:
  name: billing-exporter
  namespace: billing
spec:
  serviceAccountName: billing-exporter
  containers:
  - name: exporter
    image: registry.example.com/billing-exporter:1.4.2
    volumeMounts:
    - name: warehouse-token
      mountPath: /var/run/secrets/warehouse   # not a subPath: those never see rotation
      readOnly: true
  volumes:
  - name: warehouse-token
    projected:
      sources:
      - serviceAccountToken:
          audience: warehouse.example.com   # rejected by the API server and by verifiers expecting another audience
          expirationSeconds: 600            # the minimum the API accepts
          path: token                       # the kubelet rewrites it; the app must re-read it

Short-lived means rotation is the normal path. The kubelet refreshes a projected token once it is older than 80% of its TTL (or 24 hours), and the application is responsible for re-reading the file; a client that reads the token once at startup works in every test and fails in production exactly one TTL later. Pod-bound tokens also stop authenticating when the pod is deleted, which is revocation for free. What remains is replay within the lifetime: a JWT is still a bearer credential, so for service-to-service traffic an X.509 identity over mTLS, which requires possession of the private key, is the stronger form.

The TTL is also a dependency budget. The issuer is part of the control plane (101): if it is down for longer than the gap between the refresh point and expiry, the data plane loses its credentials. At 80% refresh that gap is a fifth of the TTL — two minutes for a 600-second token — so the TTL is chosen from issuer availability as much as from exposure. Static third-party API keys remain secrets; keep them in a manager that the workload reaches with its identity, never baked into the image (34).

Last technical review: 2026-08-04 · Reference · 112 principles