Kubernetes Service Accounts and Workload Identity
Every pod in Kubernetes runs under a ServiceAccount — a namespaced identity that controls what the pod is allowed to do within the Kubernetes API and, through cloud-specific Workload Identity mechanisms like IRSA on EKS and Workload Identity on GKE, what it is allowed to do against cloud provider APIs such as S3, Secrets Manager, or Pub/Sub. Getting ServiceAccount permissions right is the foundation of a least-privilege security posture. This guide covers the complete lifecycle: creating accounts, binding them to RBAC roles, using bound service account tokens, and federating identities to cloud IAM.
Table of Contents
Service Account Basics
A ServiceAccount is a namespaced Kubernetes resource that provides an identity for processes running in pods. Every namespace has a default ServiceAccount created automatically. If a pod does not specify a serviceAccountName, it runs under default.
# List service accounts in a namespace
kubectl get serviceaccounts -n production
# Create a dedicated service account for your app
kubectl create serviceaccount api-server-sa -n production
# Describe to see token references
kubectl describe serviceaccount api-server-sa -n production
# Assign service account to a pod
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
namespace: production
spec:
template:
spec:
serviceAccountName: api-server-sa
containers:
- name: api-server
image: myrepo/api-server:1.4.2
When a pod starts, Kubernetes mounts the ServiceAccount's token as a projected volume at /var/run/secrets/kubernetes.io/serviceaccount/token. The pod can use this token to authenticate to the Kubernetes API server — for example, a controller that watches ConfigMaps to reload its configuration.
Service Account Tokens and Projection
Projected service account tokens (introduced in Kubernetes 1.12, GA in 1.20) are audience-scoped, time-limited JWT tokens. Unlike the old long-lived Secret tokens, they are automatically rotated and cannot be used to impersonate the service account beyond their expiry.
# Explicitly configure a projected token with custom audience and expiry
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
serviceAccountName: api-server-sa
volumes:
- name: kube-api-token
projected:
sources:
- serviceAccountToken:
path: token
expirationSeconds: 3600
audience: kubernetes.default.svc
- name: aws-token
projected:
sources:
- serviceAccountToken:
path: token
expirationSeconds: 86400
audience: sts.amazonaws.com # For IRSA
containers:
- name: api-server
image: myrepo/api-server:1.4.2
volumeMounts:
- name: aws-token
mountPath: /var/run/secrets/eks.amazonaws.com/serviceaccount
readOnly: true
# Create a short-lived token manually (for testing)
kubectl create token api-server-sa -n production --duration=1h
# Decode the token to inspect its claims
kubectl create token api-server-sa -n production | \
cut -d. -f2 | base64 -d 2>/dev/null | jq .
Binding Service Accounts to RBAC Roles
A ServiceAccount on its own grants no permissions. You must bind it to a Role (namespace-scoped) or ClusterRole (cluster-scoped) via a RoleBinding or ClusterRoleBinding.
# Role: allow reading ConfigMaps in the production namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: configmap-reader
namespace: production
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list", "watch"]
---
# RoleBinding: bind the role to our service account
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: api-server-configmap-reader
namespace: production
subjects:
- kind: ServiceAccount
name: api-server-sa
namespace: production
roleRef:
kind: Role
name: configmap-reader
apiGroup: rbac.authorization.k8s.io
# Test what a service account can do
kubectl auth can-i get configmaps \
--as=system:serviceaccount:production:api-server-sa \
-n production
# yes
kubectl auth can-i delete pods \
--as=system:serviceaccount:production:api-server-sa \
-n production
# no
Disabling Automount and Least Privilege
By default, Kubernetes mounts a service account token into every pod. For workloads that never need to call the Kubernetes API — which is most application pods — this is unnecessary and slightly increases the attack surface. Disable it at the ServiceAccount level or per pod.
# Disable automount on the ServiceAccount (applies to all pods using it)
apiVersion: v1
kind: ServiceAccount
metadata:
name: no-k8s-api-access
namespace: production
automountServiceAccountToken: false
---
# Or disable per pod, overriding the ServiceAccount setting
apiVersion: v1
kind: Pod
spec:
automountServiceAccountToken: false
serviceAccountName: my-sa
containers:
- name: app
image: myrepo/app:1.0
default ServiceAccount for any workload that needs Kubernetes API access. Create dedicated ServiceAccounts with exactly the permissions needed. The default ServiceAccount in most clusters has no roles bound — but attackers who compromise a pod under default can still use the mounted token to probe the API for information.
Workload Identity on EKS (IRSA)
IAM Roles for Service Accounts (IRSA) allows EKS pods to assume AWS IAM roles without storing credentials as Secrets. The mechanism uses OIDC federation — EKS acts as an OIDC identity provider and AWS STS validates the pod's service account token before issuing temporary credentials.
# 1. Enable OIDC provider for your EKS cluster
eksctl utils associate-iam-oidc-provider \
--cluster my-cluster \
--region us-east-1 \
--approve
# 2. Create IAM role with trust policy
OIDC_PROVIDER=$(aws eks describe-cluster --name my-cluster \
--query "cluster.identity.oidc.issuer" --output text | sed 's|https://||')
cat > trust-policy.json << EOF
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Federated": "arn:aws:iam::123456789:oidc-provider/${OIDC_PROVIDER}"},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"${OIDC_PROVIDER}:sub": "system:serviceaccount:production:api-server-sa",
"${OIDC_PROVIDER}:aud": "sts.amazonaws.com"
}
}
}]
}
EOF
aws iam create-role --role-name api-server-role --assume-role-policy-document file://trust-policy.json
aws iam attach-role-policy --role-name api-server-role \
--policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
# 3. Annotate the ServiceAccount with the IAM role ARN
apiVersion: v1
kind: ServiceAccount
metadata:
name: api-server-sa
namespace: production
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789:role/api-server-role
The AWS SDKs inside the pod automatically detect the IRSA environment variables (AWS_ROLE_ARN and AWS_WEB_IDENTITY_TOKEN_FILE) injected by the EKS Pod Identity Webhook and use them to obtain temporary credentials from STS. No code changes are needed in the application.
Workload Identity on GKE
GKE Workload Identity federates Kubernetes ServiceAccounts with Google Cloud service accounts, allowing pods to call Google Cloud APIs without downloading service account key files.
# Enable Workload Identity on the cluster
gcloud container clusters update my-cluster \
--workload-pool=my-project.svc.id.goog \
--region us-central1
# Create a Google Cloud service account
gcloud iam service-accounts create api-server-gsa \
--project my-project
# Grant the GSA permissions (e.g., Pub/Sub publisher)
gcloud projects add-iam-policy-binding my-project \
--member="serviceAccount:api-server-gsa@my-project.iam.gserviceaccount.com" \
--role="roles/pubsub.publisher"
# Bind the Kubernetes SA to the Google SA
gcloud iam service-accounts add-iam-policy-binding \
api-server-gsa@my-project.iam.gserviceaccount.com \
--role roles/iam.workloadIdentityUser \
--member "serviceAccount:my-project.svc.id.goog[production/api-server-sa]"
# Annotate the Kubernetes ServiceAccount
kubectl annotate serviceaccount api-server-sa \
--namespace production \
iam.gke.io/gcp-service-account=api-server-gsa@my-project.iam.gserviceaccount.com
Auditing Service Account Permissions
Over time, service account permissions accumulate. Regular audits catch over-privileged accounts before they become a security risk.
# List all role bindings for a service account
kubectl get rolebindings,clusterrolebindings -A \
-o json | jq '.items[] | select(.subjects[]?.name=="api-server-sa") | {name:.metadata.name, role:.roleRef.name}'
# Check all permissions granted to a service account
kubectl auth can-i --list \
--as=system:serviceaccount:production:api-server-sa \
-n production
# Find service accounts with cluster-admin binding (critical finding)
kubectl get clusterrolebindings -o json | \
jq '.items[] | select(.roleRef.name=="cluster-admin") | .subjects[]'
Production Security Checklist
- Create a dedicated ServiceAccount per application — never share ServiceAccounts between workloads.
- Disable
automountServiceAccountTokenon workloads that don't need Kubernetes API access. - Never grant
cluster-adminto application ServiceAccounts — scope permissions to the minimum required verbs and resources. - Use IRSA (EKS) or Workload Identity (GKE) instead of long-lived IAM credentials stored as Secrets.
- Rotate any legacy long-lived ServiceAccount Secrets and migrate to projected tokens.
- Audit ClusterRoleBindings quarterly and remove any that grant broad permissions to ServiceAccounts.