Workload identity before secrets: short-lived, audience-bound credentials instead of stored keys
#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.
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 itShort-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).