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.

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.

Kubernetes 1.24+ change: In Kubernetes 1.24, auto-created Secret-based ServiceAccount tokens were deprecated. Tokens are now short-lived and projected directly into pods via the TokenRequest API. They expire after 1 hour by default and are rotated automatically by the kubelet.

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
Security tip: Do not use the 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 automountServiceAccountToken on workloads that don't need Kubernetes API access.
  • Never grant cluster-admin to 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.