TL;DR
Ein Kubernetes-Security-Audit identifiziert systematisch Konfigurationsfehler, die Angriffsflächen in K8s-Clustern schaffen. Der Auditprozess umfasst die Analyse von RBAC-Berechtigungen, die Überprüfung von PodSecurity-Standards, die Bewertung von NetworkPolicies und die Sicherstellung der Secret-Verschlüsselung. Ein zentrales Werkzeug ist der CIS Kubernetes Benchmark, der 246 Prüfpunkte für die Sicherheit des Clusters definiert. Beispielsweise deckt die RBAC-Analyse mit Tools wie `kubectl-who-can` überprivilegierte ServiceAccounts auf, die Wildcards in ihren Berechtigungen nutzen oder als `cluster-admin` gebunden sind. Seit Kubernetes 1.25 stabilisiert, erzwingt der PodSecurity Admission Controller auf Namespace-Ebene Sicherheitsstandards wie `runAsNonRoot: true` oder das Verbot von `hostPath` Volumes, um Pods vor Privilege Escalation zu schützen.
Diese Zusammenfassung wurde KI-gestützt erstellt (EU AI Act Art. 52).
Inhaltsverzeichnis (6 Abschnitte)
Kubernetes ist das Schweizer Taschenmesser der Container-Orchestrierung - und sein Sicherheitsmodell ist entsprechend komplex. Standardmäßig sind viele Kubernetes-Cluster zu permissiv konfiguriert: ServiceAccounts können Cluster-Resourcen lesen, Pods laufen als root, NetworkPolicies fehlen und Secrets sind Base64-kodiert aber nicht verschlüsselt. Ein Kubernetes-Security-Audit folgt einem strukturierten Prozess und deckt systematisch diese Konfigurationsfehler auf.
Kubernetes Security Audit - Scope und Methodik
CIS Kubernetes Benchmark (CIS-K8s):
→ Center for Internet Security: 246 Prüfpunkte für Kubernetes
→ Kategorien: API Server, Controller Manager, Scheduler, etcd, Nodes, Policies
→ Level 1: Grundsicherheit (ohne Breaking Changes)
→ Level 2: Erhöhte Sicherheit (einige Einschränkungen)
Automatisierter CIS-Scan:
# kube-bench (Aqua Security):
kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job.yaml
kubectl logs job/kube-bench
Ausgabe:
[INFO] 1 Master Node Security Configuration
[PASS] 1.2.1 Ensure that the --anonymous-auth argument is set to false
[FAIL] 1.2.9 Ensure that the --profiling argument is set to false
[WARN] 1.2.16 Ensure that the admission control plugin AlwaysPullImages is set
# Einzelner Node:
kube-bench run --targets master
kube-bench run --targets node
RBAC Analyse - wer darf was?:
# Alle ClusterRoleBindings anzeigen:
kubectl get clusterrolebindings -o wide | grep -v "system:"
# Wer hat cluster-admin?
kubectl get clusterrolebindings -o json | \
jq '.items[] | select(.roleRef.name=="cluster-admin") | .subjects'
# kubectl-who-can (Plugin):
kubectl who-can create pods -n default
kubectl who-can delete secrets --all-namespaces
kubectl who-can get secrets -n kube-system
# Rogue ServiceAccount finden (hat zu viele Rechte):
kubectl auth can-i --list --as=system:serviceaccount:default:default
# → Liste aller erlaubten Aktionen für default ServiceAccount
# RBAC-Audit Tool:
kubectl-rbac-tool who-can get secrets
# npm install -g krane → grafische RBAC-Analyse
RBAC-Fehlkonfigurationen
Häufig gefundene RBAC-Probleme:
1. Default ServiceAccount hat zu viele Rechte:
# Prüfen:
kubectl auth can-i --list --as=system:serviceaccount:default:default
# Problem: Default SA kann Secrets lesen:
# apiVersion: rbac.authorization.k8s.io/v1
# kind: ClusterRoleBinding
# subjects:
# - kind: ServiceAccount
# name: default
# namespace: default
# Fix: Automounting deaktivieren wenn SA nicht benötigt:
apiVersion: v1
kind: ServiceAccount
metadata:
name: my-app
automountServiceAccountToken: false # Kein Token im Pod!
# Oder: Pod-Level:
spec:
automountServiceAccountToken: false
2. Wildcards in RBAC (gefährlich!):
# FALSCH - zu weit:
rules:
- apiGroups: ["*"] # Alle API Groups
resources: ["*"] # Alle Resources
verbs: ["*"] # Alle Verben
# RICHTIG - minimal:
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch"]
3. Privileged ServiceAccount für Anwendungen:
# Prüfen: welche SAs haben privilegierte Bindings?
kubectl get rolebindings,clusterrolebindings --all-namespaces -o json | \
jq '.items[] | select(.roleRef.name | test("admin|cluster-admin|edit")) |
{name: .metadata.name, namespace: .metadata.namespace, roleRef: .roleRef.name}'
4. Escape-Path: Pod kann ServiceAccount-Token stehlen:
# Wenn Pod mit high-privileged SA läuft:
# Im Pod:
cat /var/run/secrets/kubernetes.io/serviceaccount/token
# → Token kann für kubectl-Befehle genutzt werden!
# Test: welche Aktionen sind mit SA-Token möglich?
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
kubectl --token="$TOKEN" auth can-i --list
Audit-Checkliste RBAC:
□ Kein Wildcards in Production RBAC
□ Default ServiceAccount: automountServiceAccountToken=false
□ Keine cluster-admin Bindings für Anwendungen
□ Minimale Berechtigungen per Namespace getrennt
□ Regelmäßige RBAC-Review (quartalsweise)
□ Externe SA-Berechtigungen (CI/CD, Monitoring) minimiert
PodSecurity Admission Controller
PodSecurity Admission (PSA) - seit K8s 1.25 stable, OPA-Policies deprecated:
3 Security-Level:
privileged: Keine Einschränkungen (für System-Pods)
baseline: Verhindert kritische Privilege-Escalation (empfohlen als Start)
restricted: Strengste Sicherheit (für Produktions-Workloads)
PSA Labels auf Namespace:
# Namespace-Level Enforcement:
kubectl label namespace production \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/enforce-version=v1.29
# Warn + Audit ohne Enforcement (zum Testen):
kubectl label namespace staging \
pod-security.kubernetes.io/warn=restricted \
pod-security.kubernetes.io/audit=restricted
Restricted Profile - was wird erzwungen?:
□ runAsNonRoot: true (kein root!)
□ runAsUser: > 0 (nicht UID 0)
□ allowPrivilegeEscalation: false
□ readOnlyRootFilesystem: true (empfohlen)
□ seccompProfile: RuntimeDefault oder localhost
□ capabilities: DROP ALL
□ hostProcess: false
□ hostPath Volumes: verboten
Konformer Pod (restricted):
apiVersion: v1
kind: Pod
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
# Temp-Verzeichnis wenn readOnly:
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
Cluster-weites Default-Policy (AdmissionConfiguration):
# /etc/kubernetes/admission-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: PodSecurity
configuration:
apiVersion: pod-security.admission.config.k8s.io/v1
kind: PodSecurityConfiguration
defaults:
enforce: "restricted"
enforce-version: "latest"
audit: "restricted"
audit-version: "latest"
warn: "restricted"
warn-version: "latest"
exemptions:
namespaces: ["kube-system", "monitoring"] # System-Namespaces ausgenommen
Audit: existierende Pods prüfen:
# Wer würde restricted-Policy verletzen?
kubectl get pods --all-namespaces -o json | \
jq '.items[] | select(.spec.containers[].securityContext.privileged == true) |
{name: .metadata.name, namespace: .metadata.namespace}'
# Privileged Pods:
kubectl get pods --all-namespaces -o jsonpath=\
'{range .items[?(@.spec.containers[*].securityContext.privileged)]}{.metadata.namespace}/{.metadata.name}{"\n"}{end}'
NetworkPolicy - Netzwerksegmentierung
NetworkPolicy - Default Deny ist Pflicht!
Problem ohne NetworkPolicy:
→ Alle Pods können mit allen Pods in allen Namespaces kommunizieren!
→ Lateral Movement: kompromittierter Pod erreicht Datenbank, andere Services
Default Deny AllIngress + AllEgress:
# Erst: alles blockieren
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {} # Gilt für ALLE Pods in Namespace
policyTypes:
- Ingress
- Egress
# Dann: erlaubte Verbindungen explizit freischalten
Erlauben: Frontend → Backend:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-backend
namespace: production
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
Erlauben: Backend → Datenbank (anderer Namespace):
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-backend-to-db
namespace: database
spec:
podSelector:
matchLabels:
app: postgres
ingress:
- from:
- namespaceSelector:
matchLabels:
name: production
podSelector:
matchLabels:
app: backend
ports:
- port: 5432
Erlauben: DNS (Pflicht!):
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
namespace: production
spec:
podSelector: {}
egress:
- ports:
- protocol: UDP
port: 53
NetworkPolicy Audit:
# Namespaces ohne NetworkPolicy:
kubectl get namespaces | while read ns; do
count=$(kubectl get networkpolicy -n "$ns" 2>/dev/null | wc -l)
if [ "$count" -le "1" ]; then
echo "KEIN NetworkPolicy: $ns"
fi
done
# Tool: network-policy-viewer
kubectl neat | npx @lbogdan/kube-network-policies-visualizer
# → Grafische Darstellung der NetworkPolicy-Topologie
Secrets Management in Kubernetes
K8s Secrets - Base64 ist KEINE Verschlüsselung!
Standardproblem:
kubectl get secret db-password -o jsonpath='{.data.password}' | base64 -d
# → Passwort im Klartext! Base64 ist nur Encoding, kein Schutz!
etcd-Verschlüsselung aktivieren (EncryptionConfiguration):
# /etc/kubernetes/encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources: ["secrets"]
providers:
- aescbc: # AES-CBC mit 256-bit Key
keys:
- name: key1
secret: <base64-encoded-32-byte-key>
- identity: {} # Fallback für unverschlüsselte (Migration!)
# kube-apiserver Flag:
--encryption-provider-config=/etc/kubernetes/encryption-config.yaml
# Prüfen: Secret im etcd verschlüsselt?
ETCDCTL_API=3 etcdctl get /registry/secrets/default/db-password \
--endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt \
--key=/etc/kubernetes/pki/etcd/healthcheck-client.key | strings
# Wenn verschlüsselt: sollte unlesbares Binary zeigen
Externe Secret Manager (Best Practice):
Option 1: HashiCorp Vault Agent Injector:
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "my-role"
vault.hashicorp.com/agent-inject-secret-db-pass: "secret/data/db"
# Vault Agent Sidecar liest Secret + schreibt als Datei in Pod
Option 2: External Secrets Operator (ESO):
# ExternalSecret: AWS Secrets Manager → K8s Secret
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-secret
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: SecretStore
target:
name: db-password
data:
- secretKey: password
remoteRef:
key: prod/db/password
property: password
# Secret wird automatisch im K8s erstellt und täglich aktualisiert!
Option 3: CSI Secrets Store:
# Secrets als Volume in Pod mounten (direkt aus Vault/AWS/Azure)
volumes:
- name: secrets-store
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: aws-secrets
# Vorteil: Secret nie als K8s Secret gespeichert!
Secret-Audit Checkliste:
□ etcd-Verschlüsselung: EncryptionConfiguration aktiviert
□ Kein Klartext in Deployments/ConfigMaps (Secret-Scanning im Repo!)
□ RBAC: Secrets nur für notwendige ServiceAccounts lesbar
□ Rotation: Secrets regelmäßig rotiert (Vault Dynamic Secrets ideal)
□ Audit Logging: Secret-Zugriffe geloggt (K8s Audit Log)
Kubernetes Audit Logging
Kubernetes Audit Log - alle API-Anfragen protokolliert:
Audit Policy (Minimalempfehlung):
# /etc/kubernetes/audit-policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
# Keine Logs für Read-Only Requests auf non-sensitive Resources:
- level: None
verbs: ["get", "list", "watch"]
resources:
- group: ""
resources: ["events"]
# Secrets: Metadata + Request Body loggen:
- level: RequestResponse
resources:
- group: ""
resources: ["secrets", "configmaps"]
# Privileged Operations: vollständig loggen:
- level: RequestResponse
verbs: ["create", "update", "patch", "delete"]
resources:
- group: "rbac.authorization.k8s.io"
resources: ["clusterroles", "clusterrolebindings"]
# Alles andere: Metadata (ohne Body):
- level: Metadata
kube-apiserver Flags:
--audit-log-path=/var/log/kubernetes/audit.log
--audit-policy-file=/etc/kubernetes/audit-policy.yaml
--audit-log-maxage=30 # Tage aufbewahren
--audit-log-maxbackup=3 # Backup-Dateien
--audit-log-maxsize=100 # MB pro Datei
Audit Log analysieren (Sentinel KQL äquivalent):
# Verdächtige: Secrets aus anderen Namespaces gelesen:
cat /var/log/kubernetes/audit.log | jq '
select(.verb == "get" and
.objectRef.resource == "secrets" and
.user.username | test("system:serviceaccount") | not)
| {time: .stageTimestamp, user: .user.username,
namespace: .objectRef.namespace, name: .objectRef.name}'
# Neue ClusterRoleBinding:
cat /var/log/kubernetes/audit.log | jq '
select(.verb == "create" and
.objectRef.resource == "clusterrolebindings")
| {time: .stageTimestamp, user: .user.username, name: .objectRef.name}'
Falco - Echtzeit Runtime Security:
# Falco erkennt verdächtiges Verhalten zur Laufzeit:
- Container spawnt Shell: kubectl exec → Bash → ALERT
- Datei in /etc/ geschrieben: ALERT
- Privileged Container gestartet: ALERT
- Netzwerkverbindung aus Container nach extern: ALERT
Installation:
helm repo add falcosecurity https://falcosecurity.github.io/charts
helm install falco falcosecurity/falco \
--namespace falco-system \
--set falco.grpc.enabled=true \
--set falcosidekick.enabled=true # Alerts zu Slack/PagerDuty
Kubernetes-Cluster sind hochkomplex und bieten viele Angriffsflächen, die ohne strukturiertes Audit oft übersehen werden. AWARE7 prüft Kubernetes-Deployments nach CIS Kubernetes Benchmark und OWASP Kubernetes Security Testing Guide - von RBAC-Analyse über PodSecurity bis zu etcd-Verschlüsselung.
Cloud-Security Pentest anfragen | Penetrationstest Cloud-Security
Nächster Schritt
Unsere zertifizierten Sicherheitsexperten beraten Sie zu den Themen aus diesem Artikel — unverbindlich und kostenlos.
Kostenlos · 30 Minuten · Unverbindlich
