Kubernetes Hardening
Cloud Snel, Cloud Strak
Cloudomgevingen veranderen snel. Daarom moet beveiliging hier standaard en geautomatiseerd meebewegen.
Voor Kubernetes Hardening is automatisering leidend: guardrails in code, least privilege en continue driftcontrole.
Zo houd je snelheid in de cloud, zonder dat veiligheid afhankelijk wordt van handmatig geluk.
Directe maatregelen (15 minuten)
Waarom dit telt
De kern van Kubernetes Hardening is risicoreductie in de praktijk. Technische context ondersteunt de maatregelkeuze, maar implementatie en borging staan centraal.
RBAC (Role-Based Access Control)
Kubernetes zonder RBAC is als een gebouw waar elke sleutel op elk
slot past. RBAC bepaalt wie wat mag doen in het cluster. Het probleem:
de meeste organisaties configureren RBAC met de subtiliteit van een
voorhamer – cluster-admin aan iedereen, en klaar.
| Type | Scope | Gebruik |
|---|---|---|
| Role | Namespace-gebonden | Toegang tot resources binnen een specifieke namespace |
| ClusterRole | Cluster-breed | Toegang tot cluster-brede resources (nodes, PVs) of herbruikbaar over namespaces |
| RoleBinding | Namespace-gebonden | Koppelt een Role/ClusterRole aan een subject binnen een namespace |
| ClusterRoleBinding | Cluster-breed | Koppelt een ClusterRole aan een subject voor het hele cluster |
Vuistregel: gebruik Role en RoleBinding tenzij je expliciet cluster-brede toegang nodig hebt.
# Wat mag de huidige gebruiker?
kubectl auth can-i --list
# Wat mag een specifiek service account?
kubectl auth can-i --list --as=system:serviceaccount:default:my-app
# Alle ClusterRoleBindings met cluster-admin
kubectl get clusterrolebindings -o json | \
jq '.items[] | select(.roleRef.name=="cluster-admin") |
{name: .metadata.name, subjects: .subjects}'# Restrictieve Role + RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: production
name: pod-reader
rules:
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: read-pods-production
namespace: production
subjects:
- kind: Group
name: "developers"
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.ioVeelgemaakte RBAC-fouten
| Fout | Risico | Oplossing |
|---|---|---|
Wildcard verbs ("*") |
Volledige controle inclusief delete | Specificeer exact welke verbs nodig zijn |
Wildcard resources ("*") |
Toegang tot secrets, configmaps, alles | Specificeer exact welke resources |
cluster-admin aan service accounts |
Container compromise = cluster compromise | Minimale ClusterRole per applicatie |
| Default service account gebruiken | Elke pod in de namespace deelt dezelfde rechten | Dedicated service account per applicatie |
Geen automountServiceAccountToken: false |
Token automatisch in elke pod | Zet op false tenzij de pod de API nodig heeft |
Pod Security Standards
Pod Security Standards (PSS) vervangen de verouderde PodSecurityPolicies. Afdwinging via Pod Security Admission (PSA).
| Niveau | Wat het toestaat | Wanneer gebruiken |
|---|---|---|
| Privileged | Alles – geen restricties | Systeemcomponenten (kube-system) |
| Baseline | Blokkeert bekende privilege escalations | Algemene workloads |
| Restricted | Maximale lockdown | Productie-workloads |
# PSA labels op namespace
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: latest
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restrictedSecurityContext: de juiste instellingen
apiVersion: v1
kind: Pod
metadata:
name: hardened-app
namespace: production
spec:
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: registry.company.nl/app:1.4.2@sha256:abc123...
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
resources:
limits:
memory: "256Mi"
cpu: "500m"
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}| Setting | Waarom |
|---|---|
runAsNonRoot: true |
Voorkomt dat de container als root draait |
readOnlyRootFilesystem: true |
Geen schrijftoegang tot container-filesysteem |
allowPrivilegeEscalation: false |
Blokkeert setuid/setgid en ptrace-escalatie |
capabilities.drop: ALL |
Verwijdert alle Linux capabilities |
seccompProfile: RuntimeDefault |
Blokkeert gevaarlijke syscalls |
Network Policies
Zonder Network Policies kan elke pod met elke andere pod praten. Dat is het standaardgedrag.
# Default deny: blokkeer ALLE ingress en egress
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
---
# Allow: webapp mag naar de database
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-webapp-to-db
namespace: production
spec:
podSelector:
matchLabels:
app: database
ingress:
- from:
- podSelector:
matchLabels:
app: webapp
ports:
- protocol: TCP
port: 5432
---
# Blokkeer cloud metadata service
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-metadata-service
namespace: production
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 169.254.169.254/32| CNI Plugin | Network Policies | Egress | FQDN Policies |
|---|---|---|---|
| Calico | Ja | Ja | Ja (Enterprise) |
| Cilium | Ja | Ja | Ja |
| Weave Net | Ja | Ja | Nee |
| Flannel | Nee | Nee | Nee |
| AWS VPC CNI | Via Calico add-on | Via Calico | Nee |
Let op: Flannel negeert Network Policies stilzwijgend. Geen foutmelding. De policies bestaan in de API maar worden niet afgedwongen.
Secrets management
Kubernetes Secrets zijn base64-gecodeerd. Niet versleuteld. Base64 is geen encryptie.
kubectl get secret db-credentials -o jsonpath='{.data.password}' | base64 -d
# Resultaat: P@ssw0rd123 <-- zo "veilig" zijn Kubernetes SecretsEncryption at rest
# /etc/kubernetes/encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: <base64-encoded-32-byte-key>
- identity: {}External Secrets
| Oplossing | Voordeel | Nadeel |
|---|---|---|
| HashiCorp Vault | Volledige lifecycle, audit trail | Complexe setup |
| AWS Secrets Manager | Native integratie, automatische rotatie | Vendor lock-in |
| Azure Key Vault | Native Azure integratie | Vendor lock-in |
| Sealed Secrets (Bitnami) | Secrets veilig in git | Geen rotatie, cluster-gebonden |
# External Secrets Operator: secret uit Vault
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
namespace: production
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: db-credentials
data:
- secretKey: password
remoteRef:
key: secret/data/production/database
property: passwordImage security
# Kyverno: blokkeer images van niet-goedgekeurde registries
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: restrict-image-registries
spec:
validationFailureAction: Enforce
rules:
- name: validate-registries
match:
any:
- resources:
kinds:
- Pod
validate:
message: "Images mogen alleen van goedgekeurde registries komen."
pattern:
spec:
containers:
- image: "registry.company.nl/* | europe-docker.pkg.dev/company/*"# Trivy: scan en blokkeer bij CRITICAL findings
trivy image --exit-code 1 --severity CRITICAL registry.company.nl/app:1.4.2
# Grype: alternatieve scanner
grype registry.company.nl/app:1.4.2 --fail-on critical| Praktijk | Waarom |
|---|---|
Nooit latest gebruiken |
Reproduceerbaarheid, audit trail |
Digest pinning (@sha256:...) |
Voorkomt tag overwriting (supply chain) |
imagePullPolicy: Always |
Voorkomt stale cached images |
| Signed images (Cosign) | Garandeert herkomst en integriteit |
Admission Controllers
| Feature | OPA Gatekeeper | Kyverno |
|---|---|---|
| Taal | Rego (eigen taal) | YAML (native K8s) |
| Leercurve | Steil | Laag |
| Mutatie | Beperkt | Ja |
| Generatie | Nee | Ja |
# Kyverno: vereis resource limits + blokkeer privileged
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-resource-limits
spec:
validationFailureAction: Enforce
rules:
- name: check-limits
match:
any:
- resources:
kinds:
- Pod
validate:
message: "Containers moeten CPU en memory limits hebben."
pattern:
spec:
containers:
- resources:
limits:
memory: "?*"
cpu: "?*"
---
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: disallow-privileged
spec:
validationFailureAction: Enforce
rules:
- name: no-privileged
match:
any:
- resources:
kinds:
- Pod
validate:
message: "Privileged containers zijn niet toegestaan."
deny:
conditions:
any:
- key: "{{ request.object.spec.containers[].securityContext.privileged }}"
operator: AnyIn
value: [true]etcd-beveiliging
etcd is de database van Kubernetes. Wie toegang heeft tot etcd, heeft alles: secrets, RBAC, de volledige cluster state.
| Maatregel | Implementatie | Waarom |
|---|---|---|
| TLS client certs | --client-cert-auth=true, --cert-file,
--key-file |
Alleen geauthenticeerde clients |
| Peer TLS | --peer-client-cert-auth=true |
etcd nodes authenticeren elkaar |
| Firewall | Alleen TCP 2379/2380 van API server | Niemand anders hoeft bij etcd |
| Backup-encryptie | etcdctl snapshot save + GPG/AES |
Backups bevatten secrets in plaintext |
| Aparte nodes | etcd op dedicated machines | Reduceer aanvalsoppervlak |
Audit logging
# /etc/kubernetes/audit-policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: None
nonResourceURLs: ["/healthz*", "/readyz*", "/livez*"]
- level: Metadata
resources:
- group: ""
resources: ["secrets", "configmaps"]
- level: RequestResponse
resources:
- group: "rbac.authorization.k8s.io"
resources: ["clusterroles", "clusterrolebindings", "roles", "rolebindings"]
- level: RequestResponse
resources:
- group: ""
resources: ["pods/exec", "pods/attach", "pods/portforward"]
- level: Metadata
omitStages: ["RequestReceived"]# API server flags
# --audit-policy-file=/etc/kubernetes/audit-policy.yaml
# --audit-log-path=/var/log/kubernetes/audit.log
# --audit-log-maxage=30
# --audit-webhook-config-file=/etc/kubernetes/audit-webhook.yaml (SIEM)| Audit Event | Betekenis | Prioriteit |
|---|---|---|
kubectl exec in productie |
Interactieve shell in pod | Hoog |
| Secret GET door onbekend SA | Credential theft | Kritiek |
| ClusterRoleBinding aangemaakt | Privilege escalation | Hoog |
Pod met hostPID: true |
Container escape | Kritiek |
Veelvoorkomende fouten
| # | Fout | Impact | Oplossing |
|---|---|---|---|
| 1 | cluster-admin voor applicatie SA’s |
Cluster takeover bij pod compromise | Minimale Role per applicatie |
| 2 | Geen Network Policies | Laterale beweging tussen alle pods | Default deny + expliciete allow |
| 3 | Containers als root | Privilege escalation | runAsNonRoot: true |
| 4 | Secrets in env vars | Zichtbaar via kubectl describe pod |
Volume mounts of External Secrets |
| 5 | latest tag op images |
Supply chain risico | Versie + digest pinning |
| 6 | Geen resource limits | DoS op het cluster | Verplichten via admission controller |
| 7 | etcd zonder TLS | Cluster data leesbaar op netwerk | TLS met client certificaten |
| 8 | Dashboard exposed | Cluster controle via browser | Verwijderen of achter VPN |
| 9 | Kubelet API open | Node-level command execution | --anonymous-auth=false |
| 10 | Geen audit logging | Blind vliegen | Audit policy + SIEM |
Checklist
| # | Maatregel | Prioriteit |
|---|---|---|
| 1 | RBAC: geen wildcard verbs/resources, geen cluster-admin voor apps | Kritiek |
| 2 | RBAC: dedicated service account,
automountServiceAccountToken: false |
Hoog |
| 3 | PSA: restricted op productie-namespaces |
Kritiek |
| 4 | SecurityContext: runAsNonRoot,
readOnlyRootFilesystem, drop ALL caps |
Kritiek |
| 5 | Network Policies: default deny ingress + egress | Kritiek |
| 6 | Network Policies: metadata service geblokkeerd, CNI enforcement geverifieerd | Kritiek |
| 7 | Secrets: encryption at rest + external secrets operator | Hoog |
| 8 | Images: goedgekeurde registries, geen latest, scanning
in CI/CD |
Hoog |
| 9 | Admission controller: Kyverno of OPA Gatekeeper actief | Hoog |
| 10 | etcd: TLS + firewall + versleutelde backups | Kritiek |
| 11 | Audit logging: policy geconfigureerd + SIEM integratie | Hoog |
| 12 | Kubelet: anonymous auth uitgeschakeld | Kritiek |
Kubernetes is het systeem dat zo complex is dat er een hele industrie is ontstaan om het te beheren. Denk daar even over na: het platform dat bedoeld was om deployments te vereenvoudigen, is zo ingewikkeld dat je er consultants, certificeringen en gespecialiseerde teams voor nodig hebt. En dat zijn de organisaties die het serieus nemen.
De rest – en dat is het merendeel – heeft een Kubernetes-cluster omdat iemand op een conferentie heeft gehoord dat “iedereen het doet.” Ze hebben vijftig microservices, drie mensen die weten wat een Pod is, en nul Network Policies. De containers draaien als root, het dashboard hangt aan het internet, en de enige RBAC-configuratie is dat iedereen cluster-admin is “zodat het werkt.”
Het mooiste is nog de illusie van container-isolatie. “We draaien in containers, dus we zijn veilig.” Ja, containers die als root draaien. Met hostNetwork. Met hostPID. Met het volledige Linux capability-set. Dat is geen isolatie – dat is een root shell met extra stappen. Maar het staat mooi op de architectuurslide.
Elk startupje heeft tegenwoordig een Kubernetes-cluster. Niet omdat ze het nodig hebben – een monoliet op een VM was prima geweest – maar omdat het op het CV staat. En die cluster? Geen security policy. Geen audit logging. Geen Network Policies. Maar wel een helm chart met elf dependencies en een Slack-integratie die elke deployment viert met een feesttoeter-emoji. Prioriteiten.
Samenvatting
Kubernetes hardening draait om lagen. RBAC beperkt wie wat mag doen. Pod Security Standards beperken wat containers mogen. Network Policies beperken met wie ze mogen praten. Secrets management beschermt gevoelige data. Image security garandeert dat je vertrouwde code draait. Admission controllers dwingen al deze regels af. etcd-beveiliging beschermt de kroonjuwelen van het cluster. En audit logging zorgt dat je ziet wat er gebeurt. Geen van deze maatregelen is optioneel. Begin met de kritieke items uit de checklist, en test regelmatig of de policies daadwerkelijk worden afgedwongen.
In het volgende hoofdstuk kijken we naar Infrastructure as Code security – hoe je ervoor zorgt dat de Terraform, Pulumi en CloudFormation templates waarmee je deze clusters bouwt, niet zelf de bron zijn van misconfiguraties en kwetsbaarheden.
Verder lezen in de kennisbank
Deze artikelen in het portaal geven je meer achtergrond en praktische context:
- De cloud — andermans computer, jouw verantwoordelijkheid
- Containers en Docker — wat het is en waarom je het moet beveiligen
- Encryptie — de kunst van het onleesbaar maken
- Least Privilege — geef mensen alleen wat ze nodig hebben
Je hebt een account nodig om de kennisbank te openen. Inloggen of registreren.
Gerelateerde securitymaatregelen
Deze artikelen bieden aanvullende context en verdieping: