Kubernetes-Audit-Log-Format: Anatomie eines Audit-Events
Das Kubernetes-Audit-Log-Format Feld für Feld: Stages, Level, user, sourceIPs, objectRef, responseStatus, Annotationen und was davon in der Forensik zählt.
TL;DR. Ein Kubernetes-Audit-Event ist ein JSON-Objekt vom Kind Event (audit.k8s.io/v1), das einen Request in einer bestimmten Stage beschreibt. Die Felder, die Sie in jeder Untersuchung brauchen, sind user, impersonatedUser, sourceIPs, userAgent, verb, objectRef, requestURI, responseStatus.code, die Annotationen authorization.k8s.io/decision und reason und, wenn das Audit-Level es erlaubt, requestObject. Deduplizieren Sie Stages über auditID, vertrauen Sie nur der letzten Quell-IP und denken Sie daran, dass es auf Level Metadata keine Bodies gibt.
Die meisten Leute begegnen dem Audit-Log-Format zum ersten Mal mitten in einem Incident, in einem CloudWatch- oder Log-Analytics-Fenster mit tausenden fast identischen Zeilen. Wer weiß, welche Felder Beweise tragen und welche Rauschen sind, spart Stunden.
Ein vollständiges Event
Hier ein gekürztes Event für ein kubectl exec in einen Pod, so wie der API-Server es mit dem Log-Backend schreibt (ein JSON-Objekt pro Zeile). Namen und Adressen sind fiktiv.
{
"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\""
}
}
Das vollständige Schema steht in der Referenz der kube-apiserver-Audit-Konfiguration. Im Folgenden, was jedes Feld für Ermittler wert ist.
Stages: ein Request, mehrere Events
Ein Request kann bis zu vier Events erzeugen, eines pro Stage:
| Stage | Wann es erzeugt wird | Forensischer Wert |
|---|---|---|
RequestReceived | Sobald der Handler den Request erhält | Duplikat des finalen Events, ohne Antwort |
ResponseStarted | Header gesendet, Body noch nicht (lang laufende Requests: watch, exec, attach, port-forward) | Das einzige Event einer noch offenen exec-Session |
ResponseComplete | Antwort abgeschlossen | Die normale „ein Request“-Zeile |
Panic | Der Handler ist in Panic gegangen | Selten; einen Blick wert, wenn es auftaucht |
Alle Events eines Requests teilen dieselbe auditID. Viele Policies (auch die verwalteten von EKS und GKE) lassen RequestReceived weg. Fassen Sie beim Zählen die Stages über auditID zusammen, sonst zählen Sie jedes exec doppelt. Der Analyzer auf der Startseite macht genau das: Er behält ein Event pro Request und für exec, attach und port-forward das ResponseStarted-Event.
Level: wie viel vom Request Sie bekommen
Die Audit-Policy weist jedem Request eines von vier Levels zu:
None: wird nicht protokolliert.Metadata: Benutzer, Quelle, Verb, Objekt, Response-Code. Keine Bodies.Request: Metadaten plus Request-Body (requestObject).RequestResponse: zusätzlich der Response-Body (responseObject).
Das Level steht im Feld level des Events, Sie können also an den Daten selbst erkennen, ob ein fehlender Body „nicht gesendet“ oder „nicht protokolliert“ bedeutet. Das ist wichtig: Ein create auf daemonsets auf Level Metadata sagt Ihnen, dass ein DaemonSet angelegt wurde, nicht, dass es privilegiert war. Der Artikel zur Audit-Policy zeigt, wie Sie Bodies dort bekommen, wo es darauf ankommt.
Wer: user, groups, Impersonation
user.username ist die authentifizierte Identität. Ihre Form verrät viel:
system:serviceaccount:<namespace>:<name>: ein Service-Account-Token.system:node:<node-name>: ein Kubelet.system:anonymous: ein nicht authentifizierter Request, der durchgelassen wurde (anonymer Zugriff).- Eine E-Mail-Adresse, ein IAM-ARN-Mapping oder ein Entra-ID-Objekt: ein Mensch oder eine Cloud-Identität, je nach Plattform.
user.groups erklärt viele Autorisierungsentscheidungen. user.extra kann nützliche Kennungen tragen: Für Service-Account-Tokens fügen neuere Versionen eine Credential-ID (die JTI des Tokens) hinzu, mit der sich zwei Tokens desselben Service Accounts unterscheiden lassen, wie die Dokumentation zu Service Accounts beschreibt.
Nutzt der Aufrufer Impersonation-Header, enthält impersonatedUser die Identität, unter der der Request autorisiert wurde. Lesen Sie beide Felder immer zusammen.
Woher: sourceIPs und userAgent
sourceIPs wird aus X-Forwarded-For, dann X-Real-Ip, dann der Remote-Adresse der Verbindung gebildet. Die Referenz ist eindeutig: Alle IPs außer der letzten kann der Client setzen. Verwenden Sie die letzte und wissen Sie, was vor Ihrem API-Server sitzt (ein Cloud-Load-Balancer, ein Konnectivity- oder VPN-Hop), bevor Sie Schlüsse ziehen.
userAgent meldet der Client selbst, er ist trivial zu fälschen, aber Angreifer machen sich selten die Mühe. Der übliche Agent eines Controllers sieht aus wie kube-controller-manager/v1.30.4 …; ein Workload mit der Client-Bibliothek zeigt seinen Binary-Namen; ein Mensch zeigt kubectl/…. Ein Service Account, der plötzlich kubectl/ oder curl/ spricht, ist ein starkes Signal für ein von Hand wiederverwendetes Token.
Was: verb, objectRef, requestURI
verb ist das Kubernetes-Verb (get, list, watch, create, update, patch, delete, deletecollection), nicht die HTTP-Methode. objectRef enthält resource, namespace, name, apiGroup, apiVersion und subresource.
Drei Details, die gern übersehen werden:
- Ein
listoderwatchohneobjectRef.namespacegilt clusterweit. Aufsecretsliefert das jedes Secret des Clusters. - Bei
createkannobjectRef.namefehlen, weil der Name im Body steht. Auf LevelMetadatasehen Sie womöglich nur die Collection. - Subressourcen tragen die Aktion.
pods/exec,pods/attach,pods/portforward,pods/log,serviceaccounts/token,nodes/proxysind „der gefährliche Teil“ einer ansonsten langweiligen Ressource.
requestURI behält den Query-String. Bei exec enthält er jedes command=-Argument, auch auf Level Metadata. Das heißt auch: Secrets, die auf der Kommandozeile eines exec übergeben werden, landen im Audit-Log, ein Punkt, der in kubernetes/kubernetes Issue #97795 angesprochen wird.
Ergebnis: responseStatus und Annotationen
responseStatus.code ist der HTTP-Code: 200/201 Erfolg, 101 Protokollwechsel für Streaming (exec, attach, port-forward), 401 nicht authentifiziert, 403 verboten, 404 nicht gefunden, 409 Konflikt. Eine Häufung von 403 für eine Identität ist jemand, der seine Berechtigungen kartiert.
Die Annotationen authorization.k8s.io/decision (allow / forbid) und authorization.k8s.io/reason nennen das RBAC-Binding, das den Zugriff gewährt hat. Der EKS Best Practices Guide empfiehlt, damit nachzuvollziehen, warum ein Aufruf erlaubt wurde. Im Incident zeigt der Grund direkt auf das Binding, das Sie entfernen müssen. Auch Pod Security Admission schreibt hier Annotationen, wenn ein Pod im Audit-Modus gegen eine Policy verstößt.
Zeitstempel
requestReceivedTimestamp ist der Eingang des Requests, stageTimestamp der Zeitpunkt, an dem diese Stage erreicht wurde. Beide in UTC mit Mikrosekunden. Bei exec-Sessions ergibt die Differenz zwischen ResponseStarted und ResponseComplete die Sessiondauer, wenn beide protokolliert werden.
Cloud-Wrapper ändern die Hülle, nicht das Event
Bei EKS ist das Event die message eines CloudWatch-Logs-Eintrags. Bei AKS ist es ein JSON-String in properties.log (Storage Account, Event Hubs) oder in der Spalte log_s (AzureDiagnostics), oder auf Spalten der Tabelle AKSAudit verteilt. Bei GKE wird es als Cloud-Audit-Logs-Eintrag umgeschrieben: protoPayload.methodName wie io.k8s.core.v1.pods.exec.create, protoPayload.resourceName, authenticationInfo.principalEmail, requestMetadata.callerIp. Der Export-Leitfaden behandelt jede Variante. Der Analyzer bildet alle auf dieselben Felder ab, der Rest dieser Serie gilt also unabhängig von der Quelle.