Skip to main content

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

# 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

# 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

# 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

# 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)

# 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"