Kubernetes Service Accounts Guide — Pod Identity and Access
In this tutorial, you'll learn about Kubernetes Service Accounts Guide. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
A Kubernetes Service Account provides an identity for pods to authenticate with the Kubernetes API server and access cloud provider resources, separate from user accounts.
What You'll Learn
You'll master Service Accounts — creating and managing accounts, token-based authentication, mounting credentials in pods, binding RBAC roles, automount control, and workload identity federation for cloud access.
Why This Problem Matters
Every pod that interacts with the Kubernetes API (operators, CI/CD runners, monitoring agents) needs authenticated identity. Using the default Service Account with excessive permissions is a security risk. Dedicated Service Accounts with minimal RBAC are essential for security.
Real-World Use
Doda Browser's CI/CD pipeline uses a dedicated Service Account for deployment operations. The account has permissions only to create/update Deployments and Services in specific namespaces. The default Service Account in the namespace has no API access.
Service Account Architecture
flowchart TB
subgraph Kubernetes
SA[ServiceAccount]
Token[JSON Web Token]
Secret[API Secret]
Role[Role/ClusterRole]
Binding[RoleBinding]
end
subgraph Pod
App[Application]
Volume[Token Volume
mounted at
/var/run/secrets/kubernetes.io/serviceaccount]
end
subgraph External
K8sAPI[Kubernetes API]
CloudAPI[Cloud Provider API
via Workload Identity]
end
SA --> Token
Token --> Secret
Secret --> Volume
Volume --> App
App -->|Authenticate via token| K8sAPI
Role --> Binding
Binding --> SA
SA --> CloudAPI
Creating a Service Account
# service-account.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: deploy-bot
namespace: production
annotations:
description: "CI/CD deployment service account"
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: deploy-role
namespace: production
rules:
- apiGroups: ["apps"]
resources: ["deployments", "statefulsets"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
- apiGroups: [""]
resources: ["services", "configmaps"]
verbs: ["get", "list", "watch", "create", "update"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: deploy-bot-binding
namespace: production
subjects:
- kind: ServiceAccount
name: deploy-bot
namespace: production
roleRef:
kind: Role
name: deploy-role
apiGroup: rbac.authorization.k8s.io
kubectl apply -f service-account.yaml
kubectl get serviceaccount -n production deploy-bot
Expected output:
NAME SECRETS AGE
deploy-bot 0 10s
Using a Service Account in a Pod
apiVersion: v1
kind: Pod
metadata:
name: ci-runner
namespace: production
spec:
serviceAccountName: deploy-bot
automountServiceAccountToken: true
containers:
- name: runner
image: alpine:3.20
command:
- sh
- "-c"
- |
KUBE_TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
KUBE_CA=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
KUBE_NS=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)
curl -s --cacert $KUBE_CA \
-H "Authorization: Bearer $KUBE_TOKEN" \
https://kubernetes.default.svc/api/v1/namespaces/$KUBE_NS/pods
resources:
limits:
cpu: 100m
memory: 128Mi
Python Client with Service Account
import os
from kubernetes import client, config
def get_in_cluster_client():
config.load_incluster_config()
return client.CoreV1Api()
def list_pods_with_sa(v1: client.CoreV1Api, namespace: str = None):
ns = namespace or os.environ.get(
"KUBERNETES_NAMESPACE", "default"
)
pods = v1.list_namespaced_pod(ns)
return pods.items
def create_deployment_with_sa():
config.load_incluster_config()
apps = client.AppsV1Api()
deploy = client.V1Deployment(
metadata=client.V1ObjectMeta(
name="sa-deployed-app",
labels={"app": "sa-demo"}
),
spec=client.V1DeploymentSpec(
replicas=2,
selector={"matchLabels": {"app": "sa-demo"}},
template=client.V1PodTemplateSpec(
metadata=client.V1ObjectMeta(
labels={"app": "sa-demo"}
),
spec=client.V1PodSpec(
service_account_name="deploy-bot",
containers=[
client.V1Container(
name="app",
image="nginx]
)
]
)
)
)
)
apps.create_namespaced_deployment("production", deploy)
# Test that the SA can list pods (but should fail on namespaces)
v1 = get_in_cluster_client()
try:
pods = list_pods_with_sa(v1, "production")
print(f"Success! Found {len(pods)} pods in 'production'")
except client.exceptions.ApiException as e:
print(f"Permission denied: {e.reason}")
try:
pods = list_pods_with_sa(v1, "kube-system")
print(f"Found {len(pods)} pods in 'kube-system'")
except client.exceptions.ApiException as e:
print(f"Expected error: {e.reason}")
Expected output:
Success! Found 3 pods in 'production'
Expected error: Forbidden
The deploy-bot SA has access to pods in production (via RoleBinding), but not in kube-system.
Token Management
# Create a long-lived token secret (Kubernetes <1.24)
# In v1.24+, tokens are auto-generated by the TokenRequest API
# Manually create a token secret
kubectl create token deploy-bot -n production
# Create a token with expiration
kubectl create token deploy-bot -n production --duration=24h
# List token secrets
kubectl get secrets -n production | grep deploy-bot
Expected output:
deploy-bot-token-abc12 kubernetes.io/service-account-token 1 10s
Disabling Auto-Mount
# Disable globally for all pods in a namespace
apiVersion: v1
kind: ServiceAccount
metadata:
name: default
namespace: sensitive
automountServiceAccountToken: false
---
# Disable per pod
apiVersion: v1
kind: Pod
metadata:
name: no-sa
spec:
automountServiceAccountToken: false
containers:
- name: app
image: alpine
command: ["sleep", "3600"]
Workload Identity (Cloud Provider)
# GKE Workload Identity
apiVersion: v1
kind: ServiceAccount
metadata:
name: gcs-reader
namespace: data-pipeline
annotations:
iam.gke.io/gcp-service-account: gcs-reader-sa@project.iam.gserviceaccount.com
---
# AWS IAM Roles for Service Accounts (IRSA)
apiVersion: v1
kind: ServiceAccount
metadata:
name: s3-writer
namespace: data-pipeline
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/s3-writer-role
---
# Azure Workload Identity
apiVersion: v1
kind: ServiceAccount
metadata:
name: blob-reader
namespace: data-pipeline
annotations:
azure.workload.identity/client-id: 12345678-1234-1234-1234-123456789012
RBAC with Service Accounts
# Cluster-level permissions
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: cluster-monitor
rules:
- apiGroups: [""]
resources: ["nodes", "persistentvolumes", "namespaces"]
verbs: ["get", "list", "watch"]
- apiGroups: ["metrics.k8s.io"]
resources: ["pods", "nodes"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: cluster-monitor-binding
subjects:
- kind: ServiceAccount
name: monitoring-agent
namespace: monitoring
roleRef:
kind: ClusterRole
name: cluster-monitor
apiGroup: rbac.authorization.k8s.io
Common Mistakes
1. Using the Default Service Account for Everything
The default Service Account typically has minimal permissions, but many deployments leave it unmounted or with no explicit RBAC. Create dedicated Service Accounts per application with minimal permissions.
2. Overly Permissive ClusterRole
Binding a ClusterRole with wildcard permissions (*) to a Service Account defeats RBAC. Use specific resources and verbs. Grant only get, list, watch for read-only, not create, update, delete, patch.
3. Not Disabling Automount When Unnecessary
If your application doesn't use the Kubernetes API, disable automountServiceAccountToken: false. The mounted token is an attack vector if the container is compromised.
4. Using User Credentials in Pods
Setting KUBECONFIG or ~/.kube/config with user credentials inside a container violates security best practices. Always use Service Account tokens.
5. Hardcoding Namespace in Service Account Name
Service Accounts are namespaced. kubectl run --serviceaccount=deploy-bot uses the SA from the current namespace. Specify namespace explicitly: --namespace production.
6. Forgetting Token Rotation
Service Account tokens should be rotated regularly. In Kubernetes 1.24+, tokens are auto-generated and auto-expired. For older versions, manually rotate tokens or use a token controller.
7. No Audit on Service Account Usage
Without audit logging, you can't detect compromised Service Accounts. Enable audit logging in kube-apiserver and monitor for unusual API calls from Service Accounts.
Practice Questions
1. What is the difference between a user account and a Service Account?
User accounts are for humans (kubectl users, API consumers). Service Accounts are for applications (pods, automated processes). User accounts are global; Service Accounts are namespaced. User accounts require external authentication; Service Accounts are authenticated via tokens.
2. How does a pod authenticate to the Kubernetes API using a Service Account?
The token is automatically mounted at /var/run/secrets/Kubernetes.io/serviceaccount/token. The pod presents this token in the Authorization: Bearer header of API requests, along with the CA certificate from the same directory.
3. What happens to a Service Account token when the Service Account is deleted?
All tokens associated with the Service Account are deleted. Pods using that Service Account will lose API access but continue running. New API calls fail with 401 Unauthorized.
4. How do you grant a Service Account access to a specific cloud resource?
Use workload identity federation: annotate the Service Account with the cloud provider's IAM role (e.g., eks.amazonaws.com/role-arn for AWS, iam.gke.io/gcp-service-account for GCP). The cloud provider's OIDC integration exchanges the Kubernetes token for a cloud identity token.
5. Challenge: Design a least-privilege Service Account strategy for a platform team.
The team manages 50 Microservices across 3 environments (dev, staging, production). Each microservice needs specific API permissions. CI/CD pipelines need deployment permissions. Monitoring needs read-only access cluster-wide. Design a hierarchy of Service Accounts and RBAC bindings with the principle of Least Privilege.
Mini Project: Service Account Permission Tester
import os
import json
from kubernetes import client, config, watch
class SAPermissionTester:
def __init__(self, namespace: str = "default"):
self.namespace = namespace
try:
config.load_incluster_config()
self.v1 = client.CoreV1Api()
self.rbac = client.RbacAuthorizationV1Api()
except config.ConfigException:
self.v1 = None
def test_permissions(self, sa_name: str) -> dict:
results = {}
# Check what RoleBindings reference this SA
try:
bindings = self.rbac.list_namespaced_role_binding(
self.namespace
)
for binding in bindings.items:
for subject in binding.subjects:
if (subject.kind == "ServiceAccount"
and subject.name == sa_name):
role_name = binding.role_ref.name
role = self.rbac.read_namespaced_role(
role_name, self.namespace
)
results["role"] = role_name
results["rules"] = [
{
"resources": r.resources,
"verbs": r.verbs,
"apiGroups": r.api_groups
}
for r in role.rules
]
except Exception as e:
results["error"] = str(e)
return results
def display_permissions(self, sa_name: str):
perms = self.test_permissions(sa_name)
print(f"ServiceAccount: {sa_name}")
print(f"Namespace: {self.namespace}")
print(f"Bound Role: {perms.get('role', 'none')}")
print("Permissions:")
for rule in perms.get("rules", []):
for resource in rule.get("resources", []):
print(f" {resource}: {', '.join(rule.get('verbs', []))}")
tester = SAPermissionTester("production")
# Simulate display without actual cluster
print("ServiceAccount: deploy-bot")
print("Namespace: production")
print("Bound Role: deploy-role")
print("Permissions:")
print(" deployments: get, list, watch, create, update, patch")
print(" statefulsets: get, list, watch, create, update, patch")
print(" services: get, list, watch, create, update")
print(" configmaps: get, list, watch, create, update")
print(" pods: get, list, watch")
Expected output:
ServiceAccount: deploy-bot
Namespace: production
Bound Role: deploy-role
Permissions:
deployments: get, list, watch, create, update, patch
statefulsets: get, list, watch, create, update, patch
services: get, list, watch, create, update
configmaps: get, list, watch, create, update
pods: get, list, watch
FAQ
What's Next
Congratulations on completing this Service Accounts guide! Here's where to go from here:
- Practice daily — Create a dedicated Service Account for each application
- Build a project — Set up workload identity federation for cloud resource access
- Explore related topics — OIDC federation, token request API, Pod Identity, OPA Gatekeeper for SA validation
- Join the community — Share your Service Account strategies and get feedback
Remember: every expert was once a beginner. Keep authenticating!
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro