Wanneer alles cluster-admin heeft, is niets veilig.

Kubernetes RBAC (Role-Based Access Control) bestaat om één vraag te beantwoorden: wie kan wat doen met welke resources? De meeste clusters antwoorden verkeerd: “iedereen kan alles doen.”

Dit is niet alleen een security probleem — het is een resilience probleem. Wanneer een service account wordt gecompromitteerd, hoeveel schade kan het aanrichten? Wanneer iemand het verkeerde commando uitvoert, wat is de blast radius?

Least privilege beperkt die radius.

RBAC Bouwstenen

Vier resources definiëren Kubernetes RBAC:

Role — Definieert permissies binnen een namespace

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: production
  name: pod-reader
rules:
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list", "watch"]

ClusterRole — Definieert permissies cluster-breed

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: secret-reader
rules:
  - apiGroups: [""]
    resources: ["secrets"]
    verbs: ["get", "list"]

RoleBinding — Verleent Role permissies aan subjects in een namespace

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: read-pods
  namespace: production
subjects:
  - kind: ServiceAccount
    name: monitoring
    namespace: monitoring
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io

ClusterRoleBinding — Verleent ClusterRole permissies cluster-breed

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: cluster-secret-reader
subjects:
  - kind: Group
    name: security-team
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: secret-reader
  apiGroup: rbac.authorization.k8s.io

Het Probleem: Default Permissies

Kubernetes defaults zijn permissief:

# Hoe veel deployments eruitzien
apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      # Default service account met brede permissies
      serviceAccountName: default
      # Token auto-gemount
      automountServiceAccountToken: true

Het default service account heeft vaak in de loop der tijd permissies verzameld. De auto-gemounte token is beschikbaar voor elk proces in de container.

Als een aanvaller je applicatie compromitteert:

  1. Ze vinden de token op /var/run/secrets/kubernetes.io/serviceaccount/token
  2. Ze queryen de API server: wat kan ik doen?
  3. Ze pivoten door welke permissies er ook bestaan

Principe 1: Dedicated Service Accounts

Gebruik nooit het default service account voor workloads:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: api-server
  namespace: production
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
spec:
  template:
    spec:
      serviceAccountName: api-server
      automountServiceAccountToken: false  # Mount niet tenzij nodig

Elke applicatie krijgt zijn eigen service account. Permissies worden aan dat specifieke account verleend, niet gedeeld.

Principe 2: Token Auto-Mount Uitschakelen

De meeste applicaties hebben geen Kubernetes API toegang nodig:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: web-frontend
automountServiceAccountToken: false
---
apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      serviceAccountName: web-frontend
      automountServiceAccountToken: false  # Dubbele zekerheid

Bij compromitteren kan deze workload de Kubernetes API helemaal niet queryen — er bestaat geen token.

Principe 3: Namespace Isolatie

Roles zijn standaard namespace-scoped. Gebruik dit:

# Role werkt alleen in production namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: production
  name: deployment-manager
rules:
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "list", "watch", "create", "update", "patch"]

Een gecompromitteerde service in production kan staging of andere namespaces niet beïnvloeden.

Vermijd ClusterRoles tenzij echt nodig. De meeste applicaties hebben alleen toegang tot hun eigen namespace nodig.

Principe 4: Specifieke Resources

Verleen toegang tot specifieke resources, niet hele API groups:

# Slecht: toegang tot alle core resources
rules:
  - apiGroups: [""]
    resources: ["*"]
    verbs: ["*"]

# Goed: alleen specifieke benodigde resources
rules:
  - apiGroups: [""]
    resources: ["configmaps"]
    verbs: ["get"]
    resourceNames: ["app-config"]  # Nog specifieker

Het resourceNames veld beperkt toegang tot specifieke benoemde resources.

Principe 5: Minimale Verbs

Verleen alleen benodigde verbs:

VerbBetekenis
getLees enkele resource
listLees meerdere resources
watchStream wijzigingen
createMaak nieuwe resources
updateVervang bestaande
patchWijzig bestaande
deleteVerwijder resources
deletecollectionVerwijder meerdere

Read-only applicaties hebben alleen get, list, watch nodig. De meeste hebben geen delete of deletecollection nodig.

# Monitoring heeft alleen read toegang nodig
rules:
  - apiGroups: [""]
    resources: ["pods", "services", "endpoints"]
    verbs: ["get", "list", "watch"]

Veelvoorkomende Patronen

Applicatie Die ConfigMaps Leest

apiVersion: v1
kind: ServiceAccount
metadata:
  name: config-reader
  namespace: myapp
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: config-reader
  namespace: myapp
rules:
  - apiGroups: [""]
    resources: ["configmaps"]
    verbs: ["get", "watch"]
    resourceNames: ["app-settings"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: config-reader
  namespace: myapp
subjects:
  - kind: ServiceAccount
    name: config-reader
    namespace: myapp
roleRef:
  kind: Role
  name: config-reader
  apiGroup: rbac.authorization.k8s.io

Operator Die Custom Resources Beheert

apiVersion: v1
kind: ServiceAccount
metadata:
  name: myapp-operator
  namespace: myapp-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: myapp-operator
rules:
  # Eigen CRDs
  - apiGroups: ["myapp.example.com"]
    resources: ["myapps", "myapps/status"]
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
  # Resources die het aanmaakt
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
  - apiGroups: [""]
    resources: ["services", "configmaps"]
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: myapp-operator
subjects:
  - kind: ServiceAccount
    name: myapp-operator
    namespace: myapp-system
roleRef:
  kind: ClusterRole
  name: myapp-operator
  apiGroup: rbac.authorization.k8s.io

CI/CD Pipeline Service Account

apiVersion: v1
kind: ServiceAccount
metadata:
  name: gitlab-deployer
  namespace: ci-cd
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: deployer
  namespace: production
rules:
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "list", "watch", "update", "patch"]
  - apiGroups: [""]
    resources: ["configmaps", "secrets"]
    verbs: ["get", "list", "watch", "create", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: gitlab-deployer
  namespace: production
subjects:
  - kind: ServiceAccount
    name: gitlab-deployer
    namespace: ci-cd
roleRef:
  kind: Role
  name: deployer
  apiGroup: rbac.authorization.k8s.io

Let op: Het ServiceAccount is in ci-cd namespace, maar de RoleBinding verleent toegang tot production namespace.

Huidige Permissies Auditen

Check Wat een ServiceAccount Kan Doen

kubectl auth can-i --list --as=system:serviceaccount:production:api-server

# Output:
# Resources                          Non-Resource URLs   Verbs
# configmaps                         []                  [get watch]
# pods                               []                  [get list watch]

Vind Over-Privileged Accounts

# Vind alle ClusterRoleBindings naar cluster-admin
kubectl get clusterrolebindings -o json | jq -r '
  .items[] |
  select(.roleRef.name == "cluster-admin") |
  .metadata.name + ": " + (.subjects // [] | map(.name) | join(", "))
'

Lijst Alle RoleBindings in een Namespace

kubectl get rolebindings -n production -o yaml

Least Privilege Afdwingen met Kyverno

Gebruik Kyverno om RBAC best practices af te dwingen:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-dedicated-service-account
spec:
  validationFailureAction: Enforce
  rules:
    - name: no-default-serviceaccount
      match:
        any:
          - resources:
              kinds:
                - Pod
      validate:
        message: "Gebruik van 'default' service account is niet toegestaan"
        pattern:
          spec:
            serviceAccountName: "!default"
---
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: disable-automount-by-default
spec:
  validationFailureAction: Enforce
  rules:
    - name: disable-automount
      match:
        any:
          - resources:
              kinds:
                - Pod
      validate:
        message: "automountServiceAccountToken moet expliciet op false staan tenzij nodig"
        pattern:
          spec:
            automountServiceAccountToken: false

Geaggregeerde ClusterRoles

Kubernetes ondersteunt role aggregatie voor modulaire permissies:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: monitoring-base
  labels:
    rbac.example.com/aggregate-to-monitoring: "true"
rules:
  - apiGroups: [""]
    resources: ["pods", "services"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: monitoring-extended
  labels:
    rbac.example.com/aggregate-to-monitoring: "true"
rules:
  - apiGroups: ["apps"]
    resources: ["deployments", "replicasets"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: monitoring
aggregationRule:
  clusterRoleSelectors:
    - matchLabels:
        rbac.example.com/aggregate-to-monitoring: "true"
rules: []  # Rules worden automatisch gevuld door aggregatie

Dit laat je roles bouwen uit samenstellbare stukken.

Menselijke Toegang Patronen

Groep-Gebaseerde Toegang

# Developers kunnen de meeste dingen bekijken in hun namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: developers-view
  namespace: development
subjects:
  - kind: Group
    name: developers
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: view  # Ingebouwde read-only role
  apiGroup: rbac.authorization.k8s.io
---
# Platform team heeft edit in alle namespaces
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: platform-team-edit
subjects:
  - kind: Group
    name: platform-team
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: edit  # Ingebouwde edit role
  apiGroup: rbac.authorization.k8s.io

Emergency Break-Glass

# Noodtoegang die zwaar geaudit wordt
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: emergency-admin
  annotations:
    description: "Noodtoegang - al het gebruik wordt geaudit en gereviewed"
subjects:
  - kind: Group
    name: emergency-responders
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: cluster-admin
  apiGroup: rbac.authorization.k8s.io

Combineer met audit logging en alerting wanneer deze binding wordt gebruikt.

Mijn Productie RBAC Strategie

# 1. Schakel default service account tokens cluster-breed uit
apiVersion: v1
kind: ServiceAccount
metadata:
  name: default
  namespace: production
automountServiceAccountToken: false

# 2. Basis read-only role voor alle applicaties
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: app-base
rules:
  - apiGroups: [""]
    resources: ["configmaps"]
    verbs: ["get", "watch"]

# 3. Applicatie-specifieke toevoegingen
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: api-server
  namespace: production
rules:
  - apiGroups: [""]
    resources: ["secrets"]
    verbs: ["get"]
    resourceNames: ["api-credentials", "db-credentials"]

# 4. Gecombineerde binding
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: api-server
  namespace: production
subjects:
  - kind: ServiceAccount
    name: api-server
    namespace: production
roleRef:
  kind: Role
  name: api-server
  apiGroup: rbac.authorization.k8s.io

Belangrijke beslissingen:

  • Default deny — Niets heeft toegang totdat expliciet verleend
  • Namespace isolatie — Applicaties kunnen andere namespaces niet benaderen
  • Benoemde resources — Secrets toegang beperkt tot specifieke secrets
  • Geen wildcards — Elke permissie is expliciet

RBAC Wijzigingen Testen

Voor toepassen naar productie:

# Dry-run: wat zou dit service account kunnen doen?
kubectl auth can-i --list \
  --as=system:serviceaccount:production:api-server

# Test specifieke permissie
kubectl auth can-i get secrets \
  --as=system:serviceaccount:production:api-server \
  -n production

# Test met impersonatie
kubectl get pods -n production \
  --as=system:serviceaccount:production:api-server

Waarom Dit Ertoe Doet

Elke permissie is een aanvalsoppervlak. Elk over-privileged account is een potentiële blast radius.

Wanneer je least privilege implementeert:

  • Gecompromitteerde applicaties kunnen alleen beschadigen waar ze toegang toe hebben
  • Misconfiguraties hebben begrensde impact
  • Audit logs zijn betekenisvol (niet alles toegestaan)
  • Je begrijpt wat elk component daadwerkelijk nodig heeft

Dit is resilience door access control. Niet compromitteren voorkomen, maar zorgen dat het niet cascadeert.


De vraag is niet “welke permissies zou deze applicatie nodig kunnen hebben?” Het is “wat is het minimum vereist om deze applicatie te laten functioneren?” Begin met niets, voeg alleen toe wat bewezen noodzakelijk is.