Security guides
Security guide

Kubernetes Audit Logs: Enable and Investigate

By Sazabi Team

Last verified on Nov 8, 2026.

Kubernetes audit logs are a chronological record of requests to the API server: who asked, what they asked for, from where, and whether it was allowed. They are off unless you configure them, and what they contain depends on the policy you set. This guide covers turning them on for EKS, GKE, and self-managed clusters, then the specific events to look for when you suspect a compromise.

What an Audit Event Contains

The cluster audits activity from users, from applications that call the Kubernetes API, and from the control plane itself. Each audit event starts inside kube-apiserver. It is filtered by your policy and written to a backend. A single request can produce more than one event, one per stage:

StageWhen it is generated
RequestReceivedAs soon as the audit handler gets the request
ResponseStartedAfter response headers are sent; only for long-running requests such as watch
ResponseCompleteWhen the response body is finished
PanicWhen a panic occurred

The event fields you will query most are auditID, stage, verb, user, impersonatedUser, sourceIPs, userAgent, objectRef (resource, namespace, name, subresource), responseStatus, requestURI, and annotations. Two cautions from the API reference: userAgent is client-supplied and must not be trusted, and in sourceIPs all but the last address can be set arbitrarily by the client.

Audit Policy Levels

A policy is a list of rules, evaluated in order. The first matching rule sets the audit level for the event. The levels are:

LevelWhat is recorded
NoneNothing for events matching the rule
MetadataRequesting user, timestamp, resource, verb, and so on, but not request or response body
RequestMetadata plus the request body, not the response body
RequestResponseMetadata, request body, and response body

Two rules from the Kubernetes docs matter for security work. If you omit --audit-policy-file, no events are logged. And a policy with no rules is treated as illegal.

The smallest useful policy logs everything at Metadata, which already shows exec, secret reads, and RBAC changes without storing bodies:

apiVersion: audit.k8s.io/v1
kind: Policy
omitStages:
  - RequestReceived
rules:
  - level: Metadata

Request bodies are what let you see privileged pod specs, but they cost volume. A middle path, based on the example policy in the Kubernetes docs, is to record pod and RBAC changes with their request body and keep secrets at Metadata so secret values never land in the audit log:

apiVersion: audit.k8s.io/v1
kind: Policy
omitStages:
  - RequestReceived
rules:
  - level: Metadata
    resources:
      - group: ''
        resources: ['secrets', 'configmaps']
  - level: Request
    resources:
      - group: ''
        resources: ['pods']
      - group: 'rbac.authorization.k8s.io'
  - level: Metadata

Order matters. The secrets rule comes first so a later broad rule cannot capture secrets at a higher level. The pods resource does not match subresources such as pods/exec, so exec requests fall through to the final Metadata rule, which is enough to see them.

Enabling Audit Logs

Self-Managed Clusters

The log backend writes events as JSON lines to a file. Configure it with these kube-apiserver flags:

--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

--audit-log-path set to - writes to standard output, and leaving it unset disables the log backend. --audit-log-maxage is the maximum number of days to retain old files, --audit-log-maxbackup the maximum number of files, and --audit-log-maxsize the size in megabytes before rotation. If the API server runs as a static pod, mount the policy file and the log directory in with hostPath volumes, or the records will not persist.

The alternative is the webhook backend, which sends events to a remote HTTP API. You point --audit-webhook-config-file at a kubeconfig-format file. Webhook batching is on by default and events are dropped if the buffer overflows, so watch the apiserver_audit_event_total and apiserver_audit_error_total metrics the API server exposes.

Amazon EKS

EKS control plane logs are not sent to CloudWatch by default. You enable each type individually, and the audit type delivers Kubernetes audit events:

aws eks update-cluster-config \
  --region us-east-1 \
  --name my-cluster \
  --logging '{"clusterLogging":[{"types":["audit"],"enabled":true}]}'

Events appear in the log group /aws/eks/my-cluster/cluster, in streams named kube-apiserver-audit-<id>. AWS says delivery is best effort and typically takes a few minutes, and standard CloudWatch ingestion and storage rates apply. The audit policy is managed by EKS, so you cannot tune what it records the way you can on a self-managed cluster.

Google Kubernetes Engine (GKE)

GKE writes Kubernetes audit logs to Cloud Audit Logs using the k8s_cluster resource type. Admin Activity logs, which cover writes such as creating pods and changing RBAC, are always on and cannot be disabled. Data Access logs, which cover reads, are disabled by default. Enable them for the Kubernetes Engine API if you need to see get and list on secrets. A Logs Explorer filter for RBAC changes, from the GKE documentation:

resource.type="k8s_cluster"
protoPayload.methodName:"io.k8s.authorization.rbac.v1"
NOT protoPayload.authenticationInfo.principalEmail:"system"

Investigating with jq

On a self-managed cluster the audit file is one JSON object per line, so jq works directly. On EKS, export the kube-apiserver-audit streams and extract the message, or use the CloudWatch Logs Insights dot notation shown below. The examples assume one event per line.

Exec into Pods

MITRE ATT&CK T1609, Container Administration Command, covers abuse of the Kubernetes API server or kubelet to run commands inside containers. In the audit log it is a request against the exec subresource of a pod:

jq -c 'select(.objectRef.resource=="pods" and .objectRef.subresource=="exec")
  | {t:.requestReceivedTimestamp, user:.user.username, ip:.sourceIPs[0],
     ns:.objectRef.namespace, pod:.objectRef.name, code:.responseStatus.code,
     uri:.requestURI}' audit.log

The requestURI carries the command as query parameters. Compare the user against who is expected to debug production. An exec by a service account that has never done it before is worth a look, as is any exec into a pod that holds credentials.

Secret Reads by Unusual Users

jq -r 'select(.objectRef.resource=="secrets" and (.verb=="get" or .verb=="list" or .verb=="watch"))
  | [.user.username, .verb, .objectRef.namespace // "-", .objectRef.name // "*"] | @tsv' audit.log \
  | sort | uniq -c | sort -rn

A list with no name returns every secret in the namespace. Count by user, then ask why each non-controller identity needs that access.

RBAC Escalation

Creating a ClusterRoleBinding is the usual way to grant cluster-admin. Pair this with privilege change detection:

jq -c 'select(.objectRef.resource=="clusterrolebindings"
  and (.verb=="create" or .verb=="update" or .verb=="patch"))
  | {t:.requestReceivedTimestamp, user:.user.username, name:.objectRef.name,
     code:.responseStatus.code}' audit.log

At Request level, add .requestObject.roleRef.name to see which role was bound.

Anonymous and Denied Requests

jq -c 'select(.user.username=="system:anonymous")
  | {t:.requestReceivedTimestamp, ip:.sourceIPs[0], verb, uri:.requestURI,
     code:.responseStatus.code}' audit.log

Anonymous requests that return 200 on anything beyond health endpoints need explaining. For denials, the authorization.k8s.io/decision annotation holds the decision, for example forbid. A burst of forbidden requests from one identity is someone probing what a stolen token can do.

Privileged Pod Creation

This needs Request level or higher on pods, because the spec lives in requestObject:

jq -c 'select(.verb=="create" and .objectRef.resource=="pods" and .objectRef.subresource==null
  and ([.requestObject.spec.containers[]?.securityContext.privileged] | any))
  | {t:.requestReceivedTimestamp, user:.user.username, ns:.objectRef.namespace,
     pod:(.objectRef.name // .requestObject.metadata.name)}' audit.log

Also check hostPath volumes, hostNetwork, and hostPID. MITRE tracks the technique as Deploy Container (T1610) and Escape to Host (T1611).

The Same Searches on EKS

In CloudWatch Logs Insights, JSON fields use dot notation, so the exec query becomes:

fields @timestamp, user.username, sourceIPs.0, objectRef.namespace, objectRef.name
| filter @logStream like /kube-apiserver-audit/
| filter objectRef.resource = "pods" and objectRef.subresource = "exec"
| sort @timestamp desc

Impersonation and Timeline Building

Check impersonatedUser. An event where user is an admin and impersonatedUser is a different identity shows someone acting as another account. Group events by auditID to reassemble one request across stages, and sort by requestReceivedTimestamp to build a timeline. Cross-reference with kubectl logs from the affected pods for what happened inside the container, since audit logs show API calls and not process activity.

Searching Audit Logs with Sazabi

Sazabi, an AI-native observability and security platform, has no separate Kubernetes log source. Audit events arrive through the source your platform already uses: AWS CloudWatch for EKS, Google Cloud for GKE, and the OpenTelemetry Collector reading the audit file for self-managed clusters. The Sazabi docs use a Metadata-level policy for self-managed clusters and note that on GKE you must enable Data Access logs to record reads. Once events arrive, you can ask for them in plain language, for example sazabi logs ask "kubernetes audit events that exec'd into pods in the last day". See the Kubernetes audit logs setup page and the kubectl integration. For the general concept, see audit log.

Frequently Asked Questions

How do I enable audit logging in Kubernetes?

Start kube-apiserver with --audit-policy-file pointing at a policy and --audit-log-path for the log backend. If the policy flag is omitted, no events are logged. On EKS, turn on the audit control plane log type. On GKE, Admin Activity logs are already on.

Where are Kubernetes audit logs stored on EKS?

In CloudWatch Logs, in the group /aws/eks/<cluster name>/cluster, in log streams named kube-apiserver-audit followed by an ID. They are only delivered after you enable the audit log type.

What is the difference between Metadata and RequestResponse audit levels?

Metadata records who did what to which resource and when, without bodies. RequestResponse also records the request body and the response body, which is far larger and can contain secret values.

Can I see who ran kubectl exec in a pod?

Yes. An exec request is audited against the pods resource with the exec subresource. The event shows the user, source IP, user agent, namespace, and pod name.

Sources

  1. Audit levels (None, Metadata, Request, RequestResponse), stages, first-match policy rules, log and webhook backends, and the kube-apiserver flags for the log backend (accessed Nov 8, 2026)
  2. Audit event fields: auditID, stage, verb, user, impersonatedUser, sourceIPs, userAgent, objectRef, responseStatus, requestObject, and annotations (accessed Nov 8, 2026)
  3. The authorization.k8s.io/decision audit annotation records the authorization decision, for example forbid (accessed Nov 8, 2026)
  4. EKS control plane logging is off by default, the audit log type, the CloudWatch log group /aws/eks/<cluster>/cluster, and kube-apiserver-audit streams (accessed Nov 8, 2026)
  5. GKE writes Kubernetes audit logs to Cloud Audit Logs with the k8s_cluster resource type; Admin Activity logs are always on and Data Access logs are off by default (accessed Nov 8, 2026)
  6. CloudWatch Logs Insights uses dot notation to reference fields in JSON log events (accessed Nov 8, 2026)
  7. T1609 Container Administration Command covers abuse of the Kubernetes API server or kubelet to run commands inside containers (accessed Nov 8, 2026)
  8. Sazabi receives EKS audit logs through CloudWatch, GKE through Google Cloud, and self-managed clusters through the OpenTelemetry Collector (accessed Nov 8, 2026)