Common Misconfiguration
Using the default service account with automatic token mounting allows pods to authenticate to the Kubernetes API with potentially unnecessary permissions. Default service accounts often accumulate excessive permissions over time.Vulnerable Example
Copy
# Vulnerable: Using default service account with auto-mounting
apiVersion: apps/v1
kind: Deployment
metadata:
name: vulnerable-app
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: vulnerable-app
template:
metadata:
labels:
app: vulnerable-app
spec:
# No serviceAccountName specified - uses 'default'
# automountServiceAccountToken not set to false
containers:
- name: app
image: myapp:latest
# Token is automatically mounted at /var/run/secrets/kubernetes.io/serviceaccount/
---
# Overly permissive RBAC for default service account
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: overpermissive-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin # DANGEROUS: Full cluster access
subjects:
- kind: ServiceAccount
name: default
namespace: production
---
# Another common mistake: broad permissions
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: too-many-permissions
namespace: production
rules:
- apiGroups: ["*"] # All API groups
resources: ["*"] # All resources
verbs: ["*"] # All verbs
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: default-sa-binding
namespace: production
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: too-many-permissions
subjects:
- kind: ServiceAccount
name: default
namespace: production
Secure Example
Copy
# Secure: Custom service account with minimal permissions
# 1. Create dedicated service account
apiVersion: v1
kind: ServiceAccount
metadata:
name: app-sa
namespace: production
annotations:
description: "Service account for production app with minimal permissions"
automountServiceAccountToken: false # Disable automatic token mounting
---
# 2. Create minimal Role with only required permissions
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: app-role
namespace: production
rules:
# Only if the app needs to read ConfigMaps
- apiGroups: [""]
resources: ["configmaps"]
resourceNames: ["app-config"] # Specific ConfigMap name
verbs: ["get", "watch"]
# Only if the app needs to read specific Secrets
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["app-secret"] # Specific Secret name
verbs: ["get"]
---
# 3. Bind the Role to the ServiceAccount
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: app-rolebinding
namespace: production
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: app-role
subjects:
- kind: ServiceAccount
name: app-sa
namespace: production
---
# 4. Use the service account in Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: secure-app
namespace: production
labels:
app: secure-app
spec:
replicas: 3
selector:
matchLabels:
app: secure-app
template:
metadata:
labels:
app: secure-app
spec:
serviceAccountName: app-sa
automountServiceAccountToken: false # Explicitly disable if not needed
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 2000
containers:
- name: app
image: myapp:1.2.3
imagePullPolicy: IfNotPresent
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
limits:
memory: "256Mi"
cpu: "500m"
requests:
memory: "128Mi"
cpu: "250m"
---
# 5. For pods that need API access, mount token explicitly
apiVersion: v1
kind: Pod
metadata:
name: api-client
namespace: production
spec:
serviceAccountName: app-sa
automountServiceAccountToken: true # Only when necessary
containers:
- name: client
image: api-client:1.0.0
volumeMounts:
- name: token
mountPath: /var/run/secrets/tokens
readOnly: true
volumes:
- name: token
projected:
sources:
- serviceAccountToken:
path: token
expirationSeconds: 3600 # Short-lived token
audience: api-server # Specific audience
Service Account for Different Use Cases
Copy
# 1. Read-only service account for monitoring
apiVersion: v1
kind: ServiceAccount
metadata:
name: monitoring-sa
namespace: monitoring
automountServiceAccountToken: false
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: monitoring-reader
rules:
- apiGroups: [""]
resources: ["pods", "nodes", "namespaces", "services", "endpoints"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments", "daemonsets", "statefulsets"]
verbs: ["get", "list", "watch"]
- apiGroups: ["metrics.k8s.io"]
resources: ["pods", "nodes"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: monitoring-reader-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: monitoring-reader
subjects:
- kind: ServiceAccount
name: monitoring-sa
namespace: monitoring
---
# 2. Service account for CI/CD with namespace-limited deployment rights
apiVersion: v1
kind: ServiceAccount
metadata:
name: cicd-deployer
namespace: production
automountServiceAccountToken: false
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: cicd-deployer-role
namespace: production
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
- apiGroups: [""]
resources: ["services"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list"] # Read-only for secrets
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: cicd-deployer-binding
namespace: production
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: cicd-deployer-role
subjects:
- kind: ServiceAccount
name: cicd-deployer
namespace: production
---
# 3. No permissions service account for apps that don't need API access
apiVersion: v1
kind: ServiceAccount
metadata:
name: no-permissions-sa
namespace: production
annotations:
description: "Service account with no RBAC permissions for apps that don't need API access"
automountServiceAccountToken: false
# No Role or RoleBinding created - this SA has no permissions
Audit and Remediation Script
Copy
# ConfigMap with audit script
apiVersion: v1
kind: ConfigMap
metadata:
name: sa-audit-script
namespace: security
data:
audit.sh: |
#!/bin/bash
echo "=== Service Account Security Audit ==="
echo ""
# Check for pods using default service account
echo "Pods using default service account:"
kubectl get pods -A -o json | jq -r '.items[] |
select(.spec.serviceAccountName == "default" or .spec.serviceAccountName == null) |
"\(.metadata.namespace)/\(.metadata.name)"'
echo ""
# Check for service accounts with automount enabled
echo "Service accounts with automount enabled:"
kubectl get serviceaccounts -A -o json | jq -r '.items[] |
select(.automountServiceAccountToken != false) |
"\(.metadata.namespace)/\(.metadata.name)"'
echo ""
# Check for overly permissive RBAC
echo "ClusterRoleBindings with cluster-admin:"
kubectl get clusterrolebindings -o json | jq -r '.items[] |
select(.roleRef.name == "cluster-admin") |
.metadata.name'
echo ""
# Check for wildcard permissions
echo "Roles with wildcard permissions:"
kubectl get roles,clusterroles -A -o json | jq -r '.items[] |
select(.rules[]? | .apiGroups[]? == "*" or .resources[]? == "*" or .verbs[]? == "*") |
"\(.metadata.namespace // "cluster")/\(.metadata.name)"'
---
# CronJob to run audit regularly
apiVersion: batch/v1
kind: CronJob
metadata:
name: sa-audit
namespace: security
spec:
schedule: "0 2 * * *" # Daily at 2 AM
jobTemplate:
spec:
template:
spec:
serviceAccountName: audit-sa
automountServiceAccountToken: true
containers:
- name: audit
image: bitnami/kubectl:latest
command: ["/bin/bash", "/scripts/audit.sh"]
volumeMounts:
- name: script
mountPath: /scripts
volumes:
- name: script
configMap:
name: sa-audit-script
defaultMode: 0755
restartPolicy: OnFailure
Advanced Policy Enforcement (Admission Webhook)
Copy
# Admission controller to enforce service account policies
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: service-account-validator
webhooks:
- name: validate-service-accounts.security.io
clientConfig:
service:
name: sa-validator
namespace: security
path: "/validate"
caBundle: LS0tLS1CRUdJTi... # Base64 encoded CA cert
rules:
- operations: ["CREATE", "UPDATE"]
apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
admissionReviewVersions: ["v1", "v1beta1"]
sideEffects: None
failurePolicy: Fail
namespaceSelector:
matchLabels:
enforce-sa-policy: "true"

