Kubernetes Audit Policy: What to Log for Forensics
Write a Kubernetes audit policy that captures the evidence an investigation needs (exec, RBAC, workloads, tokens) without logging secrets or drowning in noise.
TL;DR. An audit policy is an ordered list of rules; the first match sets the audit level of each request. For forensics, you want Metadata on everything, Request or RequestResponse on writes to RBAC objects, pods and workload controllers, Request on pods/exec, pods/attach, pods/portforward, nodes/proxy and serviceaccounts/token, and strictly Metadata on secrets, configmaps and token reviews. Drop the health checks and the kubelet and kube-proxy chatter. Ship the result outside the cluster and keep it for at least 90 days.
A self-managed API server writes nothing until you give it a policy: the Kubernetes auditing documentation is explicit that without --audit-policy-file no events are logged. On EKS, GKE and AKS, the provider sets the policy for you, and your only choice is which logs to export (see the EKS, GKE and AKS export guide). Either way, knowing what a good policy looks like tells you what evidence you can expect.
How rules are evaluated
Each rule can match on users, userGroups, verbs, resources (with API group and optional resourceNames), namespaces and nonResourceURLs. Rules are processed in order and the first matching rule wins. A request that matches no rule is not logged. That has two practical consequences:
- Put your exclusions (
level: None) and your "never log bodies" rules before the broad rules. - End with a catch-all
level: Metadataso nothing falls through silently.
omitStages removes stages globally or per rule. Almost everyone omits RequestReceived, which duplicates the final event. omitManagedFields: true drops the bulky metadata.managedFields from logged bodies.
What investigators need, and why
| Evidence | Minimum level | Why |
|---|---|---|
| Who called what, from where, with which result | Metadata on everything | Identity, source IP, verb, object, code: the backbone of every timeline |
Pod and workload specs (pods, deployments, daemonsets, statefulsets, jobs, cronjobs) on create / update / patch | Request | Without the body you cannot see privileged, hostPID, a hostPath or the image |
RBAC writes (roles, rolebindings, clusterroles, clusterrolebindings) | Request or RequestResponse | The roleRef and subjects tell you whether a binding grants cluster-admin and to whom |
pods/exec, pods/attach, pods/portforward, nodes/proxy | Metadata is enough, Request is fine | The command is in requestURI; the session itself is never recorded |
serviceaccounts/token | Request, not RequestResponse | The request shows the requested expiry and audience; the response contains the token itself |
secrets, configmaps, tokenreviews | Metadata only | Bodies contain the credentials; logging them turns the audit log into a credential store |
Deletion of events | Metadata | Deleting Events is a cheap anti-forensics step |
A forensics-oriented policy
This policy is a starting point, not a drop-in. Test it on a non-production cluster and watch log volume.
apiVersion: audit.k8s.io/v1
kind: Policy
omitStages:
- "RequestReceived"
omitManagedFields: true
rules:
# 1. Noise: health checks and high-frequency system reads
- level: None
nonResourceURLs: ["/healthz*", "/livez*", "/readyz*", "/version"]
- level: None
users: ["system:kube-proxy"]
verbs: ["watch"]
resources:
- group: ""
resources: ["endpoints", "services", "services/status"]
- level: None
userGroups: ["system:nodes"]
verbs: ["get"]
resources:
- group: ""
resources: ["nodes", "nodes/status"]
# 2. Credentials: never log bodies
- level: Metadata
resources:
- group: ""
resources: ["secrets", "configmaps"]
- group: "authentication.k8s.io"
resources: ["tokenreviews"]
# 3. Interactive access and token minting (request only: the
# TokenRequest response contains the token)
- level: Request
resources:
- group: ""
resources: ["pods/exec", "pods/attach", "pods/portforward",
"nodes/proxy", "serviceaccounts/token"]
# 4. Events: keep deletions, drop the rest
- level: Metadata
verbs: ["delete", "deletecollection"]
resources:
- group: ""
resources: ["events"]
- group: "events.k8s.io"
resources: ["events"]
- level: None
resources:
- group: ""
resources: ["events"]
- group: "events.k8s.io"
resources: ["events"]
# 5. Writes to workloads, RBAC, service accounts, admission: full bodies
- level: RequestResponse
verbs: ["create", "update", "patch", "delete", "deletecollection"]
resources:
- group: ""
resources: ["pods", "serviceaccounts", "namespaces", "nodes"]
- group: "apps"
- group: "batch"
- group: "rbac.authorization.k8s.io"
- group: "admissionregistration.k8s.io"
# 6. Everything else
- level: Metadata
A few choices worth explaining:
- Rule 2 before rule 5. Secrets must stay at
Metadataeven oncreateandupdate, so the secrets rule has to match first. - Rule 3 uses
Request. Streaming subresources have no meaningful body; what you need (the command, the container) is in the URI. Forserviceaccounts/token,RequestResponsewould log a working bearer token. - Kubelet status updates (
nodes/status,pods/statuspatches) fall into rule 6 atMetadata. That is plenty for investigations and far cheaper than bodies. - Reads stay at
Metadata. Logging response bodies oflistcalls is what makes audit logs explode in size; it rarely helps an investigation.
For comparison, the policy AWS publishes in the EKS best practices guide follows the same structure: Metadata for secrets, configmaps and token reviews, Request for serviceaccounts/token, full bodies for most known API groups. One notable difference: it drops events entirely, so event deletions are not visible on EKS.
Enabling it on a self-managed API server
On a kubeadm-style control plane, the API server runs as a static pod. Add the flags to /etc/kubernetes/manifests/kube-apiserver.yaml and mount the policy and the log directory:
- --audit-policy-file=/etc/kubernetes/audit-policy.yaml
- --audit-log-path=/var/log/kubernetes/audit/audit.log
- --audit-log-maxage=30
- --audit-log-maxbackup=10
- --audit-log-maxsize=100
maxage is in days, maxsize in megabytes. The kubelet restarts the API server when the manifest changes. Distributions such as k3s and RKE2 expose the same flags through their own configuration; check their documentation for the exact keys.
The documentation also notes that auditing increases API server memory use, proportional to what you log. That is one more reason to keep bodies to the writes that matter.
Get the logs off the box
An attacker with cluster-admin and a privileged pod can read and edit files on control-plane nodes. Logs that only exist there are logs the attacker controls. Two options:
- The webhook backend (
--audit-webhook-config-file) sends batches to an HTTP endpoint: a log collector, a SIEM, a queue. - The log backend plus a shipper (Fluent Bit, Vector, the cloud agent) tails
audit.loginto central storage.
The NSA/CISA Kubernetes Hardening Guide also recommends enabling audit logging and storing logs outside the cluster. Keep at least 90 days: initial access often predates detection by weeks.
Check your policy against real detections
A good test: run a known-bad sequence on a lab cluster (exec into a pod, create a privileged pod with a hostPath of /, bind a service account to cluster-admin), export the log and drop it into the audit log analyzer. If the privileged-pod or cluster-admin findings do not appear, the relevant bodies are not being logged. The analyzer's reference section lists the detections; the how-to guide explains how to read the result.