🎉 DevOps Interview Prep Bundle is live — 1000+ Q&A across 20 topicsGet it →
All Articles

What Is a Kubernetes Service Account Token and How Does It Work?

Kubernetes service account tokens explained — what they are, how pods use them for API access, the difference between legacy tokens and projected tokens, and how IRSA works on AWS EKS.

Shubham5 min read
Share:Tweet

If you have ever run kubectl exec into a pod and found a file at /var/run/secrets/kubernetes.io/serviceaccount/token, you have seen a Kubernetes service account token without necessarily knowing what it was or why it was there.

Service account tokens are how pods authenticate to the Kubernetes API server — and increasingly, to external services like AWS via IRSA. Understanding how they work is essential for both security and debugging.

What Is a Service Account?

A Service Account is a Kubernetes object that gives a pod an identity. Think of it as the cluster's equivalent of a user account, but for workloads rather than humans.

Every namespace has a default service account. If you do not specify a service account for your pod, it gets the default one automatically.

bash
# See service accounts in a namespace
kubectl get serviceaccounts -n production
 
# Describe a specific one
kubectl describe serviceaccount my-app -n production

Pods use service accounts to:

  1. Authenticate to the Kubernetes API server (to read ConfigMaps, create Events, etc.)
  2. Get AWS credentials via IRSA (on EKS)
  3. Get GCP credentials via Workload Identity (on GKE)

How Tokens Work

When a pod runs with a service account, Kubernetes automatically mounts a token into the pod at a well-known path:

bash
# Inside a pod
cat /var/run/secrets/kubernetes.io/serviceaccount/token

This token is a JWT (JSON Web Token) signed by the Kubernetes API server. Applications can send this token in an HTTP Authorization header when calling the Kubernetes API, and the API server validates the signature to confirm the pod's identity.

bash
# From inside a pod, calling the Kubernetes API directly
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
curl -H "Authorization: Bearer $TOKEN" \
     --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
     https://kubernetes.default.svc/api/v1/namespaces/default/configmaps

Legacy Tokens vs Projected Tokens

There are two types of service account tokens, and the difference matters for security.

Legacy tokens (Kubernetes < 1.24): Long-lived tokens stored as Secrets. They never expire. If a pod is compromised and the token is extracted, the attacker has indefinite access.

yaml
# Old style — auto-created Secret for service account
apiVersion: v1
kind: Secret
type: kubernetes.io/service-account-token
metadata:
  name: my-app-token
  annotations:
    kubernetes.io/service-account.name: my-app

Projected tokens (Kubernetes 1.21+, default since 1.24): Short-lived tokens automatically rotated by the kubelet. They have an expiration time (default 1 hour), are audience-bound, and are tied to a specific pod's lifetime. Much more secure.

You can see the projected token configuration in a pod spec:

yaml
volumes:
- name: kube-api-access
  projected:
    sources:
    - serviceAccountToken:
        expirationSeconds: 3607
        path: token
    - configMap:
        name: kube-root-ca.crt
        items:
        - key: ca.crt
          path: ca.crt
    - downwardAPI:
        items:
        - path: namespace
          fieldRef:
            fieldPath: metadata.namespace

The kubelet automatically refreshes this token before it expires, so your application always has a valid token without any action required.

RBAC: What the Token Actually Allows

Having a service account token only proves identity — it does not grant any permissions. Permissions come from RBAC (Role-Based Access Control) bindings.

By default, the default service account has almost no permissions. To give your pod permission to read ConfigMaps:

yaml
# 1. Create a Role with specific permissions
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: configmap-reader
  namespace: production
rules:
- apiGroups: [""]
  resources: ["configmaps"]
  verbs: ["get", "list", "watch"]
---
# 2. Bind the Role to a ServiceAccount
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: read-configmaps
  namespace: production
subjects:
- kind: ServiceAccount
  name: my-app
  namespace: production
roleRef:
  kind: Role
  name: configmap-reader
  apiGroup: rbac.authorization.k8s.io
---
# 3. Use the ServiceAccount in your Pod
apiVersion: v1
kind: Pod
spec:
  serviceAccountName: my-app
  containers:
  - name: app
    image: myapp:latest

IRSA: Using Service Account Tokens for AWS Access

IRSA (IAM Roles for Service Accounts) is the most important use of service account tokens beyond the Kubernetes API. It lets pods get AWS credentials by exchanging a Kubernetes token for AWS credentials — no AWS access keys in Secrets needed.

How it works:

  1. EKS acts as an OIDC identity provider
  2. Kubernetes issues tokens with a specific audience for AWS
  3. AWS IAM validates the token against the OIDC provider
  4. AWS returns temporary credentials for the configured IAM role

Setting it up:

bash
# 1. Create an IAM role with a trust policy for the service account
aws iam create-role \
  --role-name my-app-role \
  --assume-role-policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/EXAMPLE"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "oidc.eks.us-east-1.amazonaws.com/id/EXAMPLE:sub": 
            "system:serviceaccount:production:my-app"
        }
      }
    }]
  }'
 
# 2. Annotate the service account with the role ARN
kubectl annotate serviceaccount my-app \
  -n production \
  eks.amazonaws.com/role-arn=arn:aws:iam::123456789:role/my-app-role

Once annotated, pods using my-app service account automatically get temporary AWS credentials via the projected token mechanism. The AWS SDK picks them up automatically — no code changes needed.

Debugging Service Account Issues

Pod cannot access Kubernetes API:

bash
# Check if service account has the right role binding
kubectl auth can-i list pods \
  --as=system:serviceaccount:production:my-app \
  -n production
 
# Check what the service account can do
kubectl auth can-i --list \
  --as=system:serviceaccount:production:my-app \
  -n production

Pod cannot access AWS (IRSA not working):

bash
# Check the service account annotation
kubectl describe serviceaccount my-app -n production | grep eks.amazonaws.com
 
# Check if the projected token has the right audience
# Inside the pod:
cat /var/run/secrets/eks.amazonaws.com/serviceaccount/token | \
  python3 -c "import sys,json,base64; t=sys.stdin.read().split('.')[1]; print(json.loads(base64.b64decode(t+'==')))"
# Look for "aud": ["sts.amazonaws.com"]

Disable automounting if the pod does not need API access:

yaml
spec:
  automountServiceAccountToken: false  # More secure for pods that don't need it
  containers:
  - name: app
    image: myapp:latest

This is a security best practice for pods that have no reason to call the Kubernetes API — reduces the blast radius if the pod is compromised.


More Kubernetes security? Read our RBAC Kubernetes explained guide and Kubernetes pod security admission explained.

🔧

Today I Fixed

Short real fixes from production — posted daily

Browse fixes
Newsletter

Stay ahead of the curve

Get the latest DevOps, Kubernetes, AWS, and AI/ML guides delivered straight to your inbox. No spam — just practical engineering content.

Related Articles

Comments