Kubernetes-beveiliging: RBAC, Pod-beveiligingsnormen en OPA Gatekeeper
Een standaard en verrassend onveilig Kubernetes-cluster: containers kunnen draaien als root, hostknooppuntbestandssysteem aankoppelen, toegang krijgen tot server-API met token van ServiceAccount met brede machtigingen. In 2025 vindt 68% van de beveiligingsincidenten plaats in omgevingen containerized was eerder te wijten aan configuratiefouten dan aan kwetsbaarheden software (bron: Sysdig Container Security Report 2025). Het goede nieuws: Kubernetes biedt krachtige tools om deze risicovolle opstellingen te elimineren.
Dit artikel behandelt een volledige clusterverharding: RBAC controleren wie kan wat doen met de Kubernetes API, Pod-beveiligingsnormen voorkomen bevoorrechte containers en risicovolle configuraties, e OPA Poortwachter voor implementeer aangepast bedrijfsbeleid zoals “alle afbeeldingen moeten afkomstig zijn intern register" of "geen implementatie zonder resourcelimieten".
Wat je gaat leren
- Kubernetes-autorisatiemodel: RBAC, werkwoorden, bronnen, reikwijdte
- Principe van minste bevoegdheden: Role, ClusterRole, RoleBinding toegepast
- ServiceAccount: hoe u automatisch gekoppelde tokens kunt beperken en IRSA/Workload Identity kunt gebruiken
- Pod-beveiligingsnormen: bevoorrecht, basislijn, beperkt en handhaving op naamruimte
- OPA Gatekeeper: Installatie, ConstraintTemplate in Rego, Constraint
- Gemeenschappelijk beleid: goedgekeurde registerimages, verplichte resourcelimieten, geen root
- Auditregistratie: controleer wie wat doet in het cluster
- Volledige verhardingschecklist
RBAC: Rolgebaseerde toegangscontrole
RBAC (Role-Based Access Control) is het belangrijkste autorisatiemechanisme van Kubernetes. Definieert WHO (Onderwerp: Gebruiker, Groep, ServiceAccount) kan worden uitgevoerd welke actie (werkwoord: ophalen, opsommen, bekijken, creëren, bijwerken, patchen, verwijderen) omhoog welke bron (Bron: pods, implementaties, geheimen) in welke reikwijdte (naamruimte met Role/RoleBinding, of het hele cluster met ClusterRole/ClusterRoleBinding).
Rol en rolbinding voor een ontwikkelteam
# rbac-developer-role.yaml
# Un ruolo per i developer: possono vedere e modificare
# deployment/pod/service nel loro namespace, ma non secrets
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: developer
namespace: team-alpha
rules:
# Deployment: lettura + scaling
- apiGroups: ["apps"]
resources: ["deployments", "replicasets"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
# Pod: lettura + exec + logs
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["pods/log"]
verbs: ["get", "list"]
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create"]
# Service e ConfigMap: accesso completo
- apiGroups: [""]
resources: ["services", "configmaps", "endpoints"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# HPA
- apiGroups: ["autoscaling"]
resources: ["horizontalpodautoscalers"]
verbs: ["get", "list", "watch"]
# Ingress
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: team-alpha-developers
namespace: team-alpha
subjects:
# Gruppo di utenti (gestito da OIDC/SSO)
- kind: Group
name: "team-alpha-devs"
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: developer
apiGroup: rbac.authorization.k8s.io
ClusterRole voor SRE/Admin met beperkte reikwijdte
# rbac-sre-clusterrole.yaml
# SRE: accesso in lettura a tutto il cluster, write solo su namespace specifici
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: cluster-reader
rules:
- apiGroups: ["*"]
resources: ["*"]
verbs: ["get", "list", "watch"]
# Nega esplicita: non puo leggere i secrets (sovrascritta da DENY)
# NOTA: in RBAC Kubernetes non esiste DENY esplicita - usa Gatekeeper per questo
---
# SRE: read su tutto + write limitato ai namespace di produzione
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: sre-cluster-reader
subjects:
- kind: Group
name: "platform-sre"
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: cluster-reader
apiGroup: rbac.authorization.k8s.io
# Verifica i permessi di un utente/serviceaccount
kubectl auth can-i create pods --as=system:serviceaccount:production:api-server-sa
kubectl auth can-i delete secrets --as=developer-user -n production
kubectl auth can-i '*' '*' --as=alice # lista tutto quello che alice puo fare
ServiceAccount: Beperk automatische tokens
Standaard koppelt elke Pod automatisch een geldig ServiceAccount-token. In de peulen dat ze noemen de Kubernetes API niet, dit token is nutteloos maar vergroot het aanvalsoppervlak:
# serviceaccount-minimal.yaml
# ServiceAccount dedicata per ogni applicazione (mai usare default)
apiVersion: v1
kind: ServiceAccount
metadata:
name: api-server-sa
namespace: production
annotations:
# Su AWS EKS: delega i permessi IAM tramite IRSA
eks.amazonaws.com/role-arn: "arn:aws:iam::123456789:role/ApiServerRole"
automountServiceAccountToken: false # non montare il token automaticamente
---
# Deployment che usa la ServiceAccount
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
namespace: production
spec:
template:
spec:
serviceAccountName: api-server-sa
automountServiceAccountToken: false # ridondante ma esplicito
containers:
- name: api-server
image: my-registry/api-server:1.2.0
---
# Se il Pod deve chiamare l'API K8s, usa un token proiettato con scadenza
# invece del token auto-mounted (che non scade mai)
apiVersion: v1
kind: Pod
spec:
serviceAccountName: api-server-sa
volumes:
- name: api-token
projected:
sources:
- serviceAccountToken:
path: token
expirationSeconds: 3600 # scade ogni ora
audience: kubernetes.default.svc
containers:
- name: api-server
volumeMounts:
- name: api-token
mountPath: /var/run/secrets/kubernetes.io/serviceaccount
readOnly: true
Pod-beveiligingsnormen (PSS)
Pod-beveiligingsstandaarden vervangen de verouderde PodSecurityPolicies (verwijderd in K8s 1.25). Ze definiëren drie beveiligingsniveaus die van toepassing zijn op naamruimteniveau via labels:
- Bevoorrecht: geen beperkingen (alleen voor systeemnaamruimten zoals kube-system)
- Basislijnen: voorkomt de meest beruchte en risicovolle configuraties (bevoorrechte containers, hostPath, hostNetwork)
- Beperkt: Huidige best practices voor maximale beveiliging (niet-root, seccomp, mogelijkheden wegvallen)
Elk niveau heeft drie modi: enforce (blokkeert de Pod), audit
(logt maar blokkeert niet), warn (toont een waarschuwing voor de gebruiker).
Pas Pod-beveiligingsnormen toe op naamruimten
# Applica PSS al namespace tramite label
kubectl label namespace production \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/enforce-version=latest \
pod-security.kubernetes.io/audit=restricted \
pod-security.kubernetes.io/audit-version=latest \
pod-security.kubernetes.io/warn=restricted \
pod-security.kubernetes.io/warn-version=latest
# Per namespace di sistema che richiedono pod privilegiati
kubectl label namespace kube-system \
pod-security.kubernetes.io/enforce=privileged
# Testa cosa succederebbe se applicassi restricted a un namespace esistente
kubectl label --dry-run=server --overwrite namespace production \
pod-security.kubernetes.io/enforce=restricted
Pod voldoet aan beperkt niveau
# pod-restricted-compliant.yaml
# Un Pod che rispetta tutte le regole del livello Restricted
apiVersion: v1
kind: Pod
metadata:
name: secure-pod
namespace: production
spec:
securityContext:
runAsNonRoot: true # non girare come root
runAsUser: 1000 # UID non-root
runAsGroup: 3000
fsGroup: 2000
seccompProfile:
type: RuntimeDefault # profilo seccomp di default
supplementalGroups: [1000]
containers:
- name: app
image: my-registry/api-server:1.2.0
securityContext:
allowPrivilegeEscalation: false # fondamentale: previene sudo
readOnlyRootFilesystem: true # filesystem immutabile
capabilities:
drop:
- ALL # rimuovi tutte le Linux capabilities
# add: [] - non aggiungere nessuna capability
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
volumeMounts:
- name: tmp
mountPath: /tmp # se l'app scrive su /tmp, monta un volume
- name: cache
mountPath: /app/cache
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir: {}
automountServiceAccountToken: false
OPA Gatekeeper: beleidsengine voor Kubernetes
De Pod-beveiligingsnormen omvatten een vaste reeks controles. OPA Gatekeeper staat dit toe om aangepast beleid te definiëren met behulp van Rego, de taal van OPA. Het is geïmplementeerd als een Admission Webhook die elk verzoek aan de API-server onderschept en geldig volgens gedefinieerd beleid.
OPA Gatekeeper-installatie
# Installa Gatekeeper con Helm
helm repo add gatekeeper https://open-policy-agent.github.io/gatekeeper/charts
helm repo update
helm install gatekeeper gatekeeper/gatekeeper \
--namespace gatekeeper-system \
--create-namespace \
--version 3.17.0 \
--set auditInterval=30 \
--set constraintViolationsLimit=100 \
--set logLevel=INFO
# Verifica installazione
kubectl get pods -n gatekeeper-system
ConstraintTemplate: alleen afbeeldingen uit goedgekeurd register
# constraint-template-registry.yaml
# Il template definisce lo schema e la logica in Rego
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8sallowedrepos
annotations:
description: "Richiede che le immagini provengano solo da registry approvati"
spec:
crd:
spec:
names:
kind: K8sAllowedRepos
validation:
openAPIV3Schema:
type: object
properties:
repos:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sallowedrepos
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not starts_with_allowed(container.image)
msg := sprintf("Il container '%v' usa l'immagine '%v' che non proviene da un registry approvato", [container.name, container.image])
}
violation[{"msg": msg}] {
container := input.review.object.spec.initContainers[_]
not starts_with_allowed(container.image)
msg := sprintf("L'initContainer '%v' usa l'immagine '%v' non approvata", [container.name, container.image])
}
starts_with_allowed(image) {
repo := input.parameters.repos[_]
startswith(image, repo)
}
---
# Constraint: applica la policy con i parametri specifici
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
metadata:
name: require-approved-registry
spec:
enforcementAction: deny # deny|warn|dryrun
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
excludedNamespaces:
- kube-system
- gatekeeper-system
- monitoring
parameters:
repos:
- "registry.company.internal/"
- "gcr.io/company-project/"
- "public.ecr.aws/company/"
ConstraintTemplate: verplichte resourcelimieten
# constraint-template-resource-limits.yaml
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequiredresources
spec:
crd:
spec:
names:
kind: K8sRequiredResources
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredresources
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not container.resources.limits.cpu
msg := sprintf("Container '%v': cpu limit obbligatorio ma mancante", [container.name])
}
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not container.resources.limits.memory
msg := sprintf("Container '%v': memory limit obbligatorio ma mancante", [container.name])
}
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not container.resources.requests.cpu
msg := sprintf("Container '%v': cpu request obbligatoria ma mancante", [container.name])
}
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredResources
metadata:
name: require-resource-limits
spec:
enforcementAction: deny
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
excludedNamespaces:
- kube-system
- gatekeeper-system
ConstraintTemplate: geen containerhoofdmap
# constraint-template-no-root.yaml
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8spsphostnamespace
spec:
crd:
spec:
names:
kind: K8sPSPHostNamespace
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8spsphostnamespace
violation[{"msg": msg}] {
input.review.object.spec.hostPID == true
msg := "hostPID non e consentito"
}
violation[{"msg": msg}] {
input.review.object.spec.hostIPC == true
msg := "hostIPC non e consentito"
}
violation[{"msg": msg}] {
input.review.object.spec.hostNetwork == true
not input.review.object.metadata.annotations["policy.company.internal/exempt-hostnetwork"]
msg := "hostNetwork non e consentito senza annotation di esenzione"
}
# Verifica violazioni esistenti nel cluster
kubectl get constraints
kubectl describe k8srequiredresources require-resource-limits
# Nella sezione Status.Violations vedrai tutti i Pod non conformi
Auditregistratie
Kubernetes kan elk verzoek aan de API-server loggen in een gestructureerd auditlogboek. Het is essentieel voor compliance- en incidentonderzoek.
# audit-policy.yaml - configurazione dell'audit log
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
# Registra tutto sui Secrets a livello RequestResponse (incluso il contenuto)
- level: RequestResponse
resources:
- group: ""
resources: ["secrets"]
# Registra le modifiche a RBAC
- level: RequestResponse
resources:
- group: "rbac.authorization.k8s.io"
resources: ["roles", "clusterroles", "rolebindings", "clusterrolebindings"]
# Registra exec nei Pod (potenziale accesso malevolo)
- level: Request
resources:
- group: ""
resources: ["pods/exec", "pods/portforward", "pods/proxy"]
# Per tutto il resto: registra solo i metadata (chi ha fatto cosa, quando)
- level: Metadata
omitStages:
- RequestReceived
# Kube-apiserver config (aggiungi all'avvio di kube-apiserver):
# --audit-log-path=/var/log/kubernetes/audit.log
# --audit-log-maxage=30
# --audit-log-maxbackup=10
# --audit-log-maxsize=100
# --audit-policy-file=/etc/kubernetes/audit-policy.yaml
Controlelijst voor het versterken van Kubernetes
Controlelijst voor versterking van de beveiliging
- RBAC: Least privilege-principe voor elke ServiceAccount en gebruikersgroep
- Serviceaccount:
automountServiceAccountToken: falseop alle Pods die de server-API niet aanroepen - Vervaldatum token: gebruik tokens waarmee geprojecteerd wordt
expirationSecondsin plaats van permanente tokens - Pod-beveiligingsnormen:
restrictedin alle productienaamruimten,baselineop enscenering - Geen wortel:
runAsNonRoot: trueeallowPrivilegeEscalation: falseop alle containers - Onveranderlijk bestandssysteem:
readOnlyRootFilesystem: true+ volume lege map voor /tmp en /cache - Resourcelimieten: CPU- en geheugenlimieten op alle containers (Gatekeeper voor handhaving)
- Goedgekeurde registers: Gatekeeper-beperking die afbeeldingen uit ongeautoriseerde registers blokkeert
- Netwerkbeleid: default-deny op alle productienaamruimten (zie artikel 1)
- Geheimen: gebruik externe geheime manager (AWS Secrets Manager, Vault) in plaats van Secret Kubernetes gewoon
- Auditlogboeken: ingeschakeld en gecentraliseerd in een SIEM of logaggregator
- enz.: gecodeerd in rust met encryptieconfiguratie
- Server-API's: anonieme toegang uitgeschakeld, toegangscontroleurs ingeschakeld
Veel voorkomende beveiligingsfouten
- Standaardnaamruimte voor workloads: de standaardnaamruimte heeft standaard permissieve RBAC; Gebruik altijd speciale naamruimten voor uw workloads
- ClusterAdmin voor CI/CD-pijplijnen: GitOps-pijplijnen hebben geen clusterbeheerder nodig; Maak een ServiceAccount met de minimale machtigingen die nodig zijn voor de doelnaamruimte
- Geheim als omgevingsvariabelen: omgevingsvariabelen zijn zichtbaar in
kubectl describe poden in de logboeken; gebruik bestanden die zijn aangekoppeld door Secrets of externe geheimmanagers - Nieuwste tags in afbeeldingen: de nieuwste tag kan veranderen; Gebruik altijd SHA256-digest of onveranderlijke tags om reproduceerbaarheid en veiligheid te garanderen
- Negeer CVE's van basisimages: scan afbeeldingen regelmatig met Trivy, Snyk of Grype; een Ubuntu-basisimage met kritische CVE's annuleert het clusterverhardingswerk
Conclusies en volgende stappen
De beveiliging van een Kubernetes-cluster is een gelaagd proces: RBAC controleert de toegang met de API voorkomt Pod Security Standards risicovolle configuraties op Pod-niveau, en met OPA Gatekeeper kunt u bedrijfsbeleid coderen als versiecode en controleerbaar. Geen van deze instrumenten alleen is voldoende; samen vormen ze één verdediging in de diepte waardoor het aanvalsoppervlak drastisch wordt verkleind.
De volgende stap om de beveiliging verder te versterken en een systeem te implementeren runtime-beveiligingstools zoals Falco, die containersysteemoproepen in realtime monitoren en alert op afwijkend gedrag (shell geopend in een container in productie, lezen van referentiebestanden, onverwachte netwerkverbindingen).
Aankomende artikelen in de Kubernetes at Scale-serie
Vorige artikelen
Gerelateerde serie
- DevSecOps — beleid-als-code, beveiligingsscans in CI/CD-pijplijnen
- Kubernetes-netwerken — Netwerkbeleid om werklasten te isoleren
- Platformtechniek — beveiliging zoals automatische vangrails voor teams







