Kubernetes has three different identity mechanisms that are relevant to workload security, and they interact in ways that produce security problems if you don't understand each one's scope. This post covers all three — ServiceAccounts, projected service account tokens, and Workload Identity Federation with cloud providers — along with the gaps that none of them address.
ServiceAccounts: The Baseline
Every Kubernetes pod runs under a ServiceAccount. If you don't specify one, it runs under the default ServiceAccount in its namespace. ServiceAccounts exist primarily to give pods a Kubernetes API identity — they're the subject used in RBAC bindings that control what the Kubernetes API will let a pod do (list pods, read ConfigMaps, create Jobs, and so on).
The classic ServiceAccount token was mounted automatically into every pod at /var/run/secrets/kubernetes.io/serviceaccount/token. This token had no expiry. It was a signed JWT created at pod start time, valid forever, identifying the pod as the ServiceAccount. If a workload didn't need Kubernetes API access at all — which is most workloads — it still got this token mounted, and that token was a valid, non-expiring credential that an attacker with pod access could use.
Starting in Kubernetes 1.22 (and becoming the default in 1.24), the auto-mounted token changed to a projected service account token. The differences matter:
- Audience-bound: The token is issued for a specific audience (
apiby default, meaning the Kubernetes API server). A token issued forapicannot be used to authenticate to a different service. - Time-limited: Default TTL is 1 hour. The kubelet automatically rotates the token before it expires.
- Pod-bound: The token is bound to the specific pod UID. If the pod is deleted, the token is invalid even if it hasn't expired yet.
This is a significant improvement. But it's still limited to Kubernetes API authentication.
Projected Tokens for External Audiences
The audience field in projected service account tokens is the key to using Kubernetes identity for non-Kubernetes authentication. You can request a projected token with a custom audience value, and that token will be signed by the Kubernetes API server but targeted at your specified audience:
apiVersion: v1
kind: Pod
metadata:
name: payment-processor
spec:
serviceAccountName: payment-processor
volumes:
- name: aws-token
projected:
sources:
- serviceAccountToken:
path: token
expirationSeconds: 3600
audience: sts.amazonaws.com
containers:
- name: app
image: payment-processor:latest
volumeMounts:
- name: aws-token
mountPath: /var/run/secrets/token
readOnly: true
This pod gets a projected token with audience sts.amazonaws.com. That token can be presented to AWS STS's AssumeRoleWithWebIdentity API to get temporary IAM credentials, because AWS is configured to trust tokens from your Kubernetes OIDC issuer for that audience.
This is Kubernetes Workload Identity Federation — the same OIDC exchange model as GitHub Actions, but using the Kubernetes API server as the OIDC provider. EKS, GKE, and AKS all support this pattern natively through their respective Workload Identity implementations.
EKS Pod Identity and IRSA
AWS has two mechanisms for pod-to-AWS authentication. The older one is IRSA (IAM Roles for Service Accounts), which uses the projected token pattern above — you annotate a Kubernetes ServiceAccount with an IAM role ARN, and the EKS admission controller patches the pod to mount the appropriate projected token.
apiVersion: v1
kind: ServiceAccount
metadata:
name: payment-processor
namespace: payments
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/payment-processor-prod
The newer mechanism is EKS Pod Identity, introduced in late 2023. It moves the token exchange from the pod's volume mount to an agent running on the node. The pod calls the local agent (via an environment variable pointing to the agent endpoint), the agent fetches temporary credentials from AWS using the pod's identity, and returns them to the pod. The advantage: no need to configure OIDC providers per cluster, and the credentials are automatically refreshed by the agent rather than requiring the pod to handle token expiry.
Both IRSA and EKS Pod Identity solve one specific problem: pods authenticating to AWS. They don't address pods authenticating to each other, pods authenticating to managed databases that aren't RDS, or pods authenticating to third-party services.
The Default ServiceAccount Problem
The most common workload identity misconfiguration in Kubernetes: using the default ServiceAccount in a namespace, or creating one ServiceAccount per namespace and binding it to everything.
The default ServiceAccount in each namespace exists by Kubernetes design and cannot be deleted. By default, it has no RBAC permissions beyond what's granted to all authenticated identities. The problem arises when a team decides to grant the default ServiceAccount permissions to simplify development — they need pods to read some ConfigMaps or call the Kubernetes API, so they create a ClusterRoleBinding for the default ServiceAccount, and now every pod in that namespace that doesn't specify a different ServiceAccount inherits those permissions.
The correct pattern: one ServiceAccount per workload, with RBAC that precisely reflects what that workload needs. Set automountServiceAccountToken: false at the ServiceAccount level for any workload that doesn't need Kubernetes API access, and explicitly set automountServiceAccountToken: true in the pod spec only where needed.
apiVersion: v1
kind: ServiceAccount
metadata:
name: payment-processor
namespace: payments
automountServiceAccountToken: false
RBAC: What Gets Accumulated
In a Kubernetes deployment that's been running for a year, the RBAC bindings tend to accumulate permission debt in a predictable pattern:
An engineer needs a pod to read a ConfigMap. They create a Role with get on ConfigMaps and bind it to the ServiceAccount. Three months later, a different engineer needs the same pod to also list ConfigMaps (because a library they're using calls list internally). They modify the Role or create a new RoleBinding. Six months after that, the original use of the get permission is gone from the codebase because the configuration moved to a different system, but the Role still has it.
Nobody removes RBAC permissions that have become unnecessary, because the downside (something breaks) is visible and the upside (slightly less attack surface) is invisible. Permissions drift toward the maximum ever granted, not toward the minimum currently needed.
The audit to run:
kubectl get rolebindings,clusterrolebindings \
--all-namespaces -o json \
| jq '.items[] | select(.subjects[]?.name == "default") | {kind: .kind, name: .metadata.name, namespace: .metadata.namespace, role: .roleRef.name}'
Anything binding the default ServiceAccount to a role warrants examination. The expectation should be: most namespaces return nothing. If they return results, you have broadly-permissioned default identities.
Token Projection for Service-to-Service Auth
Kubernetes projected tokens with custom audiences can be used for service-to-service authentication within a cluster — not just for cloud provider auth. The pattern:
- Service A requests a projected token with audience set to Service B's expected audience string (e.g.,
inventory-api.prod.internal). - Service A includes this token in the
Authorization: Bearerheader when calling Service B. - Service B validates the token against the Kubernetes OIDC discovery endpoint, verifying the signature, expiry, audience, and the ServiceAccount name in the
subclaim.
This gives you authenticated service-to-service calls without shared secrets or a service mesh. The Kubernetes OIDC discovery endpoint (https://KUBE_APISERVER/.well-known/openid-configuration) serves the public keys needed for validation.
In practice, this pattern is underused because most teams either use service meshes for mTLS (which handles authentication at the transport layer) or don't authenticate service-to-service calls at all (relying on network policies). The projected-token approach is most useful in environments where a full service mesh is too heavy but you need application-layer authentication.
What Kubernetes Identity Doesn't Cover
Kubernetes ServiceAccounts and projected tokens give you identity within the Kubernetes ecosystem. They don't cover:
- Authentication to databases that aren't on cloud-managed services with IAM integration (e.g., self-hosted PostgreSQL, Redis, MongoDB)
- Authentication to third-party SaaS APIs that don't support OIDC federation
- Cross-cluster identity — a pod in cluster A authenticating to a service in cluster B using the same identity framework
- Policy enforcement that goes beyond "is this ServiceAccount allowed to call this Kubernetes API" — i.e., "is this specific workload, running this specific version, allowed to access this external resource right now"
These gaps are where workload identity management tools like Aembit complement the Kubernetes-native identity layer. Kubernetes tells you which ServiceAccount a pod runs under and can issue projected tokens for that ServiceAccount. Aembit takes that attested identity and applies it to access decisions for external systems — issuing just-in-time credentials to the downstream resource rather than requiring the workload to hold a credential for it.
The Kubernetes workload identity layer is a foundation, not a complete solution. Getting the foundation right — one ServiceAccount per workload, minimal RBAC, no auto-mounted tokens for workloads that don't need them, short-lived projected tokens for cloud federation — makes everything built on top of it more secure. Skipping the foundation work and layering external tooling on top of imprecise Kubernetes identity just moves the blast radius without reducing it.