Detection & Response
Audit Logs
Configuring and reading the API server audit log to reconstruct what happened.
The API server audit log is the flight recorder of the cluster. Every request (who made it, what verb, which object, what the server answered) can be written down. When something goes wrong, this is the record you reconstruct the incident from.
Audit logging is off by default. A cluster without it has no control-plane memory: after an incident you will know something happened, but not who did it or what else they touched.
What Gets Recorded
Each audit event captures the request in security-relevant detail:
- the authenticated user, their groups, and any impersonation
- the verb, resource, namespace, and object name
- the source IP and user agent
- the response code, and optionally the full request and response bodies
- timestamps for received and completed stages
Levels And Stages
The audit policy decides how much is recorded per request. Four levels:
| Level | Records |
|---|---|
None | Nothing; used to drop noise |
Metadata | Who, what, when, but no bodies |
Request | Metadata plus the request body |
RequestResponse | Both bodies, full fidelity |
Events fire at stages (RequestReceived, ResponseComplete, ResponseStarted for watches, Panic). Logging only ResponseComplete halves volume and still tells you what happened and whether it succeeded.
A Starter Policy
Metadata for everything, full fidelity where attackers live, and noise dropped explicitly:
apiVersion: audit.k8s.io/v1
kind: Policy
omitStages: ["RequestReceived"]
rules:
# High-volume, low-value reads: drop
- level: None
users: ["system:kube-proxy"]
verbs: ["watch"]
- level: None
userGroups: ["system:nodes"]
verbs: ["get"]
resources:
- group: ""
resources: ["nodes", "nodes/status"]
# Where attackers live: record everything
- level: RequestResponse
resources:
- group: ""
resources: ["secrets", "configmaps", "serviceaccounts/token"]
- group: "rbac.authorization.k8s.io"
resources: ["clusterrolebindings", "rolebindings", "clusterroles", "roles"]
- level: RequestResponse
verbs: ["create"]
resources:
- group: ""
resources: ["pods/exec", "pods/attach", "pods/portforward"]
# Everything else: metadata
- level: Metadata
Rules are evaluated top to bottom; the first match wins.
Enable It On The Lab Cluster
The audit flags are set on the API server itself, so on kind this means a cluster config, and recreating the cluster:
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: k8s-security-lab
nodes:
- role: control-plane
extraMounts:
- hostPath: ./audit-policy.yaml
containerPath: /etc/kubernetes/audit/audit-policy.yaml
readOnly: true
kubeadmConfigPatches:
- |
kind: ClusterConfiguration
apiServer:
extraArgs:
audit-policy-file: /etc/kubernetes/audit/audit-policy.yaml
audit-log-path: /var/log/kubernetes/audit.log
audit-log-maxage: "7"
audit-log-maxsize: "100"
extraVolumes:
- name: audit
hostPath: /etc/kubernetes/audit
mountPath: /etc/kubernetes/audit
readOnly: true
- name: audit-log
hostPath: /var/log/kubernetes
mountPath: /var/log/kubernetes
kind delete cluster --name k8s-security-lab
kind create cluster --config kind-audit.yaml
Generate an interesting event and find it:
kubectl create namespace lab
kubectl run audit-demo --image=busybox:1.36 --restart=Never -n lab -- sleep 3600
kubectl exec -n lab audit-demo -- id
docker exec k8s-security-lab-control-plane \
grep '"subresource":"exec"' /var/log/kubernetes/audit.log | tail -1
Expected output (trimmed):
{"kind":"Event","verb":"create","user":{"username":"kubernetes-admin"},
"objectRef":{"resource":"pods","namespace":"lab","name":"audit-demo","subresource":"exec"},
"responseStatus":{"code":101}}
That single line answers who exec’d into which pod, from where, and when.
What To Alert On
A short list that catches most real attacks:
pods/exec,pods/attach,pods/portforward: interactive access to workloads.get/liston secrets, especially cluster-wide lists or unusual identities.- Writes to RBAC objects, webhooks, and CronJobs: persistence and escalation.
- Requests from
system:anonymousorsystem:unauthenticatedthat are not health checks. - A service account calling APIs its application has never called before.
Ship Logs Off The Node
A log an attacker can delete is not evidence. Forward audit logs to storage outside the cluster: a log pipeline (Fluent Bit, Vector) tailing audit-log-path, or the --audit-webhook-config-file flag pushing events directly to a collector. Managed clusters do this for you: EKS ships audit logs to CloudWatch, GKE to Cloud Logging, but usually off by default, so turn it on.
Clean Up
kubectl delete pod audit-demo -n lab
Keep the audit-enabled cluster; later pages assume signals are being recorded.
Practical Guidance
- Turn audit logging on before you need it; it cannot be enabled retroactively.
- Start from the policy above, not
Metadatafor everything; the volume of an unfiltered log is what kills audit programs. - Record
RequestResponsefor secrets, RBAC, and exec; that is where incidents are reconstructed. - Ship logs off-cluster with retention measured in months, not days.
- Alert on the short list first; a handful of high-signal alerts beats a thousand unread ones.