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:
- Ze vinden de token op
/var/run/secrets/kubernetes.io/serviceaccount/token - Ze queryen de API server: wat kan ik doen?
- 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:
| Verb | Betekenis |
|---|---|
| get | Lees enkele resource |
| list | Lees meerdere resources |
| watch | Stream wijzigingen |
| create | Maak nieuwe resources |
| update | Vervang bestaande |
| patch | Wijzig bestaande |
| delete | Verwijder resources |
| deletecollection | Verwijder 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.
