How to Manage ServiceAccounts in Kubernetes

How to Manage ServiceAccounts in Kubernetes

Every Pod running in a Kubernetes cluster authenticates to the API server as something, and if you never explicitly created that something, it’s the default ServiceAccount in whatever namespace the Pod lives in — a fact that has quietly caused more privilege-escalation incidents than almost any other Kubernetes default. Understanding and deliberately managing ServiceAccounts is one of the highest-leverage security practices available in the platform.

What a ServiceAccount Actually Is

A ServiceAccount is an identity for processes running inside Pods — distinct from User accounts, which represent humans. When a Pod runs, the kubelet mounts a token for its assigned ServiceAccount into the container filesystem, and anything inside that container can use that token to talk to the Kubernetes API with whatever permissions have been bound to that ServiceAccount via RBAC.

The Default Behavior (and Why It’s Risky)

Every namespace gets a default ServiceAccount automatically:

kubectl get serviceaccount -n production
NAME      SECRETS   AGE
default   0         30d

Any Pod created without an explicit serviceAccountName uses this one. By itself, default has no RBAC bindings and thus no permissions — but if anything in the namespace ever binds a Role to default (a common shortcut during quick debugging), every single unlabeled Pod in that namespace inherits those permissions. This is the most common way clusters end up with far broader effective access than anyone intended.

Creating a Dedicated ServiceAccount

apiVersion: v1
kind: ServiceAccount
metadata:
  name: myapp-sa
  namespace: production
kubectl apply -f myapp-sa.yaml

Binding Permissions with RBAC

A ServiceAccount with no RoleBinding can do essentially nothing beyond authenticate. Grant only what’s needed:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: configmap-reader
  namespace: production
rules:
  - apiGroups: [""]
    resources: ["configmaps"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: myapp-configmap-reader
  namespace: production
subjects:
  - kind: ServiceAccount
    name: myapp-sa
    namespace: production
roleRef:
  kind: Role
  name: configmap-reader
  apiGroup: rbac.authorization.k8s.io
kubectl apply -f rbac.yaml

Assigning the ServiceAccount to a Pod

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
  namespace: production
spec:
  replicas: 2
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      serviceAccountName: myapp-sa
      automountServiceAccountToken: true
      containers:
        - name: myapp
          image: registry.example.com/myapp:1.0.0

Disabling Auto-Mounted Tokens Where Not Needed

Most application Pods never actually call the Kubernetes API at all — for those, auto-mounting a token is pure unnecessary attack surface:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: myapp-sa
  namespace: production
automountServiceAccountToken: false

This can also be set at the Pod level to override the ServiceAccount default:

spec:
  serviceAccountName: myapp-sa
  automountServiceAccountToken: false

Verifying Effective Permissions

kubectl auth can-i get configmaps \
  --as=system:serviceaccount:production:myapp-sa \
  -n production
yes
kubectl auth can-i delete deployments \
  --as=system:serviceaccount:production:myapp-sa \
  -n production
no

This --as impersonation check is the single most useful command for auditing whether a ServiceAccount actually has the access you think it has, before something goes wrong in production.

ClusterRole vs Role: Scope Matters

A Role is namespace-scoped; a ClusterRole applies cluster-wide (or, when bound via a RoleBinding rather than ClusterRoleBinding, can grant cluster-defined permissions within a single namespace). Default to Role unless there’s a specific, justified need for cross-namespace access:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: pod-reader-all-namespaces
rules:
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: myapp-pod-reader
subjects:
  - kind: ServiceAccount
    name: myapp-sa
    namespace: production
roleRef:
  kind: ClusterRole
  name: pod-reader-all-namespaces
  apiGroup: rbac.authorization.k8s.io

Cloud IAM Integration (IRSA, Workload Identity)

Modern clusters increasingly bind ServiceAccounts to cloud IAM identities directly, avoiding long-lived cloud credentials as Kubernetes Secrets entirely. AWS example (IRSA on EKS):

apiVersion: v1
kind: ServiceAccount
metadata:
  name: myapp-sa
  namespace: production
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/myapp-role

GKE Workload Identity follows the same pattern with a different annotation:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: myapp-sa
  namespace: production
  annotations:
    iam.gke.io/gcp-service-account: myapp@my-project.iam.gserviceaccount.com

Token Expiration and Bound Tokens

Modern Kubernetes (1.24+) uses time-bound, audience-scoped tokens by default rather than the old long-lived Secret-based tokens — a meaningful security improvement worth confirming on any cluster inherited from an older version:

kubectl create token myapp-sa -n production --duration=1h

Monitoring ServiceAccount Usage

kubectl get serviceaccounts --all-namespaces
kubectl get rolebindings,clusterrolebindings -A \
  -o jsonpath='{range .items[*]}{.subjects[*].name}{"\n"}{end}' | sort -u

Periodically audit for ServiceAccounts bound to overly broad ClusterRoles — cluster-admin bindings especially deserve scrutiny:

kubectl get clusterrolebindings -o json | \
  jq '.items[] | select(.roleRef.name=="cluster-admin") | .subjects'

ServiceAccounts and Image Pull Secrets

A less-discussed but frequently useful ServiceAccount feature: attaching image pull secrets automatically to every Pod that uses it, rather than repeating imagePullSecrets on every single Pod spec across a namespace:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: myapp-sa
  namespace: production
imagePullSecrets:
  - name: private-registry-creds
kubectl create secret docker-registry private-registry-creds \
  --docker-server=registry.example.com \
  --docker-username=ci \
  --docker-password=$REGISTRY_PASSWORD \
  --namespace production

Any Pod using myapp-sa will now pull from the private registry without needing its own copy of the pull secret reference — a real convenience once a namespace has more than a handful of Deployments pulling from the same private registry.

Projected Volumes for Audience-Scoped Tokens

Beyond the default auto-mounted token, workloads that need to authenticate to something other than the Kubernetes API itself — a service mesh sidecar, a cloud API expecting a specific audience — can request a token scoped to exactly that audience via a projected volume, rather than reusing the general-purpose API server token for an unrelated purpose:

apiVersion: v1
kind: Pod
metadata:
  name: myapp
  namespace: production
spec:
  serviceAccountName: myapp-sa
  containers:
    - name: myapp
      image: registry.example.com/myapp:1.0.0
      volumeMounts:
        - name: vault-token
          mountPath: /var/run/secrets/tokens
  volumes:
    - name: vault-token
      projected:
        sources:
          - serviceAccountToken:
              path: vault-token
              audience: vault
              expirationSeconds: 600

The audience: vault field means this specific token is only valid for whatever service (here, HashiCorp Vault, configured to trust the cluster’s OIDC issuer) is configured to accept that audience — it won’t work against the Kubernetes API server itself, and the general-purpose API token won’t work against Vault. This audience separation is what makes ServiceAccount-based federation with external secret stores and cloud IAM safe: a leaked token is only useful for the one narrow purpose it was actually minted for.

Impersonation for Human Debugging

Beyond kubectl auth can-i --as, full impersonation lets an administrator run commands as a ServiceAccount to reproduce exactly what a workload can and cannot do, which is often faster than reasoning through RBAC YAML by hand:

kubectl get configmaps \
  --as=system:serviceaccount:production:myapp-sa \
  -n production

If this succeeds but the application itself still reports a permissions error, the problem is almost certainly in the application code’s own request path (wrong namespace, wrong resource name) rather than in RBAC — a useful diagnostic split when a “permission denied” bug report comes in.

Aggregated ClusterRoles for Composable Permissions

For platforms exposing extension points to other teams — custom controllers, operators, admission webhooks — aggregated ClusterRoles let a base role automatically absorb rules from other roles matching a label selector, rather than requiring a central team to manually update one giant permissions list every time a new component is added:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: myapp-operator-base
  labels:
    rbac.authorization.k8s.io/aggregate-to-myapp-operator: "true"
aggregationRule:
  clusterRoleSelectors:
    - matchLabels:
        rbac.authorization.k8s.io/aggregate-to-myapp-operator: "true"
rules: []
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: myapp-operator-crds
  labels:
    rbac.authorization.k8s.io/aggregate-to-myapp-operator: "true"
rules:
  - apiGroups: ["myapp.io"]
    resources: ["widgets"]
    verbs: ["get", "list", "watch", "create", "update", "delete"]

Kubernetes’ own view, edit, and admin built-in ClusterRoles use exactly this aggregation mechanism internally, which is precisely why installing a CRD with a correctly labeled ClusterRole automatically makes it visible to anyone already bound to edit or admin — no manual RBAC update required on the consuming side. Understanding this mechanism is genuinely useful for anyone building a platform other teams will extend with their own CRDs and controllers.

A Practical Checklist for Every New Workload

Given everything covered above, a reasonable default checklist worth applying to every new Deployment before it reaches production: create a dedicated ServiceAccount rather than reusing default; set automountServiceAccountToken: false unless the workload genuinely calls the Kubernetes API; bind only the specific verbs and resources actually needed via a namespaced Role, not a ClusterRole, unless cross-namespace access is a genuine requirement; and, where the cloud provider supports it, federate to cloud IAM rather than storing static credentials as Kubernetes Secrets. None of these individually prevents every possible incident, but together they meaningfully shrink what a compromised Pod can actually do — which, in practice, is the entire point of taking ServiceAccount hygiene seriously in the first place.

Common Mistakes

  • Binding permissions to the default ServiceAccount instead of creating a dedicated one, silently granting access to every unlabeled Pod in the namespace.
  • Leaving automountServiceAccountToken: true (the implicit default) on Pods that never call the Kubernetes API at all.
  • Granting cluster-admin to a workload ServiceAccount “temporarily” during debugging and forgetting to revoke it.
  • Using long-lived static cloud credentials in Secrets instead of IRSA/Workload Identity federation, when the cluster’s cloud provider supports it.

Summary

ServiceAccounts are the identity layer every Pod authenticates through, and the difference between a well-scoped ServiceAccount and a careless one is often the difference between a contained incident and a cluster-wide compromise. Create a dedicated ServiceAccount per workload, bind only the permissions actually needed via Role/RoleBinding, disable token auto-mounting where the API is never called, and federate to cloud IAM rather than storing static credentials wherever the platform supports it.

References

  • Kubernetes ServiceAccount documentation: https://kubernetes.io/docs/concepts/security/service-accounts/
  • RBAC documentation: https://kubernetes.io/docs/reference/access-authn-authz/rbac/
  • IAM Roles for Service Accounts (AWS): https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html
  • GKE Workload Identity: https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity
Total
1
Shares

Leave a Reply

Previous Post
How to Set Up a Kubernetes CI/CD Pipeline

How to Set Up a Kubernetes CI/CD Pipeline

Next Post
How to Configure Network Plugins in Kubernetes

How to Configure Network Plugins in Kubernetes

Related Posts