Applications running on Kubernetes often need to call external services, from object storage and databases to message queues and SaaS APIs. Shipping static keys in Secrets raises risk, increases operational toil, and complicates incident response. Workload identity replaces those long lived credentials with verifiable attributes about the workload and issues short lived tokens on demand. The result is a cleaner trust model, stronger least privilege, and a path to consistent identity across clusters and environments.
Workload identity ties an application to an identity derived from the platform, for example a Kubernetes ServiceAccount, a SPIFFE ID, or an attested claim set from a trusted issuer. The identity is exchanged for a limited credential that is scoped to a task and a timeframe. The verifier evaluates claims such as namespace, service account name, and audience, then returns a token or role that expires quickly. This pattern reduces the blast radius of any single secret and embeds context into access decisions. It also enables per workload authorization without manual key distribution.
The threat model for static credentials is straightforward. Secrets can be copied, rotated late, or mistakenly shared across services. An attacker who finds a persistent API key can use it anywhere until revocation. Workload identity raises the bar by making tokens short lived, binding them to a specific workload, and relying on a trust relationship that can be centrally audited. Breach response is also simpler, because revoking a single role or issuer relationship cuts access quickly and predictably across the fleet.
At the cluster layer, a few prerequisites matter. The Kubernetes API server should expose an OIDC issuer URL that aligns with the cluster identity. Bound service account token volume must be enabled, which provides per pod projected tokens with configurable audiences and lifetimes. Legacy service account secrets should be disabled so tokens are not stored as long lived secrets. RBAC for service accounts should be scoped narrowly, because the service account is now a bridge to external systems. Admission controls should validate that deployments use projected tokens with an explicit audience.
A common pattern is OIDC federation from a Kubernetes service account to an external identity provider that issues temporary credentials. The pod receives a projected service account token from the control plane. The application presents that token to a federation endpoint, which verifies the issuer and claims, and then returns a short lived credential. Different platforms implement the exchange with different APIs, but the mechanics are similar. The core control points are the OIDC issuer URL, the expected audience value, and the claims that uniquely name the workload.
# Kubernetes ServiceAccount and Deployment using projected service account token for OIDC federation
apiVersion: v1
kind: Namespace
metadata:
name: app-payments
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: payments-sa
namespace: app-payments
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: payments-api
namespace: app-payments
spec:
replicas: 2
selector:
matchLabels:
app: payments-api
template:
metadata:
labels:
app: payments-api
spec:
serviceAccountName: payments-sa
containers:
- name: api
image: ghcr.io/example/payments-api:1.2.3
env:
- name: OIDC_TOKEN_FILE
value: /var/run/secrets/oidc/serviceaccount/token
volumeMounts:
- name: oidc-token
mountPath: /var/run/secrets/oidc
readOnly: true
volumes:
- name: oidc-token
projected:
sources:
- serviceAccountToken:
path: serviceaccount/token
audience: sts.cloud-provider.example
expirationSeconds: 3600
Claims must be specific enough to prevent one service from assuming the identity of another. The subject claim typically encodes the namespace and service account. Trust configurations should match on both values, not just the namespace. It is also helpful to scope the audience to the exact federation endpoint so that the same token does not work against unrelated systems. These constraints are the primary guardrails that stop lateral movement across services within the cluster.
The following snippet shows how a role can be trusted by a service account from a specific namespace using OIDC claims. The key elements are the identity provider identifier, the audience value that the projected token must contain, and a subject pattern that matches one service account in one namespace. The permission policy attached to the role should be as narrow as possible to align with the single responsibility of the workload. Expiration for the downstream credential must be short, and the application should handle refresh transparently.
# Terraform IAM role and OIDC trust for a Kubernetes service account on AWS
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.17"
}
}
}
provider "aws" {
region = "us-east-1"
}
data "aws_iam_openid_connect_provider" "cluster" {
url = "https://oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E"
}
resource "aws_iam_role" "payments_writer" {
name = "payments-writer"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Federated = data.aws_iam_openid_connect_provider.cluster.arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E:aud" = "sts.amazonaws.com"
}
StringLike = {
"oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E:sub" = "system:serviceaccount:app-payments:payments-sa"
}
}
}]
})
}
resource "aws_iam_role_policy" "payments_writer_policy" {
role = aws_iam_role.payments_writer.name
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = ["dynamodb:PutItem", "dynamodb:UpdateItem"]
Resource = ["arn:aws:dynamodb:us-east-1:111122223333:table/transactions"]
}]
})
}
Similar trust compositions exist across providers. The external system validates the token issuer and audience, then matches a claim that uniquely names the workload to a role or principal. The mapping might use subject, or a set of custom claims, but the design goal is the same. A single workload in a single namespace can assume one or more roles that are limited to a small set of API actions. This approach scales to many services without shared secrets.
Permission design benefits from separation by capability rather than by team or cluster boundary. Each workload should have its own identity and role with the smallest possible set of actions. When a workload needs to call two systems, it can assume two roles independently and cache them with separate refresh cycles. This keeps blast radius contained and avoids privilege creep as applications evolve. Break the linkage between deployment ownership and permission scope, because that linkage tends to grow permissions over time.
Audience configuration deserves attention. A projected token can carry multiple audiences, but a focused audience reduces accidental reuse. Choose an audience that corresponds to the exact federation endpoint, not a generic value that is shared across systems. If a workload must federate with two different endpoints, configure two projected tokens and hand each integration its specific file path. This simple separation makes audits and incident response clearer.
Short lived tokens change application behavior. The client must refresh credentials proactively, handle token expiry during calls, and expose metrics that show refresh success and error rates. Token lifetimes should be short enough to reduce risk but long enough that normal retry and backoff patterns hide the refresh cost. The projected token from the control plane can rotate without restarting the pod, and exchange clients should watch the file for changes. Applications that currently read a Secret at start time need a small refactor so that the credential path is re read periodically.
When diagnosing trust issues, it helps to inspect the projected service account token in a running pod. The token is a JWT with base64url encoded segments. Decoding the claims confirms the issuer, audience, subject, and expiry. This aids quick verification of trust policies and claim shapes without guessing. The example below shows a simple way to read and decode the payload segment.
# Inspect a projected service account token inside a pod to verify claims
TOKEN_FILE=${OIDC_TOKEN_FILE:-/var/run/secrets/oidc/serviceaccount/token}
PAYLOAD=$(cut -d '.' -f2 "$TOKEN_FILE" | tr '_-' '/+' | base64 -d 2>/dev/null)
if [ -z "$PAYLOAD" ]; then
echo "Failed to decode token payload" >&2
exit 1
fi
echo "$PAYLOAD" | jq '. | {iss, aud, sub, exp, iat}'
Multi cluster and multi environment setups add another dimension to identity design. Roles should be bound to a specific cluster issuer where possible, so a token from one cluster cannot assume a role in another environment. Namespaces should follow a consistent naming model so that subject claims are predictable but not interchangeable across environments. Enforce a clear separation of trust domains for development, test, and production by using distinct issuers or provider instances. This avoids accidental cross environment access through copy pasted configurations.
For internal service to service authentication, SPIFFE and SPIRE provide a strong option. Instead of exchanging a JWT for an external role, workloads receive X509 or JWT SVIDs that represent their identity. Mutual TLS can then authenticate and authorize calls between services based on these identities. This can coexist with OIDC federation to external systems, because the two serve different purposes. SPIFFE improves intra cluster and cross cluster workload authentication, while OIDC federation focuses on external API access without static keys.
Migrating from static secrets to workload identity is best approached as a series of small steps. Introduce identity in read only or shadow mode by wiring the application to request a token while still using the existing key. Confirm that exchanges succeed and that permission scopes are sufficient using logs and metrics. Flip the application to use the federated credential for write paths, keep the static key for rollback, then remove the key after an observed burn in period. Document the new runbook for rotations and incident response, and make sure on call engineers can perform a manual credential revocation if needed.
Observability is essential for day two operations. Emit structured logs for each token exchange, including the audience, subject, and expiry time. Track refresh latency and error rates, and alert on sustained failures or unexpected issuers. Correlate external authorization failures with the workload identity to distinguish permission gaps from application bugs. These signals make permission tuning safe and evidence based.
Admission control and policy help keep the configuration consistent as teams scale. Require the use of projected service account tokens with explicit audiences. Block deployments that mount legacy service account secrets. Restrict hostPath mounts that could expose token files outside the expected path. Validate that service accounts intended for federation are created only in approved namespaces and with predictable names, so trust rules remain simple and auditable.
For regulated environments, keep an inventory of trust relationships across clusters and providers. Each relationship should be traceable from a workload to the exact set of actions it can perform. Changes to identity binding and permission scope should flow through the same review and approval pipeline as application code. This aids audits and reduces surprises during penetration tests or incident reviews.
Workload identity aligns Kubernetes with modern identity practices, trading static credentials for short lived tokens bound to attested workload attributes. The result is simpler key management, stronger least privilege, and clearer incident response. With careful attention to issuer configuration, audience scoping, and claim based trust, teams can adopt the pattern incrementally and safely. The operational payoff is significant, because identity shifts from scattered secrets to a consistent, enforceable control plane.
References
Kubernetes Service Account Configuration https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/
AWS EKS IAM Roles for Service Accounts https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html
Azure AKS Workload Identity https://learn.microsoft.com/en-us/azure/aks/workload-identity-overview
GKE Workload Identity Federation https://cloud.google.com/kubernetes-engine/docs/concepts/workload-identity
SPIFFE Specification and Overview https://spiffe.io/docs/latest/spiffe-about/overview/