Kubernetes Auditing
El kube-apiserver recibe todas las requests para cualquier interacción con el clúster.
Es posible guardar todas esas requests, datos y metadatos en un datastore llamado audit logs. Podemos utilizar ese histórico para consulta y mejoramiento de la seguridad del clúster, análisis y debug.
Guardar el audit log en un clúster Kubernetes es fundamental para garantizar la seguridad, conformidad y observabilidad del ambiente. El audit log es una pista de auditoría que registra todas las acciones realizadas en el clúster, desde llamadas de API hasta cambios de configuración.
Importancia del Audit Log en Kubernetes
-
Seguridad y Detección de Amenazas: Identificar actividades sospechosas o malintencionadas en el clúster, como intentos de acceso no autorizado, cambios no planificados, o explotación de vulnerabilidades. -
Conformidad y Auditoría: Para muchas organizaciones, especialmente aquellas en sectores regulados (financiero, salud, etc.), es necesario mantener una pista de auditoría de las acciones realizadas en el ambiente. Los logs de auditoría son frecuentemente exigidos por estándares de conformidad como GDPR, HIPAA, PCI-DSS, entre otros. -
Investigación de Incidentes: En caso de fallas o comportamiento inesperado del clúster, el audit log proporciona información valiosa para investigar la causa raíz. Puede mostrar cambios específicos que llevaron al problema, como alteraciones de configuración, escalamientos de privilegio o modificaciones en recursos críticos. -
Monitoreo y Análisis: Fuente importante de datos para monitoreo continuo y análisis de seguridad. Otras herramientas pueden consumir logs de auditoría de Kubernetes para generar alertas en tiempo real y reportes detallados de seguridad. -
Gestión de Políticas: Permite validar si las políticas de seguridad y operacionales están siendo seguidas. Por ejemplo, puede ser usado para monitorear si apenas usuarios autorizados están realizando acciones específicas en el clúster.
Cómo Configurar y Utilizar el Audit Log en Kubernetes
Podemos decidir cuál es el nivel de log que nos gustaría guardar, así como en varias herramientas.
Para guardar el audit log en Kubernetes, es necesario habilitar el recurso de auditoría en el API Server. Esto involucra la configuración de un policy file (archivo de política) que define cuáles eventos deben ser registrados y el nivel de detallamiento.
Stages VS Levels
Los stages en el audit log representan las diferentes fases del ciclo de vida de una solicitud hecha al kube-apiserver. Ayudan a rastrear cómo cada solicitud es procesada, desde el momento en que es recibida hasta el momento en que es concluida o descartada.

Describen en qué punto del procesamiento está una solicitud.
Los stages posibles son:
-
RequestReceived: Este es el primer stage, indicando que el servidor de la API recibió la solicitud, pero aún no la procesó. En ese punto, el servidor apenas registró el recibimiento de la solicitud. -
ResponseStarted: Indica el stage en que el servidor de la API comenzó a enviar una respuesta al cliente, pero aún no completó el envío de la respuesta. Es útil para monitorear solicitudes largas que pueden demorar para ser concluidas. -
ResponseComplete: Stage cuando el servidor de la API completó el procesamiento de la solicitud y envió la respuesta completa de vuelta al cliente. Este es el stage más común para verificar si la solicitud fue exitosa o no. -
Panic: Este stage indica que ocurrió un error crítico o un panic interno en el servidor de la API durante el procesamiento de la solicitud. Es una señal de que algo falló de manera inesperada, y es crucial para fines de depuración y auditoría.
Los Levels determinan la cantidad de información registrada en el log de auditoría en un stage. Es la profundidad o la granularidad de las informaciones auditadas, o sea la verbosidad.
-
None: No registra información. Usado para ignorar completamente la auditoría de una determinada solicitud. -
Metadata: Registra apenas información básica sobre la solicitud, como el método HTTP, camino de la API, tiempo de solicitud, usuario, e información de autenticación. Es útil para una auditoría de alto nivel. -
Request: Incluye el metadata y el contenido del cuerpo de la solicitud (payload). Usado cuando es necesario un rastreo completo de la solicitud, incluyendo los datos enviados y recibidos, pero consumirá más almacenamiento. -
RequestResponse: Incluye el Request y la respuesta, pero es solamente aplicado a requests de recursos.
| Aspecto | Stages | Levels |
|---|---|---|
| Definición | Representan las fases del ciclo de vida de una solicitud en la API de Kubernetes. | Determinan la profundidad de las informaciones a ser registradas en los logs de auditoría. |
| Objetivo | Ayudan a entender en qué punto del procesamiento se encuentra una solicitud. | Controlan la cantidad y el tipo de información registrada para cada solicitud. |
| Tipos | RequestReceived, ResponseStarted, ResponseComplete, Panic. | None, Metadata, Request, RequestResponse. |
| Uso | Para identificar cómo y cuándo una solicitud es procesada y completada. | Para definir el nivel de detalles necesarios para auditoría. |
| Granularidad | Relacionados a la cronología de la solicitud (cuándo es recibida, respondida, o falla). | Relacionados al contenido y detallamiento de la auditoría (apenas encabezado, encabezado y cuerpo, etc.). |
Obviamente que tenemos muchas api requests tendremos mucho más datos para guardar lo que puede contaminar demasiado el log o ser más difícil para buscar. Los requests no son generados solamente por nosotros cuando ejecutamos comando kubectl, sino que son generados por varios componentes de kubernetes que se comunican con él como controller manager, scheduler, kubelet, controladores de terceros, etc.
Audit logs pueden ser rastreados de diferentes modos siendo que en el CKS será usado JSON para no utilizar herramientas de terceros. Es posible definir rotación, tamaño máximo, etc.

En un clúster productivo, dependiendo de cuánta auditoría es necesaria, es interesante enviar esos logs para herramientas que consiguen trabajar con grandes cantidades de datos.
Sabiendo que podemos utilizar los stages y levels, en una policy podemos aplicar cada uno de esos conceptos en diferentes objetos, grupos y hasta verbos en kubernetes.

Activando Audit Logs
Necesitamos pasar el parámetro para activar la auditoría en el kube-apiserver, el parámetro apuntando dónde y cómo debe guardar los logs que serán generados y dónde están las políticas que debe respetar. Vamos a crear una policy simple que guardará todo en level metadata, o sea, sin guardar ningún cuerpo del mensaje, solamente los encabezados.
root@cks-master:~# cd /etc/kubernetes/
root@cks-master:/etc/kubernetes# mkdir audit
root@cks-master:/etc/kubernetes# cd audit/
root@cks-master:/etc/kubernetes# vim policy.yaml
root@cks-master:/etc/kubernetes# cat policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: Metadata
# Ver abajo lo que debe ser editado
root@cks-master:/etc/kubernetes# vim /etc/kubernetes/manifests/kube-apiserver.yaml
Para activar en el kube-apiserver vamos a agregar los siguientes campos.
--audit-log-path: especifica el camino del archivo de log que el backend de log usa para escribir eventos de auditoría. No especificar esta bandera deshabilita el backend de log.--audit-log-maxage: Número máximo de días para mantener archivos de log de auditoría antiguos.--audit-log-maxbackup: Número máximo de archivos de log de auditoría a ser retenidos.--audit-log-maxsize: Tamaño máximo en megabytes del archivo de log de auditoría antes de ser rotado.
spec:
containers:
- command:
- kube-apiserver
...
- --audit-policy-file=/etc/kubernetes/audit/policy.yaml # add
- --audit-log-path=/etc/kubernetes/audit/logs/audit.log # add
- --audit-log-maxsize=500 # add
- --audit-log-maxbackup=5 # add
...
# Acuérdate de montar los volúmenes
volumeMounts:
...
- mountPath: /etc/kubernetes/audit # add
name: audit # add
...
...
volumes:
- hostPath: # add
path: /etc/kubernetes/audit # add
type: DirectoryOrCreate # add
name: audit # add
Ahora vamos a verificar
root@cks-master:/etc/kubernetes/audit# k get pods
# Claro va a generar mucho log, entonces vamos a remover y obtener solo lo que interesa que es la última línea si somos rápidos.
root@cks-master:/etc/kubernetes/audit# cat logs/audit.log
...
{"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"Metadata","auditID":"b09a0a12-a858-4d62-9851-8ff972a4d90c","stage":"RequestReceived","requestURI":"/readyz","verb":"get","user":{"username":"system:anonymous","groups":["system:unauthenticated"]},"sourceIPs":["10.128.0.6"],"userAgent":"kube-probe/1.30","requestReceivedTimestamp":"2024-09-11T14:16:32.059780Z","stageTimestamp":"2024-09-11T14:16:32.059780Z"}
{"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"Metadata","auditID":"b09a0a12-a858-4d62-9851-8ff972a4d90c","stage":"ResponseComplete","requestURI":"/readyz","verb":"get","user":{"username":"system:anonymous","groups":["system:unauthenticated"]},"sourceIPs":["10.128.0.6"],"userAgent":"kube-probe/1.30","responseStatus":{"metadata":{},"code":200},"requestReceivedTimestamp":"2024-09-11T14:16:32.059780Z","stageTimestamp":"2024-09-11T14:16:32.062798Z","annotations":{"authorization.k8s.io/decision":"allow","authorization.k8s.io/reason":"RBAC: allowed by ClusterRoleBinding \"system:public-info-viewer\" of ClusterRole \"system:public-info-viewer\" to Group \"system:unauthenticated\""}}
...
# Vamos a crear un secret y verificar qué tenemos
root@cks-master:/etc/kubernetes/audit# k create secret generic verysecure --from-literal=user=admin
secret/verysecure created
root@cks-master:/etc/kubernetes/audit# cat /etc/kubernetes/audit/logs/audit.log | grep verysecure
{"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"Metadata","auditID":"3fca4b1a-4adc-4719-9246-07204a61f6e6","stage":"ResponseComplete","requestURI":"/api/v1/namespaces/default/secrets?fieldManager=kubectl-create\u0026fieldValidation=Strict","verb":"create","user":{"username":"kubernetes-admin","groups":["kubeadm:cluster-admins","system:authenticated"]},"sourceIPs":["10.128.0.6"],"userAgent":"kubectl/v1.30.3 (linux/amd64) kubernetes/6fc0a69","objectRef":{"resource":"secrets","namespace":"default","name":"verysecure","apiVersion":"v1"},"responseStatus":{"metadata":{},"code":201},"requestReceivedTimestamp":"2024-09-11T14:22:39.543130Z","stageTimestamp":"2024-09-11T14:22:39.557212Z","annotations":{"authorization.k8s.io/decision":"allow","authorization.k8s.io/reason":"RBAC: allowed by ClusterRoleBinding \"kubeadm:cluster-admins\" of ClusterRole \"cluster-admin\" to Group \"kubeadm:cluster-admins\""}}
{
"kind":"Event", //tipo
"apiVersion":"audit.k8s.io/v1",
"level":"Metadata", //Level
"auditID":"3fca4b1a-4adc-4719-9246-07204a61f6e6",
"stage":"ResponseComplete",
"requestURI":"/api/v1/namespaces/default/secrets?fieldManager=kubectl-create\u0026fieldValidation=Strict",
"verb":"create", // El verbo fue create
"user":{
"username":"kubernetes-admin", //Cuál user que hizo
"groups":[
"kubeadm:cluster-admins",
"system:authenticated"
]
},
"sourceIPs":[
"10.128.0.6"
],
"userAgent":"kubectl/v1.30.3 (linux/amd64) kubernetes/6fc0a69",
"objectRef":{
"resource":"secrets", // Cuál el objeto se refiere
"namespace":"default", // Namespace del recurso
"name":"verysecure", // Nombre
"apiVersion":"v1"
},
"responseStatus":{
"metadata":{
},
"code":201
},
"requestReceivedTimestamp":"2024-09-11T14:22:39.543130Z", // Horario que recibió
"stageTimestamp":"2024-09-11T14:22:39.557212Z",
"annotations":{
"authorization.k8s.io/decision":"allow",
"authorization.k8s.io/reason":"RBAC: allowed by ClusterRoleBinding \"kubeadm:cluster-admins\" of ClusterRole \"cluster-admin\" to Group \"kubeadm:cluster-admins\""
}
}
Editando ese secret y agregando el campo password tenemos.
root@cks-master:/etc/kubernetes/audit# k edit secrets verysecure
secret/verysecure edited
root@cks-master:/etc/kubernetes/audit# cat /etc/kubernetes/audit/logs/audit.log | grep verysecure
# Ese fue del primero, en la creación
{"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"Metadata","auditID":"3fca4b1a-4adc-4719-9246-07204a61f6e6","stage":"ResponseComplete","requestURI":"/api/v1/namespaces/default/secrets?fieldManager=kubectl-create\u0026fieldValidation=Strict","verb":"create","user":{"username":"kubernetes-admin","groups":["kubeadm:cluster-admins","system:authenticated"]},"sourceIPs":["10.128.0.6"],"userAgent":"kubectl/v1.30.3 (linux/amd64) kubernetes/6fc0a69","objectRef":{"resource":"secrets","namespace":"default","name":"verysecure","apiVersion":"v1"},"responseStatus":{"metadata":{},"code":201},"requestReceivedTimestamp":"2024-09-11T14:22:39.543130Z","stageTimestamp":"2024-09-11T14:22:39.557212Z","annotations":{"authorization.k8s.io/decision":"allow","authorization.k8s.io/reason":"RBAC: allowed by ClusterRoleBinding \"kubeadm:cluster-admins\" of ClusterRole \"cluster-admin\" to Group \"kubeadm:cluster-admins\""}}
# A la hora de editar generó el evento requestReceived con el get entregando el yaml. Observa que tenemos el request y después el response
{"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"Metadata","auditID":"a3c1a228-f287-4742-9c56-d316b9f0bf3b","stage":"RequestReceived","requestURI":"/api/v1/namespaces/default/secrets/verysecure","verb":"get","user":{"username":"kubernetes-admin","groups":["kubeadm:cluster-admins","system:authenticated"]},"sourceIPs":["10.128.0.6"],"userAgent":"kubectl/v1.30.3 (linux/amd64) kubernetes/6fc0a69","objectRef":{"resource":"secrets","namespace":"default","name":"verysecure","apiVersion":"v1"},"requestReceivedTimestamp":"2024-09-11T15:38:00.424748Z","stageTimestamp":"2024-09-11T15:38:00.424748Z"}
{"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"Metadata","auditID":"a3c1a228-f287-4742-9c56-d316b9f0bf3b","stage":"ResponseComplete","requestURI":"/api/v1/namespaces/default/secrets/verysecure","verb":"get","user":{"username":"kubernetes-admin","groups":["kubeadm:cluster-admins","system:authenticated"]},"sourceIPs":["10.128.0.6"],"userAgent":"kubectl/v1.30.3 (linux/amd64) kubernetes/6fc0a69","objectRef":{"resource":"secrets","namespace":"default","name":"verysecure","apiVersion":"v1"},"responseStatus":{"metadata":{},"code":200},"requestReceivedTimestamp":"2024-09-11T15:38:00.424748Z","stageTimestamp":"2024-09-11T15:38:00.427022Z","annotations":{"authorization.k8s.io/decision":"allow","authorization.k8s.io/reason":"RBAC: allowed by ClusterRoleBinding \"kubeadm:cluster-admins\" of ClusterRole \"cluster-admin\" to Group \"kubeadm:cluster-admins\""}}
# Request del Edit
# Ve el verbo utilizado patch que es utilizado para aplicar alteraciones.
{"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"Metadata","auditID":"dc88feab-4cbf-4643-b1d7-4dc90fe2b59f","stage":"RequestReceived","requestURI":"/api/v1/namespaces/default/secrets/verysecure?fieldManager=kubectl-edit\u0026fieldValidation=Strict","verb":"patch","user":{"username":"kubernetes-admin","groups":["kubeadm:cluster-admins","system:authenticated"]},"sourceIPs":["10.128.0.6"],"userAgent":"kubectl/v1.30.3 (linux/amd64) kubernetes/6fc0a69","objectRef":{"resource":"secrets","namespace":"default","name":"verysecure","apiVersion":"v1"},"requestReceivedTimestamp":"2024-09-11T15:38:23.658311Z","stageTimestamp":"2024-09-11T15:38:23.658311Z"}
# Response del Edit
{"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"Metadata","auditID":"dc88feab-4cbf-4643-b1d7-4dc90fe2b59f","stage":"ResponseComplete","requestURI":"/api/v1/namespaces/default/secrets/verysecure?fieldManager=kubectl-edit\u0026fieldValidation=Strict","verb":"patch","user":{"username":"kubernetes-admin","groups":["kubeadm:cluster-admins","system:authenticated"]},"sourceIPs":["10.128.0.6"],"userAgent":"kubectl/v1.30.3 (linux/amd64) kubernetes/6fc0a69","objectRef":{"resource":"secrets","namespace":"default","name":"verysecure","apiVersion":"v1"},"responseStatus":{"metadata":{},"code":200},"requestReceivedTimestamp":"2024-09-11T15:38:23.658311Z","stageTimestamp":"2024-09-11T15:38:23.663667Z","annotations":{"authorization.k8s.io/decision":"allow","authorization.k8s.io/reason":"RBAC: allowed by ClusterRoleBinding \"kubeadm:cluster-admins\" of ClusterRole \"cluster-admin\" to Group \"kubeadm:cluster-admins\""}}
Creando un Audit Policy
Para evitar la contaminación que tenemos arriba, podemos mejorar nuestra policy.
Por ejemplo, vamos a editar nuestro policy.yaml simple para el siguiente escenario.
- No vamos a loggear nada de RequestReceived para ningún recurso.
- No vamos a loggear nada de get, watch y list para ningún recurso pues eso es solamente consulta.
- Secrets podemos mantener solamente el metadata para no obtener el cuerpo del mensaje.
- Todo el resto vamos a loggear solamente el Response que será de hecho con fue terminada la tarea.
Editar el archivo no genera cambio en el archivo de manifesto del kube-apiserver, o sea, no surtirá efecto pues no fue alterado su archivo de manifesto. Es necesario reiniciar el kube-apiserver forzadamente.
- Desactivar y reactivar el audit log es una opción.
- Matar el contenedor con el crictl.
- Remover el kube-apiserver del /etc/kubernetes/manifests y después colocar de vuelta (Me gusta esta opción).
EL ORDEN IMPORTA.
apiVersion: audit.k8s.io/v1
kind: Policy
# Esto podría ser aplicado a una única regla. En ese caso estamos aplicando en todas.
omitStages:
- "RequestReceived"
rules:
# como no especificamos el stage, será cualquier stage menos el RequestReceived que declaramos arriba.
- level: None
verbs: ["watch","list","get"]
# Como el orden importa, watch list y get arriba serán aplicados aquí
- level: Metadata
resources:
- group: ""
resources: ["secrets"]
# Todo el resto. Inclusive será aplicado en el stage RequestComplete y RequestStarted
- level: RequestResponse
# Removiendo los logs antiguos para renovar nuestro archivo de salida.
root@cks-master:/etc/kubernetes/audit# rm -rf logs/
root@cks-master:/etc/kubernetes/audit# vim policy.yaml
root@cks-master:/etc/kubernetes/audit# cat policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
omitStages:
- "RequestReceived"
rules:
- level: None
verbs: ["watch", "list", "get"]
- level: Metadata
resources:
- group: ""
resources: ["secrets"]
- level: RequestResponse
root@cks-master:/etc/kubernetes/audit# cd ../manifests/
root@cks-master:/etc/kubernetes/manifests# mv kube-apiserver.yaml ..
root@cks-master:/etc/kubernetes/manifests# mv ../kube-apiserver.yaml .
root@cks-master:/etc/kubernetes/manifests#
# Espera subir el kube-apiserver
root@cks-master:/etc/kubernetes/manifests# k get pods
NAME READY STATUS RESTARTS AGE
pod 1/1 Running 0 43h
root@cks-master:/etc/kubernetes/manifests# cat /etc/kubernetes/audit/logs/audit.log | grep RequestReceived
# Nada en la salida
root@cks-master:/etc/kubernetes/manifests# k delete secrets verysecure
secret "verysecure" deleted
# Solo el ResponseComplete
root@cks-master:/etc/kubernetes/manifests# cat /etc/kubernetes/audit/logs/audit.log | grep verysecure
{"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"Metadata","auditID":"79e90895-fe8d-4536-9124-0f0ef08ab69b","stage":"ResponseComplete","requestURI":"/api/v1/namespaces/default/secrets/verysecure","verb":"delete","user":{"username":"kubernetes-admin","groups":["kubeadm:cluster-admins","system:authenticated"]},"sourceIPs":["10.128.0.6"],"userAgent":"kubectl/v1.30.3 (linux/amd64) kubernetes/6fc0a69","objectRef":{"resource":"secrets","namespace":"default","name":"verysecure","apiVersion":"v1"},"responseStatus":{"metadata":{},"status":"Success","details":{"name":"verysecure","kind":"secrets","uid":"0848605a-b6b0-4e8d-aced-35a41e26144c"},"code":200},"requestReceivedTimestamp":"2024-09-11T17:21:22.845033Z","stageTimestamp":"2024-09-11T17:21:22.851421Z","annotations":{"authorization.k8s.io/decision":"allow","authorization.k8s.io/reason":"RBAC: allowed by ClusterRoleBinding \"kubeadm:cluster-admins\" of ClusterRole \"cluster-admin\" to Group \"kubeadm:cluster-admins\""}}
Más modelos para estudio.
apiVersion: audit.k8s.io/v1 # This is required.
kind: Policy
# Don't generate audit events for all requests in RequestReceived stage.
omitStages:
- "RequestReceived"
rules:
# Log pod changes at RequestResponse level
- level: RequestResponse
resources:
- group: ""
# Resource "pods" doesn't match requests to any subresource of pods,
# which is consistent with the RBAC policy.
resources: ["pods"]
# Log "pods/log", "pods/status" at Metadata level
- level: Metadata
resources:
- group: ""
resources: ["pods/log", "pods/status"]
# Don't log requests to a configmap called "controller-leader"
- level: None
resources:
- group: ""
resources: ["configmaps"]
resourceNames: ["controller-leader"]
# Don't log watch requests by the "system:kube-proxy" on endpoints or services
- level: None
users: ["system:kube-proxy"]
verbs: ["watch"]
resources:
- group: "" # core API group
resources: ["endpoints", "services"]
# Don't log authenticated requests to certain non-resource URL paths.
- level: None
userGroups: ["system:authenticated"]
nonResourceURLs:
- "/api*" # Wildcard matching.
- "/version"
# Log the request body of configmap changes in kube-system.
- level: Request
resources:
- group: "" # core API group
resources: ["configmaps"]
# This rule only applies to resources in the "kube-system" namespace.
# The empty string "" can be used to select non-namespaced resources.
namespaces: ["kube-system"]
# Log configmap and secret changes in all other namespaces at the Metadata level.
- level: Metadata
resources:
- group: "" # core API group
resources: ["secrets", "configmaps"]
# Log all other resources in core and extensions at the Request level.
- level: Request
resources:
- group: "" # core API group
- group: "extensions" # Version of group should NOT be included.
# A catch-all rule to log all other requests at the Metadata level.
- level: Metadata
# Long-running requests like watches that fall under this rule will not
# generate an audit event in RequestReceived.
omitStages:
- "RequestReceived"