Module 3.4: ServiceAccount Security
Complexity:
[MEDIUM]- Core knowledgeTime to Complete: 25-30 minutes
Prerequisites: Module 3.3: Secrets Management
What You’ll Be Able to Do
Section titled “What You’ll Be Able to Do”After completing this module, you will be able to:
- Assess ServiceAccount configurations for excessive API access and auto-mounted tokens
- Evaluate the risk of default ServiceAccount usage across cluster namespaces
- Identify lateral movement paths enabled by misconfigured ServiceAccount permissions
- Explain bound token volume projection and how it reduces token exposure risks
Why This Module Matters
Section titled “Why This Module Matters”Imagine checking into a hotel and receiving a keycard that doesn’t just open your room, but also grants access to the gym, the manager’s office, and the master server room. Even worse, the card never expires. If you drop it in the lobby, anyone who finds it has permanent VIP access to the entire building.
In Kubernetes, ServiceAccounts are the keycards for your pods, allowing them to authenticate to the Kubernetes API. Historically, every pod was automatically handed a token that never expired, and by default, these accounts often possess far more access than a typical application needs.
Understanding ServiceAccount security is about transitioning to a model of least privilege. You must ensure that each pod is issued a strictly time-limited, audience-bound token that opens only the specific doors it needs—and if a pod doesn’t need to leave its room at all, it shouldn’t be given a keycard in the first place. Misconfigured ServiceAccounts remain one of the most widely exploited attack vectors for lateral movement within compromised clusters.
ServiceAccount Basics
Section titled “ServiceAccount Basics”┌─────────────────────────────────────────────────────────────┐│ SERVICEACCOUNT OVERVIEW │├─────────────────────────────────────────────────────────────┤│ ││ WHAT IS A SERVICEACCOUNT? ││ • Identity for pods to authenticate to API server ││ • Namespace-scoped resource ││ • Every pod has one (default if not specified) ││ ││ HOW IT WORKS: ││ 1. Pod created with serviceAccountName ││ 2. Token projected into pod at /var/run/secrets/... ││ 3. Pod uses token to authenticate API requests ││ 4. API server validates token, extracts identity ││ 5. RBAC checked against ServiceAccount ││ ││ DEFAULT SERVICEACCOUNT: ││ • Every namespace has "default" ServiceAccount ││ • Pods use it if none specified ││ • May have unintended permissions ││ │└─────────────────────────────────────────────────────────────┘ServiceAccount Tokens
Section titled “ServiceAccount Tokens”Token Evolution
Section titled “Token Evolution”┌─────────────────────────────────────────────────────────────┐│ SERVICEACCOUNT TOKEN TYPES │├─────────────────────────────────────────────────────────────┤│ ││ LEGACY TOKENS (pre-1.24) ││ ├── Stored in Secrets ││ ├── Never expire ││ ├── Not audience-bound ││ ├── Auto-mounted to all pods ││ └── SECURITY RISK - avoid ││ ││ BOUND SERVICE ACCOUNT TOKENS (1.24+) ││ ├── JWT tokens signed by API server ││ ├── Time-limited (default 1 hour, configurable) ││ ├── Audience-bound (specific to intended recipient) ││ ├── Projected via volume (not Secret) ││ └── Automatically rotated before expiration ││ ││ TOKEN LOCATION IN POD: ││ /var/run/secrets/kubernetes.io/serviceaccount/ ││ ├── token - The JWT token ││ ├── ca.crt - Cluster CA certificate ││ └── namespace - Pod's namespace ││ │└─────────────────────────────────────────────────────────────┘Stop and think: Every pod gets a ServiceAccount token mounted by default. If most of your pods never call the Kubernetes API, what is the security cost of leaving auto-mounting enabled?
Token Request API
Section titled “Token Request API”Create tokens programmatically:
apiVersion: authentication.k8s.io/v1kind: TokenRequestmetadata: name: my-token namespace: defaultspec: audiences: - api # Who can use this token expirationSeconds: 3600 # 1 hour boundObjectRef: # Optional: bind to specific pod kind: Pod name: my-pod uid: abc-123ServiceAccount Configuration
Section titled “ServiceAccount Configuration”Basic ServiceAccount
Section titled “Basic ServiceAccount”apiVersion: v1kind: ServiceAccountmetadata: name: my-app namespace: productionautomountServiceAccountToken: false # Don't auto-mount tokenUsing ServiceAccount in Pod
Section titled “Using ServiceAccount in Pod”apiVersion: v1kind: Podmetadata: name: my-appspec: serviceAccountName: my-app automountServiceAccountToken: false # Override at pod level containers: - name: app image: myapp:1.0Projected Token (When Needed)
Section titled “Projected Token (When Needed)”apiVersion: v1kind: Podmetadata: name: api-clientspec: serviceAccountName: api-caller containers: - name: app image: myapp:1.0 volumeMounts: - name: token mountPath: /var/run/secrets/tokens readOnly: true volumes: - name: token projected: sources: - serviceAccountToken: path: api-token expirationSeconds: 3600 audience: apiDefault ServiceAccount Issues
Section titled “Default ServiceAccount Issues”┌─────────────────────────────────────────────────────────────┐│ DEFAULT SERVICEACCOUNT RISKS │├─────────────────────────────────────────────────────────────┤│ ││ PROBLEM: ││ • Every namespace has "default" ServiceAccount ││ • Pods use it automatically if not specified ││ • Token auto-mounted to pods ││ • May have roles bound (often more than needed) ││ ││ ATTACK SCENARIO: ││ 1. Attacker compromises application container ││ 2. Reads token from /var/run/secrets/... ││ 3. Uses token to query Kubernetes API ││ 4. Discovers secrets, other pods, escalates ││ ││ MITIGATIONS: ││ • Disable auto-mount for default SA ││ • Create dedicated SAs for each application ││ • Don't bind roles to default SA ││ • Use automountServiceAccountToken: false ││ │└─────────────────────────────────────────────────────────────┘Securing the Default ServiceAccount
Section titled “Securing the Default ServiceAccount”# Disable token mounting on default SAapiVersion: v1kind: ServiceAccountmetadata: name: default namespace: productionautomountServiceAccountToken: falseWar Story: The $50,000 Dashboard Breach
Section titled “War Story: The $50,000 Dashboard Breach”In a real-world incident at a mid-sized tech company, developers deployed an internal monitoring dashboard using the default ServiceAccount in a production namespace. To make setup “easier,” someone had previously bound a ClusterRole with get secrets permissions to this default account.
When an attacker discovered a simple Server-Side Request Forgery (SSRF) vulnerability in the dashboard application, they didn’t need to break out of the container to inflict massive damage. They simply directed the vulnerable application to read the auto-mounted token at /var/run/secrets/kubernetes.io/serviceaccount/token. Using this token, the attacker queried the Kubernetes API, downloaded every Secret in the cluster, extracted cloud provider credentials, and spun up cryptocurrency miners. The result was a $50,000 cloud bill and a frantic, full-scale credentials rotation—all stemming from a leaked token that should never have been mounted in the first place.
Pause and predict: Bound service account tokens expire after 1 hour by default. What happens to a long-running pod when its token expires? Does the pod crash?
Workload Identity
Section titled “Workload Identity”Map Kubernetes ServiceAccounts to cloud provider identities:
┌─────────────────────────────────────────────────────────────┐│ WORKLOAD IDENTITY │├─────────────────────────────────────────────────────────────┤│ ││ WITHOUT WORKLOAD IDENTITY: ││ • Store cloud credentials as K8s Secrets ││ • Long-lived, static credentials ││ • Same credentials for all pods using the Secret ││ • Manual rotation required ││ ││ WITH WORKLOAD IDENTITY: ││ • K8s ServiceAccount → Cloud IAM role ││ • Short-lived, auto-rotated tokens ││ • Per-pod identity ││ • No static credentials ││ ││ IMPLEMENTATIONS: ││ • AWS: IAM Roles for Service Accounts (IRSA) ││ • GCP: Workload Identity ││ • Azure: Workload Identity (formerly AAD Pod Identity) ││ │└─────────────────────────────────────────────────────────────┘AWS IRSA Example
Section titled “AWS IRSA Example”apiVersion: v1kind: ServiceAccountmetadata: name: s3-reader annotations: eks.amazonaws.com/role-arn: arn:aws:iam::123456:role/S3Reader---apiVersion: v1kind: Podmetadata: name: appspec: serviceAccountName: s3-reader containers: - name: app image: myapp:1.0 # AWS SDK automatically uses projected tokenServiceAccount Best Practices
Section titled “ServiceAccount Best Practices”┌─────────────────────────────────────────────────────────────┐│ SERVICEACCOUNT SECURITY CHECKLIST │├─────────────────────────────────────────────────────────────┤│ ││ MINIMIZE ACCESS ││ ☐ Create dedicated SA per application ││ ☐ Don't reuse SAs across different apps ││ ☐ Grant minimal RBAC permissions ││ ☐ Use namespace-scoped roles ││ ││ TOKEN MANAGEMENT ││ ☐ Disable auto-mount when API access not needed ││ ☐ Use bound tokens (short-lived, audience-bound) ││ ☐ Clean up legacy token Secrets ││ ││ CLOUD INTEGRATION ││ ☐ Use workload identity instead of static credentials ││ ☐ Map SAs to cloud roles with least privilege ││ ││ DEFAULT SA ││ ☐ Disable auto-mount on default SA ││ ☐ Don't bind roles to default SA ││ ☐ Explicitly specify SA in all pods ││ │└─────────────────────────────────────────────────────────────┘ServiceAccount Attack Vectors
Section titled “ServiceAccount Attack Vectors”┌─────────────────────────────────────────────────────────────┐│ SERVICEACCOUNT ATTACK SCENARIOS │├─────────────────────────────────────────────────────────────┤│ ││ TOKEN THEFT ││ 1. Compromise container ││ 2. Read /var/run/secrets/.../token ││ 3. Use token to access API ││ Mitigation: automountServiceAccountToken: false ││ ││ PRIVILEGE ESCALATION ││ 1. SA has create pods permission ││ 2. Create privileged pod with same SA ││ 3. Escape to host ││ Mitigation: Don't give SAs create pods permission ││ ││ SECRET EXTRACTION ││ 1. SA has get secrets permission ││ 2. Query API for all secrets ││ 3. Extract credentials ││ Mitigation: Minimal RBAC, namespace isolation ││ ││ LATERAL MOVEMENT ││ 1. SA has list pods permission ││ 2. Discover other applications ││ 3. Target other pods ││ Mitigation: Network policies, minimal RBAC ││ │└─────────────────────────────────────────────────────────────┘Did You Know?
Section titled “Did You Know?”-
Every pod has a ServiceAccount - if you don’t specify one, it uses the
defaultSA in the namespace. -
Bound tokens are JWTs - you can decode them (header + payload) but not forge them without the signing key.
-
Legacy tokens persist - even though Kubernetes 1.24+ uses bound tokens by default, old Secret-based tokens may still exist in your cluster.
-
automountServiceAccountToken can be set at both ServiceAccount and Pod level. Pod-level overrides SA-level.
Common Mistakes
Section titled “Common Mistakes”| Mistake | Why It Hurts | Solution |
|---|---|---|
| Using default SA | Shared, may have roles | Create dedicated SAs |
| Token always mounted | Attack surface even when not needed | Set automountServiceAccountToken: false |
| Static cloud credentials | Long-lived, not auditable | Use workload identity |
| Overprivileged SA | Lateral movement possible | Minimal RBAC |
| Same SA for all apps | Shared identity, shared blast radius | Per-app ServiceAccounts |
-
An attacker compromises a web application pod and finds a ServiceAccount token at
/var/run/secrets/kubernetes.io/serviceaccount/token. The pod uses thedefaultServiceAccount, which has no explicit RBAC bindings. Can the attacker still do damage with this token?Answer
Yes, potentially. Even without explicit bindings, the default ServiceAccount can perform API discovery (listing API groups and resources). The attacker can enumerate the cluster's API surface, check their permissions with `kubectl auth can-i --list`, and discover any system:authenticated bindings that grant additional permissions. In some clusters, default ServiceAccounts have been given unintended access through broad ClusterRoleBindings. The token also reveals the cluster's internal DNS and API server address, aiding further reconnaissance. Prevention: set `automountServiceAccountToken: false` on pods that don't need API access. -
Your cluster was upgraded from Kubernetes 1.23 to 1.26. A security scan reveals 200+ legacy ServiceAccount token Secrets still exist. Why are these more dangerous than the bound tokens used by current pods, and how would you remediate?
Answer
Legacy tokens are dangerous because they never expire (valid indefinitely), are not audience-bound (usable against any API), and persist even after the pod that created them is deleted — they remain as Secret objects. If any were leaked (through etcd access, RBAC over-permission, or backup exposure), the attacker has permanent API access. Remediation: identify all legacy token Secrets (`kubectl get secrets --field-selector type=kubernetes.io/service-account-token`), check if any running workloads still reference them via volume mounts, migrate those workloads to use projected bound tokens, then delete the legacy Secret objects. -
A team stores AWS credentials as Kubernetes Secrets for their pods to access S3. You recommend switching to IRSA (IAM Roles for Service Accounts). They push back: “What’s wrong with Secrets? They work fine.” Articulate the security advantages of workload identity.
Answer
Static credentials in Secrets are long-lived (never rotate automatically), shared across all pods using that Secret (same blast radius), accessible to anyone with `get secrets` RBAC permission, stored in etcd (vulnerable to etcd compromise), and require manual rotation. IRSA provides: short-lived tokens (auto-rotated, typically 1-hour expiry), per-pod identity (each pod gets its own credential), no static secrets in the cluster, automatic credential management by the cloud provider, and audit trails through IAM CloudTrail logging. If a pod is compromised with IRSA, the stolen token expires quickly; with static credentials, the attacker has indefinite S3 access. -
You set
automountServiceAccountToken: falseon both the ServiceAccount and the Pod spec. But a specific pod in the namespace legitimately needs API access for leader election. How would you grant it a token while keeping other pods token-free?Answer
Create a dedicated ServiceAccount for that pod (e.g., `leader-election-sa`) with `automountServiceAccountToken: false` at the SA level. In the specific pod spec, use a projected volume to explicitly request a bound token: define a `projected` volume with `serviceAccountToken` source, set a short `expirationSeconds` (e.g., 3600), and specify the appropriate `audience`. This gives the pod a time-limited, audience-bound token without auto-mounting. Pair this with a minimal RBAC Role that only grants the verbs and resources needed for leader election (create/get/update on leases). -
During incident response, you discover a pod was compromised and the attacker used its ServiceAccount to create a new privileged pod in the same namespace. Trace the attack chain and identify which controls at each step would have prevented it.
Answer
Attack chain: (1) Application compromised -> (2) Read auto-mounted SA token -> (3) SA had `create pods` permission -> (4) Created privileged pod -> (5) Container escape to host. Prevention at each step: (1) Application security, image scanning; (2) `automountServiceAccountToken: false` would block token access; (3) Minimal RBAC — the SA should not have had `create pods` permission; (4) Pod Security Standards (Baseline/Restricted) enforcement would reject the privileged pod at admission; (5) seccomp/AppArmor would limit escape even if the pod were somehow created. Defense in depth means any single control could have broken this chain.
Hands-On Exercise: ServiceAccount Security Review
Section titled “Hands-On Exercise: ServiceAccount Security Review”Scenario: Review this setup and identify security issues:
# ServiceAccount with too much accessapiVersion: v1kind: ServiceAccountmetadata: name: app-sa namespace: default---apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRoleBindingmetadata: name: app-adminsubjects:- kind: ServiceAccount name: app-sa namespace: defaultroleRef: kind: ClusterRole name: cluster-admin apiGroup: rbac.authorization.k8s.io---apiVersion: v1kind: Podmetadata: name: web-app namespace: defaultspec: # serviceAccountName not specified containers: - name: app image: nginx:1.25Identify the security issues:
Security Issues
-
cluster-admin bound to app-sa
- Full cluster access from any pod using app-sa
- Massive over-privilege
- Fix: Use minimal, namespace-scoped Role
-
ClusterRoleBinding instead of RoleBinding
- Grants cluster-wide permissions
- Fix: Use RoleBinding for namespace scope
-
ServiceAccount in default namespace
- default namespace often not properly secured
- Fix: Use dedicated namespace
-
Pod doesn’t specify serviceAccountName
- Will use
defaultSA, notapp-sa - The app-sa with cluster-admin is unused here
- But
defaultSA might have its own issues
- Will use
-
No automountServiceAccountToken: false
- Token mounted unnecessarily
- nginx doesn’t need API access
- Fix: Add automountServiceAccountToken: false
Secure version:
apiVersion: v1kind: ServiceAccountmetadata: name: nginx-sa namespace: productionautomountServiceAccountToken: false---apiVersion: v1kind: Podmetadata: name: web-app namespace: productionspec: serviceAccountName: nginx-sa automountServiceAccountToken: false containers: - name: app image: nginx:1.25# No RBAC binding needed if pod doesn't access APISummary
Section titled “Summary”ServiceAccount security is about controlling pod identity:
| Aspect | Risk | Mitigation |
|---|---|---|
| Default SA | Shared identity | Create dedicated SAs |
| Token mounting | Attack surface | automountServiceAccountToken: false |
| RBAC | Over-privilege | Minimal, namespace-scoped |
| Cloud access | Static credentials | Use workload identity |
| Legacy tokens | Never expire | Clean up, use bound tokens |
Key principles:
- One ServiceAccount per application
- Disable token mounting unless needed
- Use bound tokens (Kubernetes 1.24+)
- Integrate with cloud workload identity
- Never give more RBAC than necessary
Next Module
Section titled “Next Module”Module 3.5: Network Policies - Controlling pod-to-pod network traffic.