Formato del audit log de Kubernetes: anatomía de un evento
El formato del audit log de Kubernetes campo a campo: stages, niveles, user, sourceIPs, objectRef, responseStatus, anotaciones y lo que importa en forense.
TL;DR. Un evento de auditoría de Kubernetes es un objeto JSON de kind Event (audit.k8s.io/v1) que describe una solicitud en un stage concreto. Los campos que usarás en cualquier investigación son user, impersonatedUser, sourceIPs, userAgent, verb, objectRef, requestURI, responseStatus.code, las anotaciones authorization.k8s.io/decision y reason y, cuando el nivel de auditoría lo permite, requestObject. Deduplica los stages por auditID, fíate solo de la última IP de origen y recuerda que en el nivel Metadata no hay cuerpos.
La mayoría de la gente se encuentra con el formato del audit log por primera vez en pleno incidente, en una ventana de CloudWatch o Log Analytics, con miles de líneas casi idénticas. Saber qué campos contienen evidencia y cuáles son ruido ahorra horas.
Un evento completo
Este es un evento recortado de un kubectl exec en un pod, tal como lo escribe el API server con el backend de log (un objeto JSON por línea). Los nombres y direcciones son ficticios.
{
"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\""
}
}
El esquema completo está en la referencia de configuración de auditoría de kube-apiserver. A continuación, lo que vale cada campo para quien investiga.
Stages: una solicitud, varios eventos
Una solicitud puede generar hasta cuatro eventos, uno por stage:
| Stage | Cuándo se emite | Valor forense |
|---|---|---|
RequestReceived | En cuanto el handler recibe la solicitud | Duplicado del evento final, sin respuesta |
ResponseStarted | Cabeceras enviadas, cuerpo aún no (solicitudes largas: watch, exec, attach, port-forward) | El único evento de una sesión exec aún abierta |
ResponseComplete | Respuesta terminada | La línea normal de «una solicitud» |
Panic | El handler entró en pánico | Poco frecuente; merece un vistazo si aparece |
Todos los eventos de una solicitud comparten el mismo auditID. Muchas políticas (incluidas las gestionadas de EKS y GKE) omiten RequestReceived. Al contar, agrupa los stages por auditID o contarás cada exec dos veces. El analizador de la página principal lo hace: conserva un evento por solicitud y, para exec, attach y port-forward, se queda con el evento ResponseStarted.
Niveles: cuánto de la solicitud obtienes
La política de auditoría asigna uno de cuatro niveles a cada solicitud:
None: no se registra.Metadata: usuario, origen, verbo, objeto, código de respuesta. Sin cuerpos.Request: metadatos más el cuerpo de la solicitud (requestObject).RequestResponse: además, el cuerpo de la respuesta (responseObject).
El nivel se escribe en el campo level del evento, así que puedes saber a partir de los propios datos si un cuerpo ausente significa «no enviado» o «no registrado». Esto importa: un create sobre daemonsets en nivel Metadata te dice que se creó un DaemonSet, no que fuera privilegiado. El artículo sobre la política de auditoría muestra cómo conseguir los cuerpos donde importan.
Quién: user, groups, impersonation
user.username es la identidad autenticada. Su forma dice mucho:
system:serviceaccount:<namespace>:<name>: un token de cuenta de servicio.system:node:<node-name>: un kubelet.system:anonymous: una solicitud no autenticada que se dejó pasar (acceso anónimo).- Un correo, un mapeo de ARN de IAM o un objeto de Entra ID: una persona o una identidad cloud, según la plataforma.
user.groups explica muchas decisiones de autorización. user.extra puede llevar identificadores útiles: para tokens de cuenta de servicio, las versiones recientes añaden un identificador de credencial (el JTI del token), que permite distinguir dos tokens de la misma cuenta de servicio, como describe la documentación de cuentas de servicio.
Cuando quien llama usa cabeceras de impersonation, impersonatedUser contiene la identidad con la que se autorizó la solicitud. Lee siempre ambos campos juntos.
Desde dónde: sourceIPs y userAgent
sourceIPs se construye a partir de X-Forwarded-For, luego X-Real-Ip y luego la dirección remota de la conexión. La referencia es explícita: todas las IPs salvo la última las puede fijar el cliente. Usa la última y conoce lo que hay delante de tu API server (un balanceador cloud, un salto de Konnectivity o de VPN) antes de sacar conclusiones.
userAgent lo declara el cliente y es trivial de falsificar, pero los atacantes rara vez se molestan. El agente habitual de un controlador se parece a kube-controller-manager/v1.30.4 …; un workload que usa la librería cliente muestra el nombre de su binario; una persona muestra kubectl/…. Una cuenta de servicio que de repente habla kubectl/ o curl/ es una señal fuerte de un token reutilizado a mano.
Qué: verb, objectRef, requestURI
verb es el verbo de Kubernetes (get, list, watch, create, update, patch, delete, deletecollection), no el método HTTP. objectRef contiene resource, namespace, name, apiGroup, apiVersion y subresource.
Tres detalles que despistan:
- Un
listowatchsinobjectRef.namespaceabarca todo el clúster. Sobresecrets, devuelve todos los secrets del clúster. - En un
create, puede faltarobjectRef.name, porque el nombre va en el cuerpo. En nivelMetadataquizá solo veas la colección. - Los subrecursos llevan la acción.
pods/exec,pods/attach,pods/portforward,pods/log,serviceaccounts/token,nodes/proxyson «la parte peligrosa» de un recurso por lo demás aburrido.
requestURI conserva la query string. En un exec incluye cada argumento command=, incluso en nivel Metadata. También significa que los secrets pasados en la línea de comandos de un exec acaban en el audit log, algo señalado en el issue kubernetes/kubernetes #97795.
Resultado: responseStatus y anotaciones
responseStatus.code es el código HTTP: 200/201 éxito, 101 cambio de protocolo para streaming (exec, attach, port-forward), 401 no autenticado, 403 prohibido, 404 no encontrado, 409 conflicto. Una ráfaga de 403 de una misma identidad es alguien mapeando sus permisos.
Las anotaciones authorization.k8s.io/decision (allow / forbid) y authorization.k8s.io/reason nombran el binding RBAC que concedió el acceso. La guía de buenas prácticas de EKS recomienda usarlas para ver por qué se permitió una llamada. En un incidente, la razón apunta directamente al binding que hay que eliminar. Pod Security Admission también escribe anotaciones aquí cuando un pod viola una política en modo audit.
Marcas de tiempo
requestReceivedTimestamp es cuándo llegó la solicitud; stageTimestamp, cuándo se alcanzó este stage. Ambas en UTC con microsegundos. En sesiones exec, la diferencia entre ResponseStarted y ResponseComplete da la duración de la sesión cuando se registran ambas.
Los envoltorios cloud cambian el sobre, no el evento
En EKS, el evento es el message de un registro de CloudWatch Logs. En AKS es una cadena JSON en properties.log (cuenta de almacenamiento, Event Hubs) o en la columna log_s (AzureDiagnostics), o repartida en columnas en la tabla AKSAudit. En GKE se reescribe como entrada de Cloud Audit Logs: protoPayload.methodName como io.k8s.core.v1.pods.exec.create, protoPayload.resourceName, authenticationInfo.principalEmail, requestMetadata.callerIp. La guía de exportación cubre cada caso. El analizador los devuelve todos a los mismos campos, así que el resto de esta serie se aplica sea cual sea la fuente.