Skip to content

This tool is not affiliated with, endorsed by or sponsored by The Linux Foundation, the Cloud Native Computing Foundation (CNCF) or the Kubernetes project. Kubernetes and K8s are registered trademarks of The Linux Foundation. EKS, GKE, AKS and other names are trademarks of their respective owners.

Kubernetes Audit Log Format: Anatomy of an Audit Event

Field-by-field guide to the Kubernetes audit log format: stages, levels, user, sourceIPs, objectRef, responseStatus, annotations, and what matters in forensics.

Published on 5 min read

TL;DR. A Kubernetes audit event is one JSON object of kind Event (audit.k8s.io/v1) describing one request at one stage. The fields you will use in every investigation are user, impersonatedUser, sourceIPs, userAgent, verb, objectRef, requestURI, responseStatus.code, the authorization.k8s.io/decision and reason annotations, and, when the audit level allows it, requestObject. Deduplicate stages on auditID, trust only the last source IP, and remember that bodies are absent at Metadata level.

Most people meet the audit log format for the first time during an incident, in a CloudWatch or Log Analytics window, with thousands of nearly identical lines. Knowing which fields carry evidence and which are noise saves hours.

A complete event

Here is a trimmed event for kubectl exec into a pod, as the API server writes it with the log backend (one JSON object per line). Names and addresses are fictional.

{
  "kind": "Event",
  "apiVersion": "audit.k8s.io/v1",
  "level": "Metadata",
  "auditID": "4f1e0a52-8c1b-4d0e-9d3e-2b7a5c9e1f00",
  "stage": "ResponseStarted",
  "requestURI": "/api/v1/namespaces/shop/pods/api-6c9f7d8b5-q2w8x/exec?command=sh&container=api&stdin=true&stdout=true&tty=true",
  "verb": "create",
  "user": {
    "username": "alice@example.com",
    "groups": ["platform-admins", "system:authenticated"]
  },
  "sourceIPs": ["198.51.100.23"],
  "userAgent": "kubectl/v1.30.2 (linux/amd64) kubernetes/3968350",
  "objectRef": {
    "resource": "pods",
    "namespace": "shop",
    "name": "api-6c9f7d8b5-q2w8x",
    "apiVersion": "v1",
    "subresource": "exec"
  },
  "responseStatus": { "metadata": {}, "code": 101 },
  "requestReceivedTimestamp": "2026-09-14T09:12:03.412000Z",
  "stageTimestamp": "2026-09-14T09:12:03.448000Z",
  "annotations": {
    "authorization.k8s.io/decision": "allow",
    "authorization.k8s.io/reason": "RBAC: allowed by ClusterRoleBinding \"platform-admins\" of ClusterRole \"cluster-admin\" to Group \"platform-admins\""
  }
}

The full schema is in the kube-apiserver audit configuration reference. Below is what each field is worth to an investigator.

Stages: one request, several events

A request can produce up to four events, one per stage:

StageWhen it is emittedForensic value
RequestReceivedAs soon as the handler receives the requestDuplicate of the final event, without response
ResponseStartedHeaders sent, body not yet (long-running requests: watch, exec, attach, port-forward)The only event for a still-open exec session
ResponseCompleteResponse finishedThe normal "one line per request"
PanicThe handler panickedRare; worth a look if you see it

All the events of one request share the same auditID. Many policies (including the managed ones on EKS and GKE) omit RequestReceived. When counting, fold stages on auditID or you will count each exec twice. The analyzer on the home page does this: it keeps one event per request, and for exec, attach and port-forward keeps the ResponseStarted event.

Levels: how much of the request you get

The audit policy assigns one of four levels per request:

  • None: not logged at all.
  • Metadata: user, source, verb, object, response code. No bodies.
  • Request: metadata plus the request body (requestObject).
  • RequestResponse: plus the response body (responseObject).

The level is written in the event's level field, so you can tell from the data itself whether a missing body means "not sent" or "not logged". This matters: a create on daemonsets at Metadata level tells you that a DaemonSet was created, not that it was privileged. The audit policy article shows how to get bodies where they matter.

Who: user, groups, impersonation

user.username is the authenticated identity. Its shape tells you a lot:

  • system:serviceaccount:<namespace>:<name>: a service account token.
  • system:node:<node-name>: a kubelet.
  • system:anonymous: an unauthenticated request that was let through (anonymous access).
  • An email, an IAM ARN mapping or an Entra ID object: a human or a cloud identity, depending on the platform.

user.groups explains many authorization decisions. user.extra can carry useful identifiers: for service account tokens, recent versions add a credential ID (the token's JTI), which lets you tell two tokens of the same service account apart, as described in the service accounts documentation.

When the caller uses impersonation headers, impersonatedUser holds the identity the request was authorized as. Always read both fields together.

From where: sourceIPs and userAgent

sourceIPs is built from X-Forwarded-For, then X-Real-Ip, then the connection's remote address. The reference is explicit that all but the last IP can be set by the client. Use the last one, and know what sits in front of your API server (a cloud load balancer, a Konnectivity or VPN hop) before drawing conclusions.

userAgent is client-reported and trivially spoofed, but attackers rarely bother. A controller's usual agent looks like kube-controller-manager/v1.30.4 …; a workload using the client library shows its binary name; a person shows kubectl/…. A service account suddenly speaking kubectl/ or curl/ is a strong signal of a token being replayed by hand.

What: verb, objectRef, requestURI

verb is the Kubernetes verb (get, list, watch, create, update, patch, delete, deletecollection), not the HTTP method. objectRef holds resource, namespace, name, apiGroup, apiVersion and subresource.

Three details catch people out:

  1. A list or watch without objectRef.namespace is cluster-wide. On secrets, that returns every secret in the cluster.
  2. On create, objectRef.name may be missing because the name is in the body. At Metadata level you may only see the collection.
  3. Subresources carry the action. pods/exec, pods/attach, pods/portforward, pods/log, serviceaccounts/token, nodes/proxy are all "the dangerous part" of an otherwise boring resource.

requestURI keeps the query string. For exec, that includes every command= argument, even at Metadata level. It also means that secrets passed on an exec command line end up in the audit log, a point raised in kubernetes/kubernetes issue #97795.

Outcome: responseStatus and annotations

responseStatus.code is the HTTP code: 200/201 success, 101 switching protocols for streaming (exec, attach, port-forward), 401 unauthenticated, 403 forbidden, 404 not found, 409 conflict. A burst of 403 from one identity is someone mapping their permissions.

The annotations authorization.k8s.io/decision (allow / forbid) and authorization.k8s.io/reason name the RBAC binding that granted access. The EKS best practices guide recommends using them to see why a call was allowed. In an incident, the reason points straight at the binding you need to remove. Pod Security Admission also writes annotations here when a pod violates a policy in audit mode.

Timestamps

requestReceivedTimestamp is when the request arrived; stageTimestamp is when this stage was reached. Both are UTC with microseconds. For exec sessions, the difference between ResponseStarted and ResponseComplete gives the session duration when both are logged.

Cloud wrappers change the envelope, not the event

On EKS the event is the message of a CloudWatch Logs record. On AKS it is a JSON string in properties.log (storage account, Event Hubs) or in the log_s column (AzureDiagnostics), or spread over columns in the AKSAudit table. On GKE it is rewritten as a Cloud Audit Logs entry: protoPayload.methodName such as io.k8s.core.v1.pods.exec.create, protoPayload.resourceName, authenticationInfo.principalEmail, requestMetadata.callerIp. The export guide covers each. The analyzer maps all of them back to the same fields, so the rest of this series applies whatever the source.

Related articles

The blind spots of Kubernetes audit logs: in-container activity, kubelet and etcd access, policy gaps, spoofable fields, and the evidence that fills them.
Write a Kubernetes audit policy that captures the evidence an investigation needs (exec, RBAC, workloads, tokens) without logging secrets or drowning in noise.
A fictional Kubernetes incident, step by step in the audit log: exposed dashboard token, can-i recon, secret theft, privileged DaemonSet, cluster-admin, XMRig.

This tool is not affiliated with, endorsed by or sponsored by The Linux Foundation, the Cloud Native Computing Foundation (CNCF) or the Kubernetes project. Kubernetes and K8s are registered trademarks of The Linux Foundation. EKS, GKE, AKS and other names are trademarks of their respective owners.