Secure Service Account
A comprehensive service account security configuration with disabled auto-mount, projected token volumes, imagePullSecrets, and workload identity annotations.
Overview
This template implements defense-in-depth for Kubernetes service accounts across four layers: disabled automatic token mounting (most pods do not need API access), projected/bound tokens for pods that do need API access (time-limited and audience-bound), imagePullSecrets for private registries, and workload identity annotations for cloud API access without static credentials.
Security threat addressed: By default, every pod receives a long-lived service account token mounted at /var/run/secrets/kubernetes.io/serviceaccount. If an attacker compromises a pod, they can use this token to interact with the Kubernetes API, list secrets, or escalate privileges.
When to use: Apply this to every application in production. Disable token mounting by default, and use projected tokens only for the pods that genuinely need Kubernetes API access.
Threat Model
- Credential theft prevention: Disabling auto-mount removes the most common credential theft vector — stolen SA tokens from compromised containers.
- Time-limited access: Projected tokens expire (default: 1 hour) and are automatically rotated by the kubelet, limiting the window of exploitation.
- Audience binding: Tokens are restricted to a specific API audience, preventing them from being used against other services.
- Zero static credentials: Workload identity (IRSA/GKE Workload Identity) provides cloud API access without storing AWS/GCP access keys in the cluster.
MITRE ATT&CK:
- T1528 — Steal Application Access Token: Mounted SA tokens at
/var/run/secrets/enable API access if a container is compromised. - T1552.007 — Unsecured Credentials: Container API: Default mounted tokens are a prime target for credential theft.
Real-world scenario: An attacker exploits an RCE vulnerability and finds a service account token at the default mount path. With token auto-mount disabled, there is no token to steal. If the pod uses projected tokens, the attacker has at most 1 hour before the token expires.
YAML Source
# STEP 1: ServiceAccount -- Disable automatic token mounting
apiVersion: v1
kind: ServiceAccount
metadata:
name: app-workload-sa
namespace: production
labels:
app.kubernetes.io/name: k8s-security
app.kubernetes.io/part-of: k8s-security-pro
app.kubernetes.io/managed-by: k8s-security-pro
annotations:
# AWS EKS: IAM Roles for Service Accounts (IRSA)
eks.amazonaws.com/role-arn: "arn:aws:iam::123456789012:role/app-workload-role"
# GCP GKE: Workload Identity
iam.gke.io/gcp-service-account: "app-workload@my-project.iam.gserviceaccount.com"
automountServiceAccountToken: false
imagePullSecrets:
- name: registry-credentials
---
# STEP 2: Pod with projected (bound) service account token
apiVersion: v1
kind: Pod
metadata:
name: app-with-api-access
namespace: production
labels:
app.kubernetes.io/name: k8s-security
app.kubernetes.io/part-of: k8s-security-pro
app.kubernetes.io/managed-by: k8s-security-pro
app: api-consumer
spec:
serviceAccountName: app-workload-sa
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: registry.example.com/app:v1.2.3
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
resources:
requests:
memory: "64Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
env:
- name: KUBERNETES_TOKEN_PATH
value: /var/run/secrets/tokens/api-token
volumeMounts:
- name: api-token
mountPath: /var/run/secrets/tokens
readOnly: true
- name: tmp
mountPath: /tmp
volumes:
- name: api-token
projected:
defaultMode: 0440
sources:
- serviceAccountToken:
expirationSeconds: 3600
audience: "https://kubernetes.default.svc"
path: api-token
- configMap:
name: kube-root-ca.crt
items:
- key: ca.crt
path: ca.crt
- name: tmp
emptyDir:
sizeLimit: "64Mi"
---
# STEP 3: Minimal RBAC for the ServiceAccount
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: app-workload-role
namespace: production
labels:
app.kubernetes.io/name: k8s-security
app.kubernetes.io/part-of: k8s-security-pro
app.kubernetes.io/managed-by: k8s-security-pro
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: app-workload-binding
namespace: production
labels:
app.kubernetes.io/name: k8s-security
app.kubernetes.io/part-of: k8s-security-pro
app.kubernetes.io/managed-by: k8s-security-pro
subjects:
- kind: ServiceAccount
name: app-workload-sa
namespace: production
roleRef:
kind: Role
name: app-workload-role
apiGroup: rbac.authorization.k8s.io
Installation
kubectl:
kubectl apply -f 05_secure_service_account.yaml
Helm:
helm install k8s-security ./charts/k8s-security -f values-prod.yaml
Kustomize:
kubectl apply -k kustomize/overlays/prod
Verification
# Verify ServiceAccount has automount disabled
kubectl get sa app-workload-sa -n production -o jsonpath='{.automountServiceAccountToken}'
# Expected: false
# Verify pod does not have default token mounted
kubectl exec -n production app-with-api-access -- ls /var/run/secrets/kubernetes.io/serviceaccount 2>&1
# Expected: No such file or directory
# Verify projected token is mounted
kubectl exec -n production app-with-api-access -- ls /var/run/secrets/tokens/
# Expected: api-token ca.crt
# Check pods using default service account (should be zero in production)
kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"/"}{.metadata.name}{"\t"}{.spec.serviceAccountName}{"\n"}{end}' | grep "default"
CIS Benchmark References
- 5.1.5 — Ensure that default service accounts are not actively used. This template creates dedicated service accounts per application.
- 5.1.6 — Ensure that Service Account Tokens are only mounted where necessary. This template disables auto-mount and uses projected tokens only when needed.
MITRE ATT&CK References
- T1528 — Steal Application Access Token: Mounted tokens at default paths are the primary target for credential theft. Disabling auto-mount removes this vector entirely.
- T1552.007 — Unsecured Credentials: Container API: Long-lived SA tokens are accessible at a well-known path inside every container. Projected tokens with 1-hour expiry limit the exploitation window.
Further Reading
- Kubernetes RBAC Best Practices: Least Privilege Done Right — Learn about service account hardening, projected tokens, workload identity, and common RBAC mistakes to avoid.