El CKS (Certified Kubernetes Security Specialist) es la tercera y más exigente de las certificaciones de Kubernetes de la CNCF. Certifica que sabes endurecer un clúster, detectar lo que ocurre dentro de él y responder cuando algo va mal. Es la certificación que acredita al perfil que, en Rutas Norte, se asegura de que postgres-reservas —que guarda datos personales de los viajeros— esté cifrado, aislado, auditado y vigilado.

Tiene una particularidad que lo separa de los otros dos exámenes: para presentarte necesitas tener el CKA vigente. No es una recomendación, es un requisito administrativo. Y hay una razón: el CKS asume que ya sabes administrar el clúster y se dedica exclusivamente a asegurarlo, con herramientas externas que no aparecen en los otros exámenes.

Todo el contenido de esta lección tiene un enfoque estrictamente defensivo: endurecer, detectar y responder. No se enseñan ni se describen técnicas ofensivas.

Aviso imprescindible. Precio, duración, número de tareas, porcentaje de aprobado, versión de Kubernetes evaluada y reparto de dominios cambian con el tiempo. Lo que sigue es orientativo. Consulta siempre el temario oficial vigente en la web de la Linux Foundation / CNCF (training.linuxfoundation.org, cncf.io/certification/cks) antes de matricularte.

Aviso de seguridad. Las configuraciones que aparecen aquí son material de estudio orientado a un examen. Antes de aplicar cualquier política de seguridad a un entorno real, debe revisarla un profesional de seguridad con conocimiento del contexto, del modelo de amenazas y de los requisitos regulatorios de la organización.

Contenido

  1. Qué certifica el CKS y el requisito del CKA vigente
  2. El formato del examen y por qué es el más exigente
  3. Los dominios del temario y su peso orientativo
  4. Mapa completo: cada objetivo del CKS y su lección en este curso
  5. Las herramientas que el CKS exige manejar
  6. Procedimientos que hay que ejecutar sin dudar
  7. Plan de estudio y trampas del examen
  8. Ocho tareas tipo CKS resueltas

  1. Qué certifica el CKS y el requisito del CKA vigente

1.1 El perfil

El CKS acredita competencia para asegurar aplicaciones y plataformas basadas en contenedores durante todo su ciclo de vida: construcción, despliegue y ejecución. En términos de Rutas Norte:

Fase Qué se certifica Ejemplo
Construcción Que las imágenes que entran al clúster son limpias y verificables Escanear rutas-norte/api-reservas:2.4 con Trivy y firmarla antes de publicarla
Despliegue Que lo que se admite en el clúster cumple una política Impedir que un pod privilegiado llegue a rutas-norte-pro
Ejecución Que se detecta y se responde a lo anómalo Detectar una shell abierta dentro de un contenedor de producción y aislar el pod
Plataforma Que el clúster en sí está endurecido Cifrar Secrets en etcd, auditar la API, restringir RBAC

1.2 El requisito de partida

Debes tener un CKA vigente (no caducado) en el momento de presentarte al CKS. Consecuencias prácticas:

  • No puedes hacer el CKS primero. El orden es forzoso: CKA → CKS.
  • Si tu CKA caduca (la validez ronda los dos años) antes de que hagas el examen del CKS, no podrás presentarte hasta renovarlo.
  • La certificación CKS, una vez obtenida, tiene su propia validez independiente.
  • Verifica en el handbook oficial cómo se comprueba este requisito y con cuánta antelación.

Consejo de planificación: no dejes pasar demasiado tiempo entre el CKA y el CKS. Además del riesgo de caducidad, el CKS reutiliza mucho del CKA (RBAC, kubeadm, pods estáticos, diagnóstico) y es una pena perder ese estado de forma.

1.3 En qué se diferencia del CKA y del CKAD

Aspecto CKA CKAD CKS
Pregunta central ¿Funciona el clúster? ¿Funciona mi aplicación? ¿Es seguro todo esto?
Herramientas externas Ninguna Ninguna kube-bench, Trivy, Falco, AppArmor, seccomp, Kyverno/OPA, gVisor
Documentación permitida kubernetes.io kubernetes.io kubernetes.io + docs de Trivy, Falco y AppArmor
Prerrequisito Ninguno Ninguno CKA vigente
Nº de tareas orientativo 15-20 15-20 15-16 (menos, pero más largas)
Dificultad percibida Media-alta Media Alta

  1. El formato del examen y por qué es el más exigente

2.1 Características

Aspecto Descripción orientativa
Tipo 100 % práctico, terminal en el navegador, supervisado
Duración En torno a dos horas
Número de tareas Aproximadamente 15-16
Aprobado Aproximadamente un 67 %
Clústeres Varios, con cambio de contexto por tarea
Puntuación Por tarea, con puntuaciones parciales
Documentación permitida kubernetes.io/docs, más las webs oficiales de Trivy, Falco y AppArmor
Prerrequisito CKA vigente
Validez En torno a dos años

Orientativo. Verifícalo en la web oficial, incluida la lista exacta de dominios de documentación permitidos, que ha variado entre revisiones del programa.

2.2 Por qué se considera el más duro

Cuatro razones concretas:

  1. Herramientas fuera de Kubernetes. Tienes que saber invocar kube-bench, leer su salida y arreglar lo que señala; ejecutar trivy image e interpretar severidades; escribir una regla de Falco y saber dónde vive su fichero de configuración. Nada de eso está en kubectl.
  2. Tareas largas. Con ~15 tareas en 120 minutos son ~8 minutos de media, pero una sola tarea puede pedirte editar el manifiesto del apiserver, reiniciar el plano de control y esperar a que vuelva. Los tiempos muertos cuentan.
  3. Trabajo en el nodo, no solo en la API. AppArmor, seccomp, audit-policy, cifrado de etcd y kube-bench se configuran con ssh y sudo sobre ficheros del sistema.
  4. El fallo es catastrófico si te equivocas. Un error en /etc/kubernetes/manifests/kube-apiserver.yaml deja la API caída y bloquea el resto de las tareas de ese clúster. Hay que saber recuperarse.

2.3 La regla que salva el examen: copia de seguridad antes de tocar

Antes de editar cualquier fichero crítico del plano de control:

sudo cp /etc/kubernetes/manifests/kube-apiserver.yaml /root/kube-apiserver.yaml.bak

Si la API no vuelve tras el cambio, restauras y vuelves a intentarlo:

sudo cp /root/kube-apiserver.yaml.bak /etc/kubernetes/manifests/kube-apiserver.yaml

Cuidado con dónde guardas la copia. Nunca dentro de /etc/kubernetes/manifests/: el kubelet intentaría arrancar el fichero .bak como un pod estático adicional y tendrías dos apiservers peleándose por el puerto. Guárdala en /root/ o /tmp/.

2.4 Cómo comprobar que el apiserver ha vuelto

# El plano de control tarda entre 20 y 60 segundos en recuperarse
sudo crictl ps | grep kube-apiserver
kubectl get nodes

Si kubectl responde connection refused, mira los logs del contenedor:

sudo crictl ps -a | grep apiserver
sudo crictl logs <container-id> 2>&1 | tail -20

  1. Los dominios del temario y su peso orientativo

Reparto publicado en el momento de escribir; verifícalo en la web oficial.

Dominio Peso orientativo De qué va
Configuración del clúster (Cluster Setup) ~15 % NetworkPolicies, CIS benchmark, Ingress con TLS, acceso al nodo, verificación de binarios
Endurecimiento del clúster (Cluster Hardening) ~15 % RBAC mínimo, ServiceAccounts, restricción del acceso a la API, actualizaciones frecuentes
Endurecimiento del sistema (System Hardening) ~10 % Minimizar la superficie del SO, IAM, restringir acceso de red, AppArmor, seccomp
Minimizar vulnerabilidades en microservicios ~20 % Contextos de seguridad, OS-level security domains, Secrets, sandboxing (gVisor), mTLS
Cadena de suministro de la imagen (Supply Chain) ~20 % Imágenes base mínimas, listas de registros permitidos, firma y validación, análisis estático, escaneo
Monitorización, registro y seguridad en tiempo de ejecución ~20 % Análisis de comportamiento, detección de amenazas, inmutabilidad, logs de auditoría

Los tres dominios de mayor peso (microservicios, cadena de suministro y runtime, ~60 % entre los tres) son precisamente los que trabajaste en las lecciones 08-02, 08-05 y 08-06.


  1. Mapa completo: cada objetivo del CKS y su lección en este curso

4.1 Configuración del clúster (~15 %)

Objetivo oficial Lección del curso
Usar seguridad de red a nivel de red (NetworkPolicies) 04-06-politicas-de-red, 08-04-seguridad-de-red
Usar el CIS benchmark para revisar la configuración de los componentes 08-06-auditoria-escaneo-y-vulnerabilidades
Configurar objetos Ingress correctamente con seguridad TLS 04-04-controladores-de-ingress, 04-05-tls-y-certificados-con-cert-manager
Proteger los metadatos y los endpoints del nodo 08-04-seguridad-de-red
Minimizar el uso y el acceso a los elementos de la interfaz gráfica 08-01-control-de-acceso-basado-en-roles
Verificar binarios de plataforma antes del despliegue 08-05-seguridad-de-imagenes

4.2 Endurecimiento del clúster (~15 %)

Objetivo oficial Lección del curso
Restringir el acceso a la API de Kubernetes 08-01-control-de-acceso-basado-en-roles, 03-06-serviceaccounts-y-acceso-a-la-api
Usar RBAC para minimizar la exposición 08-01-control-de-acceso-basado-en-roles
Tener cuidado con las ServiceAccounts (deshabilitar la default, tokens) 03-06-serviceaccounts-y-acceso-a-la-api
Actualizar Kubernetes con frecuencia 10-02-kubeadm

4.3 Endurecimiento del sistema (~10 %)

Objetivo oficial Lección del curso
Minimizar la huella del sistema operativo host 08-02-contextos-de-seguridad-y-endurecimiento
Minimizar los roles de IAM 08-01-control-de-acceso-basado-en-roles, 10-06-kubernetes-gestionado-eks-aks-gke
Minimizar el acceso de red externo 04-06-politicas-de-red, 08-04-seguridad-de-red
Usar herramientas de endurecimiento del kernel: AppArmor, seccomp 08-02-contextos-de-seguridad-y-endurecimiento, 08-03-politicas-de-seguridad-de-pods

4.4 Minimizar vulnerabilidades en microservicios (~20 %)

Objetivo oficial Lección del curso
Aplicar dominios de seguridad a nivel de SO (PSA, securityContext) 08-02-contextos-de-seguridad-y-endurecimiento, 08-03-politicas-de-seguridad-de-pods
Gestionar los Secrets de Kubernetes 03-02-secrets, 08-06-auditoria-escaneo-y-vulnerabilidades
Usar entornos de ejecución en sandbox (gVisor, kata) 08-02-contextos-de-seguridad-y-endurecimiento
Implementar cifrado pod-a-pod con mTLS 08-04-seguridad-de-red

4.5 Cadena de suministro de la imagen (~20 %)

Objetivo oficial Lección del curso
Minimizar la huella de la imagen base 08-05-seguridad-de-imagenes
Restringir los registros permitidos 08-05-seguridad-de-imagenes, 08-03-politicas-de-seguridad-de-pods
Firmar y validar imágenes (Cosign) 08-05-seguridad-de-imagenes
Usar análisis estático de las cargas de trabajo 08-06-auditoria-escaneo-y-vulnerabilidades, 11-03-cicd-con-kubernetes
Escanear imágenes en busca de vulnerabilidades conocidas 08-06-auditoria-escaneo-y-vulnerabilidades

4.6 Monitorización, registro y seguridad en tiempo de ejecución (~20 %)

Objetivo oficial Lección del curso
Análisis de comportamiento a nivel de syscall y proceso 08-06-auditoria-escaneo-y-vulnerabilidades (Falco)
Detectar amenazas en infraestructura, apps, redes y almacenamiento 08-06-auditoria-escaneo-y-vulnerabilidades, 07-04-visualizacion-y-alertas-con-grafana
Detectar todas las fases de un ataque, dondequiera que ocurra 08-06-auditoria-escaneo-y-vulnerabilidades, 11-06-operacion-en-produccion
Realizar análisis forense profundo de actividades sospechosas 07-06-depuracion-y-eventos-del-cluster, 08-06-auditoria-escaneo-y-vulnerabilidades
Asegurar la inmutabilidad de los contenedores en ejecución 08-02-contextos-de-seguridad-y-endurecimiento
Usar los logs de auditoría para monitorizar el acceso 08-06-auditoria-escaneo-y-vulnerabilidades

  1. Las herramientas que el CKS exige manejar

Estas nueve piezas aparecieron en el módulo 8. Aquí las repasamos orientadas al examen: el comando exacto, el fichero exacto y la comprobación exacta.

5.1 kube-bench: el estándar CIS

kube-bench comprueba la configuración del clúster contra el CIS Kubernetes Benchmark y te dice qué está mal y cómo arreglarlo.

# Ejecutarlo sobre el nodo de control
kube-bench run --targets=master

# Sobre un nodo trabajador
kube-bench run --targets=node

# Solo un control concreto
kube-bench run --targets=master --check=1.2.20

Salida típica:

[INFO] 1 Control Plane Security Configuration
[INFO] 1.2 API Server
[FAIL] 1.2.20 Ensure that the --profiling argument is set to false (Automated)
[PASS] 1.2.21 Ensure that the --audit-log-path argument is set (Automated)

== Remediations master ==
1.2.20 Edit the API server pod specification file
/etc/kubernetes/manifests/kube-apiserver.yaml
on the control plane node and set the below parameter.
--profiling=false

Lo que hay que saber hacer: leer el [FAIL], ir a la sección == Remediations ==, aplicar exactamente lo que dice en el fichero que indica, y verificar volviendo a ejecutar kube-bench con --check.

Los FAIL más habituales y su arreglo:

Control Arreglo en /etc/kubernetes/manifests/kube-apiserver.yaml
--profiling - --profiling=false
--anonymous-auth - --anonymous-auth=false
--authorization-mode - --authorization-mode=Node,RBAC (nunca AlwaysAllow)
--audit-log-path - --audit-log-path=/var/log/kubernetes/audit.log
--kubelet-certificate-authority Apuntar al CA del clúster
--insecure-port Ya no existe en versiones modernas; si aparece, eliminarlo

Y en el kubelet (/var/lib/kubelet/config.yaml):

authentication:
  anonymous:
    enabled: false          # nunca true
  webhook:
    enabled: true
authorization:
  mode: Webhook             # nunca AlwaysAllow
readOnlyPort: 0             # cerrar el puerto 10255
protectKernelDefaults: true

Tras editarlo: sudo systemctl restart kubelet.

5.2 Trivy: escaneo de imágenes

# Escanear una imagen
trivy image rutas-norte/api-reservas:2.4

# Solo vulnerabilidades críticas y altas
trivy image --severity CRITICAL,HIGH rutas-norte/api-reservas:2.4

# Solo las que tienen parche disponible
trivy image --ignore-unfixed --severity CRITICAL nginx:1.27-alpine

# Salida compacta, ideal para el examen
trivy image --severity CRITICAL --format table --quiet postgres:16

# Escanear manifiestos de Kubernetes (análisis estático)
trivy config /ruta/a/manifiestos/

# Escanear un clúster completo
trivy k8s --report summary cluster

Salida:

rutas-norte/api-reservas:2.4 (alpine 3.20)
==========================================
Total: 3 (CRITICAL: 3)

┌────────────┬────────────────┬──────────┬───────────────────┬───────────────┐
│  Library   │ Vulnerability  │ Severity │ Installed Version │ Fixed Version │
├────────────┼────────────────┼──────────┼───────────────────┼───────────────┤
│ openssl    │ CVE-2024-XXXXX │ CRITICAL │ 3.1.4-r5          │ 3.1.6-r0      │
└────────────┴────────────────┴──────────┴───────────────────┴───────────────┘

Tarea típica del examen: "de estos cuatro pods, elimina los que usen imágenes con vulnerabilidades CRITICAL". Procedimiento:

# 1. Sacar las imágenes de los pods del namespace
kubectl get pods -n rutas-norte-pro \
  -o custom-columns='POD:.metadata.name,IMAGEN:.spec.containers[*].image'

# 2. Escanear cada una
for img in nginx:1.27-alpine postgres:16 redis:7-alpine node:20-alpine; do
  echo "=== $img ==="
  trivy image --severity CRITICAL --quiet "$img" | grep -c CVE || true
done

# 3. Eliminar los que corresponda
kubectl delete pod <nombre> -n rutas-norte-pro

5.3 Falco: detección en tiempo de ejecución

Falco observa las llamadas al sistema y dispara alertas cuando el comportamiento coincide con una regla.

Fichero Contenido
/etc/falco/falco.yaml Configuración general: salidas, formato, ficheros de reglas cargados
/etc/falco/falco_rules.yaml Reglas por defecto (no editar)
/etc/falco/falco_rules.local.yaml Tus reglas y sobrescrituras
/etc/falco/rules.d/ Directorio de reglas adicionales
# Estado y logs
sudo systemctl status falco
sudo journalctl -u falco -f
sudo journalctl -u falco --since "10 minutes ago" | grep Warning

# Recargar tras cambiar reglas
sudo systemctl restart falco

# Validar la sintaxis de las reglas sin arrancar
falco --validate /etc/falco/falco_rules.local.yaml

Una salida típica:

09:14:22.113 Notice A shell was spawned in a container with an attached terminal
  (user=root container_id=a1b2c3d4 container_name=api-reservas
   shell=sh parent=runc cmdline=sh)

Modificar el formato de salida de una regla existente (tarea muy frecuente): se sobrescribe en falco_rules.local.yaml.

# /etc/falco/falco_rules.local.yaml
- rule: Terminal shell in container
  output: >
    %evt.time,%user.name,%container.id,%container.name,%proc.name
  override:
    output: replace

Escribir una regla nueva:

- rule: Escritura sospechosa en /etc dentro de contenedor
  desc: Detecta escrituras en /etc dentro de un contenedor
  condition: >
    container and
    evt.type in (open, openat) and
    evt.is_open_write=true and
    fd.name startswith /etc
  output: >
    Escritura en /etc detectada (usuario=%user.name contenedor=%container.name
    fichero=%fd.name comando=%proc.cmdline)
  priority: WARNING
  tags: [filesystem, container]

Y guardar la salida donde pida el enunciado:

sudo journalctl -u falco --no-pager | grep "Terminal shell" > /opt/incidencias.log

5.4 AppArmor aplicado a un pod

Flujo completo, que hay que saber de memoria:

# 1. En el NODO donde correrá el pod: crear el perfil
sudo tee /etc/apparmor.d/k8s-rutas-norte-deny-write <<'EOF'
#include <tunables/global>

profile k8s-rutas-norte-deny-write flags=(attach_disconnected) {
  #include <abstractions/base>

  file,
  deny /** w,          # denegar toda escritura
}
EOF

# 2. Cargarlo en modo enforce
sudo apparmor_parser -q /etc/apparmor.d/k8s-rutas-norte-deny-write

# 3. Comprobar que está cargado
sudo aa-status | grep rutas-norte
   k8s-rutas-norte-deny-write

4. Aplicarlo al pod. Desde Kubernetes 1.30 hay un campo nativo en securityContext (antes solo existía la anotación):

apiVersion: v1
kind: Pod
metadata:
  name: worker-notificaciones
  namespace: rutas-norte-pro
spec:
  containers:
  - name: worker
    image: busybox:1.36
    command: ["sleep", "3600"]
    securityContext:
      appArmorProfile:
        type: Localhost
        localhostProfile: k8s-rutas-norte-deny-write

Forma antigua, todavía aceptada y la que aparece en muchos enunciados:

metadata:
  annotations:
    container.apparmor.security.beta.kubernetes.io/worker: localhost/k8s-rutas-norte-deny-write

Comprobación:

kubectl exec worker-notificaciones -n rutas-norte-pro -- touch /tmp/prueba
# touch: /tmp/prueba: Permission denied

Trampas: el perfil debe estar cargado en el nodo donde se planifique el pod (usa nodeName o etiquetas si el clúster tiene varios); el nombre en localhostProfile es el nombre del perfil, no la ruta del fichero; type: RuntimeDefault aplica el perfil por defecto del runtime y type: Unconfined desactiva AppArmor.

5.5 seccomp aplicado a un pod

apiVersion: v1
kind: Pod
metadata:
  name: api-reservas-seccomp
  namespace: rutas-norte-pro
spec:
  securityContext:
    seccompProfile:
      type: RuntimeDefault          # el perfil por defecto del runtime: lo más común
  containers:
  - name: api
    image: nginx:1.27-alpine

Con un perfil propio, el fichero debe estar en /var/lib/kubelet/seccomp/ del nodo:

sudo mkdir -p /var/lib/kubelet/seccomp/profiles
sudo tee /var/lib/kubelet/seccomp/profiles/auditar.json <<'EOF'
{
  "defaultAction": "SCMP_ACT_LOG"
}
EOF
    securityContext:
      seccompProfile:
        type: Localhost
        localhostProfile: profiles/auditar.json    # ruta RELATIVA a /var/lib/kubelet/seccomp/
type Significado
RuntimeDefault Perfil por defecto del runtime (bloquea syscalls peligrosas). La respuesta correcta el 80 % de las veces.
Localhost Perfil propio en /var/lib/kubelet/seccomp/<localhostProfile>
Unconfined Sin restricción. Nunca es la respuesta que buscan.

Trampa clásica: poner la ruta absoluta en localhostProfile. Es relativa al directorio seccomp del kubelet.

5.6 Política de auditoría del apiserver

Es la tarea larga por excelencia. Dos partes: escribir la política y conectarla al apiserver.

Parte 1 — la política, en /etc/kubernetes/audit/policy.yaml:

apiVersion: audit.k8s.io/v1
kind: Policy
omitStages:
  - RequestReceived
rules:
# No registrar peticiones de solo lectura ruidosas
- level: None
  verbs: ["get", "list", "watch"]
  resources:
  - group: ""
    resources: ["events"]

# Secrets y ConfigMaps: solo metadatos, nunca el contenido
- level: Metadata
  resources:
  - group: ""
    resources: ["secrets", "configmaps"]

# Pods del namespace crítico: cuerpo completo de petición y respuesta
- level: RequestResponse
  namespaces: ["rutas-norte-pro"]
  resources:
  - group: ""
    resources: ["pods"]

# Todo lo demás, a nivel de metadatos
- level: Metadata

Los cuatro niveles, que hay que distinguir:

Nivel Qué se registra
None Nada
Metadata Quién, qué, cuándo, sobre qué recurso. Sin cuerpos.
Request Metadatos + cuerpo de la petición
RequestResponse Metadatos + cuerpo de petición y de respuesta

Parte 2 — conectarla al apiserver, en /etc/kubernetes/manifests/kube-apiserver.yaml:

spec:
  containers:
  - command:
    - kube-apiserver
    - --audit-policy-file=/etc/kubernetes/audit/policy.yaml
    - --audit-log-path=/var/log/kubernetes/audit/audit.log
    - --audit-log-maxage=30
    - --audit-log-maxbackup=10
    - --audit-log-maxsize=100
    volumeMounts:
    - mountPath: /etc/kubernetes/audit
      name: audit-policy
      readOnly: true
    - mountPath: /var/log/kubernetes/audit
      name: audit-log
      readOnly: false
  volumes:
  - name: audit-policy
    hostPath:
      path: /etc/kubernetes/audit
      type: DirectoryOrCreate
  - name: audit-log
    hostPath:
      path: /var/log/kubernetes/audit
      type: DirectoryOrCreate

La trampa que suspende esta tarea: añadir los argumentos y olvidar los volúmenes. El apiserver corre en un contenedor: si no montas los directorios del host, no ve ni la política ni puede escribir el log, y entra en bucle de reinicio.

Comprobación:

sudo crictl ps | grep kube-apiserver
sudo tail -3 /var/log/kubernetes/audit/audit.log | head -1

5.7 Pod Security Admission

El controlador de admisión integrado que sustituyó a las PodSecurityPolicies. Se activa etiquetando el namespace.

Nivel Qué permite
privileged Todo. Sin restricciones.
baseline Impide lo obviamente peligroso: privilegiados, hostNetwork, hostPID, capacidades añadidas peligrosas
restricted Endurecimiento fuerte: runAsNonRoot, allowPrivilegeEscalation: false, drop: ALL, seccomp RuntimeDefault
Modo Efecto
enforce Rechaza los pods que no cumplen
audit Los permite, pero lo anota en el log de auditoría
warn Los permite y avisa al usuario en el terminal
kubectl label namespace rutas-norte-pro \
  pod-security.kubernetes.io/enforce=restricted \
  pod-security.kubernetes.io/enforce-version=v1.30 \
  pod-security.kubernetes.io/warn=restricted \
  pod-security.kubernetes.io/audit=restricted \
  --overwrite

Comprobación (un pod que no cumple debe ser rechazado):

kubectl run inseguro --image=nginx --privileged -n rutas-norte-pro
Error from server (Forbidden): pods "inseguro" is forbidden:
violates PodSecurity "restricted:v1.30": privileged (container "inseguro"
must not set securityContext.privileged=true), allowPrivilegeEscalation != false,
unrestricted capabilities, runAsNonRoot != true, seccompProfile

Trampa: si el namespace ya tiene pods que no cumplen, enforce no los expulsa; solo bloquea los nuevos. Usa warn y audit para detectar los existentes.

5.8 Kyverno u OPA Gatekeeper

Cuando la política que piden va más allá de lo que PSA cubre (por ejemplo, "solo imágenes del registro corporativo"), se usa un motor de políticas.

Kyverno (sintaxis YAML, más directa):

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: solo-registro-corporativo
spec:
  validationFailureAction: Enforce
  background: false
  rules:
  - name: comprobar-registro
    match:
      any:
      - resources:
          kinds:
          - Pod
          namespaces:
          - rutas-norte-pro
    validate:
      message: "Solo se admiten imagenes de registry.rutasnorte.es"
      pattern:
        spec:
          containers:
          - image: "registry.rutasnorte.es/*"
# Exigir etiquetas obligatorias
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: exigir-etiqueta-propietario
spec:
  validationFailureAction: Enforce
  rules:
  - name: comprobar-propietario
    match:
      any:
      - resources:
          kinds: ["Deployment", "StatefulSet"]
    validate:
      message: "Falta la etiqueta 'propietario'"
      pattern:
        metadata:
          labels:
            propietario: "?*"

OPA Gatekeeper (dos objetos: la plantilla con Rego y la restricción):

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8sallowedrepos
spec:
  crd:
    spec:
      names:
        kind: K8sAllowedRepos
      validation:
        openAPIV3Schema:
          type: object
          properties:
            repos:
              type: array
              items:
                type: string
  targets:
  - target: admission.k8s.gatekeeper.sh
    rego: |
      package k8sallowedrepos
      violation[{"msg": msg}] {
        container := input.review.object.spec.containers[_]
        satisfied := [good | repo := input.parameters.repos[_]
                             good := startswith(container.image, repo)]
        not any(satisfied)
        msg := sprintf("imagen no permitida: %v", [container.image])
      }
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
metadata:
  name: registros-permitidos
spec:
  match:
    kinds:
    - apiGroups: [""]
      kinds: ["Pod"]
    namespaces: ["rutas-norte-pro"]
  parameters:
    repos:
    - "registry.rutasnorte.es/"

Comprobación en ambos casos:

kubectl run prueba --image=docker.io/nginx -n rutas-norte-pro
# Error from server: admission webhook denied the request: ...

5.9 gVisor mediante RuntimeClass

gVisor (runtime runsc) intercepta las llamadas al sistema en espacio de usuario, aislando el contenedor del kernel del host. Se usa cuando la carga es poco confiable.

apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: gvisor
handler: runsc              # debe coincidir con el handler configurado en containerd
apiVersion: v1
kind: Pod
metadata:
  name: carga-no-confiable
  namespace: rutas-norte-pre
spec:
  runtimeClassName: gvisor
  containers:
  - name: app
    image: nginx:1.27-alpine

Comprobación (el kernel visto desde dentro es el de gVisor, no el del host):

kubectl exec carga-no-confiable -n rutas-norte-pre -- dmesg | head -3
[    0.000000] Starting gVisor...

Trampa: la RuntimeClass no instala nada. El handler runsc tiene que estar ya configurado en /etc/containerd/config.toml del nodo. En el examen, normalmente ya lo está y solo hay que crear la RuntimeClass y asignarla.

5.10 NetworkPolicies de denegación por defecto

El patrón que hay que escribir de memoria:

# 1. Denegar TODO en el namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: denegar-todo
  namespace: rutas-norte-pro
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress
# 2. Permitir DNS (imprescindible, se olvida siempre)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: permitir-dns
  namespace: rutas-norte-pro
spec:
  podSelector: {}
  policyTypes:
  - Egress
  egress:
  - to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: kube-system
      podSelector:
        matchLabels:
          k8s-app: kube-dns
    ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53
# 3. Permitir solo lo necesario
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-a-postgres
  namespace: rutas-norte-pro
spec:
  podSelector:
    matchLabels:
      app: api-reservas
  policyTypes:
  - Egress
  egress:
  - to:
    - podSelector:
        matchLabels:
          app: postgres-reservas
    ports:
    - protocol: TCP
      port: 5432

Trampa mortal: aplicar la denegación total y olvidar el DNS. Todo el namespace deja de resolver nombres y los síntomas parecen otra cosa completamente distinta.


  1. Procedimientos que hay que ejecutar sin dudar

Seis coreografías que caen casi seguro. Practícalas hasta que salgan sin documentación.

6.1 ServiceAccount con RBAC mínimo

# 1. La ServiceAccount
kubectl create serviceaccount app-reservas -n rutas-norte-pro

# 2. El Role con el mínimo imprescindible
kubectl create role app-reservas-role \
  --verb=get,list \
  --resource=configmaps \
  --resource-name=config-api \
  -n rutas-norte-pro

# 3. El binding
kubectl create rolebinding app-reservas-binding \
  --role=app-reservas-role \
  --serviceaccount=rutas-norte-pro:app-reservas \
  -n rutas-norte-pro

# 4. Asignarla al Deployment
kubectl set serviceaccount deployment/api-reservas app-reservas -n rutas-norte-pro

Comprobación:

kubectl auth can-i get configmap/config-api \
  --as=system:serviceaccount:rutas-norte-pro:app-reservas -n rutas-norte-pro    # yes
kubectl auth can-i get configmaps \
  --as=system:serviceaccount:rutas-norte-pro:app-reservas -n rutas-norte-pro    # no (list de todos)
kubectl auth can-i create pods \
  --as=system:serviceaccount:rutas-norte-pro:app-reservas -n rutas-norte-pro    # no

El uso de --resource-name es la clave del "mínimo privilegio" y una respuesta muy valorada.

6.2 Desactivar el montaje automático del token

Tres niveles, y hay que saber cuál pide el enunciado:

# A) En la ServiceAccount: afecta a todos los pods que la usen
apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-reservas
  namespace: rutas-norte-pro
automountServiceAccountToken: false
# B) En el Pod: gana sobre la ServiceAccount
apiVersion: v1
kind: Pod
metadata:
  name: api-reservas
  namespace: rutas-norte-pro
spec:
  serviceAccountName: app-reservas
  automountServiceAccountToken: false
  containers:
  - name: api
    image: rutas-norte/api-reservas:2.4
# C) Parchear la ServiceAccount "default" del namespace (tarea muy típica)
kubectl patch serviceaccount default -n rutas-norte-pro \
  -p '{"automountServiceAccountToken": false}'

Comprobación:

kubectl exec api-reservas -n rutas-norte-pro -- ls /var/run/secrets/kubernetes.io/serviceaccount
# ls: ...: No such file or directory

6.3 Cifrar Secrets en reposo en etcd

Paso 1 — el fichero de configuración, en /etc/kubernetes/enc/enc.yaml:

apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
  - secrets
  providers:
  - aescbc:
      keys:
      - name: clave1
        secret: <CLAVE_BASE64_DE_32_BYTES>
  - identity: {}

La clave se genera así:

head -c 32 /dev/urandom | base64

Orden de los proveedores (esto es lo que se pregunta):

  • El primero de la lista es el que se usa para cifrar.
  • Todos se usan para descifrar, en orden.
  • identity: {} significa "sin cifrar". Si va el primero, todo se escribe en claro.

Paso 2 — conectarlo al apiserver:

    - --encryption-provider-config=/etc/kubernetes/enc/enc.yaml
    volumeMounts:
    - name: enc
      mountPath: /etc/kubernetes/enc
      readOnly: true
  volumes:
  - name: enc
    hostPath:
      path: /etc/kubernetes/enc
      type: DirectoryOrCreate

Paso 3 — recifrar los Secrets ya existentes (no se cifran solos):

kubectl get secrets --all-namespaces -o json | kubectl replace -f -

Comprobación (el valor en etcd ya no está en claro):

sudo ETCDCTL_API=3 etcdctl get /registry/secrets/rutas-norte-pro/credenciales-postgres \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key | hexdump -C | head -3
00000000  2f 72 65 67 69 73 74 72  79 2f 73 65 63 72 65 74  |/registry/secret|
00000020  6b 38 73 3a 65 6e 63 3a  61 65 73 63 62 63 3a 76  |k8s:enc:aescbc:v|

La marca k8s:enc:aescbc:v1: confirma que está cifrado. Sin cifrado verías el valor legible.

6.4 Endurecer un pod inseguro que te dan hecho

Esta es la tarea CKS por antonomasia: te dan un manifiesto y te piden dejarlo conforme. La lista de comprobación mental:

Qué buscar Qué poner
privileged: true privileged: false (o eliminar)
hostNetwork, hostPID, hostIPC en true Eliminarlos
hostPath a rutas del sistema Sustituir por emptyDir o eliminar
Falta runAsNonRoot runAsNonRoot: true + runAsUser: <no-0>
Falta allowPrivilegeEscalation allowPrivilegeEscalation: false
Capacidades añadidas (SYS_ADMIN, NET_ADMIN...) capabilities: {drop: ["ALL"]}
Sistema de ficheros escribible readOnlyRootFilesystem: true
Sin perfil seccomp seccompProfile: {type: RuntimeDefault}
Token montado innecesariamente automountServiceAccountToken: false
Puertos privilegiados sin necesidad Revisar

Resultado tipo, conforme al nivel restricted:

apiVersion: v1
kind: Pod
metadata:
  name: api-reservas-endurecido
  namespace: rutas-norte-pro
spec:
  automountServiceAccountToken: false
  securityContext:
    runAsNonRoot: true
    runAsUser: 10001
    runAsGroup: 10001
    fsGroup: 10001
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: api
    image: registry.rutasnorte.es/api-reservas:2.4
    securityContext:
      privileged: false
      allowPrivilegeEscalation: false
      readOnlyRootFilesystem: true
      capabilities:
        drop: ["ALL"]
    volumeMounts:
    - name: tmp
      mountPath: /tmp
  volumes:
  - name: tmp
    emptyDir: {}

Nota práctica: con readOnlyRootFilesystem: true, casi cualquier aplicación necesita un emptyDir en /tmp para funcionar.

6.5 Aislar un pod sospechoso y detener el análisis

Procedimiento de respuesta ante un pod con comportamiento anómalo. Enfoque defensivo: contener, preservar evidencias, restaurar.

# 1. Aislarlo del Service (quitando la etiqueta que lo selecciona)
kubectl label pod api-reservas-abc123 -n rutas-norte-pro app-

# 2. Cortarle la red por completo con una NetworkPolicy dirigida
cat <<'EOF' | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: cuarentena-api-abc123
  namespace: rutas-norte-pro
spec:
  podSelector:
    matchLabels:
      cuarentena: "true"
  policyTypes:
  - Ingress
  - Egress
EOF

kubectl label pod api-reservas-abc123 -n rutas-norte-pro cuarentena=true

Sin reglas ingress ni egress, el pod queda completamente aislado pero sigue vivo para el análisis. Ese es el punto: no lo borres todavía.

# 3. Preservar evidencias
kubectl logs api-reservas-abc123 -n rutas-norte-pro > /opt/evidencias/logs.txt
kubectl describe pod api-reservas-abc123 -n rutas-norte-pro > /opt/evidencias/describe.txt
kubectl get events -n rutas-norte-pro --sort-by=.lastTimestamp > /opt/evidencias/eventos.txt
sudo journalctl -u falco --since "1 hour ago" > /opt/evidencias/falco.txt

# 4. Cuando el análisis ha terminado: eliminarlo
kubectl delete pod api-reservas-abc123 -n rutas-norte-pro --force --grace-period=0

El Deployment recreará un pod limpio. Si la causa era la imagen, hay que corregirla antes.

6.6 Encontrar quién hizo qué en el registro de auditoría

# Buscar acciones de un usuario concreto
sudo grep '"username":"soporte"' /var/log/kubernetes/audit/audit.log | tail -5

# Buscar accesos a Secrets
sudo cat /var/log/kubernetes/audit/audit.log | \
  jq 'select(.objectRef.resource=="secrets") | {user:.user.username, verb:.verb, ns:.objectRef.namespace, name:.objectRef.name, t:.requestReceivedTimestamp}'

# Buscar eliminaciones en un namespace
sudo cat /var/log/kubernetes/audit/audit.log | \
  jq 'select(.verb=="delete" and .objectRef.namespace=="rutas-norte-pro") | {user:.user.username, res:.objectRef.resource, name:.objectRef.name}'

# Sin jq disponible
sudo grep '"verb":"delete"' /var/log/kubernetes/audit/audit.log | \
  grep 'rutas-norte-pro' | tail -3

Salida esperada:

{"user":"soporte","verb":"get","ns":"rutas-norte-pro","name":"credenciales-postgres","t":"2026-08-06T09:12:44Z"}

Los campos que hay que conocer del registro de auditoría:

Campo Contenido
user.username Quién
verb Qué acción (get, list, create, delete, patch)
objectRef.resource Sobre qué tipo de recurso
objectRef.namespace / .name Sobre qué objeto exacto
sourceIPs Desde dónde
responseStatus.code Si tuvo éxito (200/201) o fue denegado (403)
requestReceivedTimestamp Cuándo
level Nivel con el que se registró

  1. Plan de estudio y trampas del examen

7.1 Plan de cuatro semanas (partiendo del CKA aprobado)

Semana Foco Qué hacer
1 Endurecimiento del clúster y del sistema Repasar 08-01, 08-02, 08-03. Ejecutar kube-bench y arreglar todos los FAIL. Practicar AppArmor y seccomp diez veces cada uno.
2 Microservicios y red Repasar 04-06, 08-04. Escribir de memoria: denegar-todo, permitir-DNS, permitir-selectivo. Pod Security Admission en los tres niveles. Kyverno con tres políticas distintas.
3 Cadena de suministro Repasar 08-05, 08-06, 11-03. Escanear con Trivy diez imágenes distintas. Firmar y verificar con Cosign. Política de registros permitidos. Análisis estático de manifiestos.
4 Runtime, auditoría y simulacros Repasar 08-06, 07-06. Escribir tres reglas de Falco. Configurar auditoría desde cero cinco veces. Cifrar etcd desde cero tres veces. Simulacros cronometrados.

Regla clave: las tareas de "editar el apiserver" (auditoría, cifrado, kube-bench) hay que repetirlas hasta que el ciclo completo (editar → esperar → verificar → recuperarse si falla) baje de 8 minutos.

7.2 Las trampas del examen

Trampa Cómo evitarla
Editar el apiserver y dejarlo roto sin darte cuenta Copia de seguridad en /root/ antes, y kubectl get nodes después
Guardar la copia dentro de /etc/kubernetes/manifests/ Se arranca como pod estático adicional. Guárdala fuera.
Configurar auditoría sin montar los volúmenes El apiserver no arranca. Argumentos y volúmenes, siempre.
Cifrar etcd y no recifrar los Secrets existentes Los antiguos siguen en claro. kubectl get secrets -A -o json | kubectl replace -f -
identity: {} primero en la lista de proveedores No cifra nada. El proveedor de cifrado va primero.
Denegar todo el tráfico y olvidar el DNS El namespace deja de funcionar entero
Perfil de AppArmor cargado en el nodo equivocado Fijar el nodo con nodeName o cargarlo en todos
Ruta absoluta en localhostProfile de seccomp Es relativa a /var/lib/kubelet/seccomp/
Aplicar PSA restricted y esperar que expulse pods existentes Solo bloquea los nuevos
Editar falco_rules.yaml en vez de falco_rules.local.yaml Se pierde en la siguiente actualización; y el examen suele pedir el local
No reiniciar el servicio tras cambiar configuración systemctl restart falco / restart kubelet
Perder tiempo instalando herramientas En el examen ya están instaladas. Búscalas con which trivy, which kube-bench.

7.3 Recursos oficiales

Recurso Para qué
Página oficial del CKS (Linux Foundation) Temario vigente, requisitos, precio, dominios de documentación permitidos
cncf/curriculum en GitHub El PDF con los objetivos exactos
kubernetes.io/docs/concepts/security/ La sección de seguridad completa
falco.org/docs/ Permitida en el examen: sintaxis de reglas y campos
aquasecurity.github.io/trivy/ Permitida: flags y formatos de salida
apparmor.net / manual de AppArmor Permitida: sintaxis de perfiles
Simulador incluido con la matrícula Práctica en entorno equivalente

  1. Ocho tareas tipo CKS resueltas

Escenarios de Rutas Norte. Enfoque exclusivamente defensivo.


Tarea 1 — Endurecer un pod inseguro (peso ~7 %, objetivo: 6 min)

Contexto: rutas-norte-pro. El Pod worker-notificaciones corre privilegiado, con hostPID y con capacidades añadidas. Modifícalo para que cumpla el estándar restricted: sin privilegios, sin escalada, sin capacidades, raíz de solo lectura, usuario no root y perfil seccomp por defecto.

kubectl get pod worker-notificaciones -n rutas-norte-pro -o yaml > worker.yaml
vim worker.yaml
apiVersion: v1
kind: Pod
metadata:
  name: worker-notificaciones
  namespace: rutas-norte-pro
spec:
  # hostPID: true  ← ELIMINADO
  securityContext:
    runAsNonRoot: true
    runAsUser: 10001
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: worker
    image: busybox:1.36
    command: ["sleep", "3600"]
    securityContext:
      privileged: false
      allowPrivilegeEscalation: false
      readOnlyRootFilesystem: true
      capabilities:
        drop: ["ALL"]
    volumeMounts:
    - name: tmp
      mountPath: /tmp
  volumes:
  - name: tmp
    emptyDir: {}
kubectl delete pod worker-notificaciones -n rutas-norte-pro
kubectl apply -f worker.yaml

Comprobación:

kubectl get pod worker-notificaciones -n rutas-norte-pro
kubectl exec worker-notificaciones -n rutas-norte-pro -- id
kubectl exec worker-notificaciones -n rutas-norte-pro -- touch /raiz
uid=10001 gid=0(root)
touch: /raiz: Read-only file system

La trampa: privileged, capabilities y readOnlyRootFilesystem van en el securityContext del contenedor; runAsNonRoot y seccompProfile pueden ir en el del pod. Y hay que borrar y recrear el pod: casi todo el securityContext es inmutable.


Tarea 2 — Política de auditoría (peso ~9 %, objetivo: 12 min)

Contexto: rutas-norte-pro. Configura la auditoría del apiserver para registrar a nivel Metadata todo acceso a Secrets, a nivel RequestResponse todo cambio sobre Pods en rutas-norte-pro, y nada para peticiones de lectura de eventos. El log debe ir a /var/log/kubernetes/audit/audit.log y conservarse 15 días.

sudo cp /etc/kubernetes/manifests/kube-apiserver.yaml /root/apiserver.bak
sudo mkdir -p /etc/kubernetes/audit /var/log/kubernetes/audit

sudo tee /etc/kubernetes/audit/policy.yaml <<'EOF'
apiVersion: audit.k8s.io/v1
kind: Policy
omitStages:
  - RequestReceived
rules:
- level: None
  verbs: ["get", "list", "watch"]
  resources:
  - group: ""
    resources: ["events"]
- level: Metadata
  resources:
  - group: ""
    resources: ["secrets"]
- level: RequestResponse
  namespaces: ["rutas-norte-pro"]
  resources:
  - group: ""
    resources: ["pods"]
- level: Metadata
EOF

sudo vim /etc/kubernetes/manifests/kube-apiserver.yaml
    - --audit-policy-file=/etc/kubernetes/audit/policy.yaml
    - --audit-log-path=/var/log/kubernetes/audit/audit.log
    - --audit-log-maxage=15
    volumeMounts:
    - mountPath: /etc/kubernetes/audit
      name: audit-policy
      readOnly: true
    - mountPath: /var/log/kubernetes/audit
      name: audit-log
  volumes:
  - name: audit-policy
    hostPath:
      path: /etc/kubernetes/audit
      type: DirectoryOrCreate
  - name: audit-log
    hostPath:
      path: /var/log/kubernetes/audit
      type: DirectoryOrCreate

Comprobación:

sudo crictl ps | grep kube-apiserver
kubectl get secrets -n rutas-norte-pro
sudo tail -1 /var/log/kubernetes/audit/audit.log | jq '{user:.user.username,res:.objectRef.resource,level:.level}'
{"user":"kubernetes-admin","res":"secrets","level":"Metadata"}

La trampa: las reglas se evalúan en orden y gana la primera que casa. Si pones - level: Metadata (el catch-all) al principio, el resto no se aplica nunca. Y sin los volúmenes, el apiserver no arranca.


Tarea 3 — Cifrar Secrets en etcd (peso ~9 %, objetivo: 12 min)

Contexto: rutas-norte-pro. Cifra los Secrets en reposo usando aescbc. Asegúrate de que el Secret credenciales-postgres, que ya existe, queda cifrado.

sudo cp /etc/kubernetes/manifests/kube-apiserver.yaml /root/apiserver.bak
sudo mkdir -p /etc/kubernetes/enc

CLAVE=$(head -c 32 /dev/urandom | base64)

sudo tee /etc/kubernetes/enc/enc.yaml <<EOF
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
  - secrets
  providers:
  - aescbc:
      keys:
      - name: clave1
        secret: ${CLAVE}
  - identity: {}
EOF

sudo chmod 600 /etc/kubernetes/enc/enc.yaml
sudo vim /etc/kubernetes/manifests/kube-apiserver.yaml
    - --encryption-provider-config=/etc/kubernetes/enc/enc.yaml
    volumeMounts:
    - name: enc
      mountPath: /etc/kubernetes/enc
      readOnly: true
  volumes:
  - name: enc
    hostPath:
      path: /etc/kubernetes/enc
      type: DirectoryOrCreate
# Esperar a que la API vuelva
until kubectl get nodes >/dev/null 2>&1; do sleep 3; done

# Recifrar los Secrets existentes
kubectl get secrets -n rutas-norte-pro -o json | kubectl replace -f -

Comprobación:

sudo ETCDCTL_API=3 etcdctl get /registry/secrets/rutas-norte-pro/credenciales-postgres \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key | hexdump -C | head -2
00000020  6b 38 73 3a 65 6e 63 3a  61 65 73 63 62 63 3a 76  |k8s:enc:aescbc:v|

La trampa: sin el kubectl replace, los Secrets antiguos siguen en claro y la tarea no puntúa. Y identity: {} debe ir después de aescbc, nunca antes.


Tarea 4 — NetworkPolicy de denegación por defecto (peso ~7 %, objetivo: 7 min)

Contexto: rutas-norte-pro. Aplica denegación por defecto de todo el tráfico de entrada y salida en el namespace. Después, permite que los pods sigan resolviendo DNS y que api-reservas alcance a postgres-reservas en el 5432.

cat <<'EOF' | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: denegar-todo
  namespace: rutas-norte-pro
spec:
  podSelector: {}
  policyTypes: [Ingress, Egress]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: permitir-dns
  namespace: rutas-norte-pro
spec:
  podSelector: {}
  policyTypes: [Egress]
  egress:
  - to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: kube-system
    ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-a-postgres
  namespace: rutas-norte-pro
spec:
  podSelector:
    matchLabels:
      app: api-reservas
  policyTypes: [Egress]
  egress:
  - to:
    - podSelector:
        matchLabels:
          app: postgres-reservas
    ports:
    - protocol: TCP
      port: 5432
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: postgres-desde-api
  namespace: rutas-norte-pro
spec:
  podSelector:
    matchLabels:
      app: postgres-reservas
  policyTypes: [Ingress]
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: api-reservas
    ports:
    - protocol: TCP
      port: 5432
EOF

Comprobación:

kubectl exec deploy/api-reservas -n rutas-norte-pro -- nslookup postgres-reservas
kubectl exec deploy/api-reservas -n rutas-norte-pro -- nc -zv postgres-reservas 5432
kubectl exec deploy/tienda-web -n rutas-norte-pro -- nc -zv -w 3 postgres-reservas 5432   # debe fallar

La trampa: hay que abrir las dos direcciones. El egress de api-reservas no basta si postgres-reservas tiene el ingress denegado. Y sin la política de DNS, ni siquiera se resuelve el nombre.


Tarea 5 — Escaneo con Trivy y retirada de imágenes vulnerables (peso ~6 %, objetivo: 6 min)

Contexto: rutas-norte-pre. Escanea las imágenes de los pods del namespace. Elimina los pods cuya imagen tenga vulnerabilidades de severidad CRITICAL. Guarda en /opt/vulnerables.txt la lista de imágenes descartadas.

kubectl get pods -n rutas-norte-pre \
  -o custom-columns='POD:.metadata.name,IMG:.spec.containers[*].image' --no-headers
tienda-web-1     nginx:1.27-alpine
api-reservas-1   node:18.0.0
redis-1          redis:7-alpine
worker-1         ubuntu:20.04
> /opt/vulnerables.txt
for img in nginx:1.27-alpine node:18.0.0 redis:7-alpine ubuntu:20.04; do
  n=$(trivy image --severity CRITICAL --quiet --format json "$img" \
      | jq '[.Results[].Vulnerabilities // []] | flatten | length')
  echo "$img -> CRITICAL: $n"
  [ "$n" -gt 0 ] && echo "$img" >> /opt/vulnerables.txt
done
nginx:1.27-alpine -> CRITICAL: 0
node:18.0.0 -> CRITICAL: 7
redis:7-alpine -> CRITICAL: 0
ubuntu:20.04 -> CRITICAL: 3
kubectl delete pod api-reservas-1 worker-1 -n rutas-norte-pre
cat /opt/vulnerables.txt

La trampa: si los pods pertenecen a un Deployment, borrarlos no sirve: se recrean con la misma imagen. Hay que escalar a 0 o borrar el Deployment, según pida el enunciado. Léelo con cuidado.


Tarea 6 — AppArmor sobre un pod (peso ~7 %, objetivo: 8 min)

Contexto: rutas-norte-pro. En el nodo nodo-1 hay un perfil de AppArmor sin cargar en /opt/perfiles/deny-write. Cárgalo en modo enforce y crea el Pod auditor (imagen busybox:1.36, comando sleep 3600) en ese nodo con ese perfil aplicado.

ssh nodo-1
sudo apparmor_parser -q /opt/perfiles/deny-write
sudo aa-status | grep deny-write
exit
   k8s-deny-write

El nombre que aparece en aa-status es el que hay que usar, no el del fichero.

apiVersion: v1
kind: Pod
metadata:
  name: auditor
  namespace: rutas-norte-pro
spec:
  nodeName: nodo-1
  containers:
  - name: auditor
    image: busybox:1.36
    command: ["sleep", "3600"]
    securityContext:
      appArmorProfile:
        type: Localhost
        localhostProfile: k8s-deny-write

Comprobación:

kubectl get pod auditor -n rutas-norte-pro -o wide
kubectl exec auditor -n rutas-norte-pro -- touch /tmp/x
touch: /tmp/x: Permission denied

La trampa: nodeName: nodo-1 es imprescindible; si el pod se planifica en otro nodo donde el perfil no está cargado, queda en Blocked con el evento cannot enforce AppArmor profile. Y si el pod queda en Pending, comprueba que nodo-1 no esté acordonado.


Tarea 7 — Regla de Falco y captura de la evidencia (peso ~8 %, objetivo: 9 min)

Contexto: rutas-norte-pro. Falco está instalado en nodo-1. Modifica la salida de la regla que detecta shells en contenedores para que muestre exactamente hora,usuario,id-contenedor,nombre-contenedor,proceso, y guarda en /opt/shells.log las detecciones de los últimos 10 minutos.

ssh nodo-1

sudo tee -a /etc/falco/falco_rules.local.yaml <<'EOF'

- rule: Terminal shell in container
  output: >
    %evt.time,%user.name,%container.id,%container.name,%proc.name
  override:
    output: replace
EOF

sudo falco --validate /etc/falco/falco_rules.local.yaml
sudo systemctl restart falco
sudo systemctl status falco --no-pager | head -5
# Provocar una detección para verificar (desde otro terminal)
kubectl exec -it deploy/api-reservas -n rutas-norte-pro -- sh -c "echo prueba"

# Capturar la evidencia
sudo journalctl -u falco --since "10 minutes ago" --no-pager \
  | grep "Terminal shell" > /opt/shells.log
cat /opt/shells.log
09:41:07.882,root,a1b2c3d4e5f6,api-reservas,sh

La trampa: hay que editar falco_rules.local.yaml, nunca falco_rules.yaml. Y sin systemctl restart falco el cambio no surte efecto. Si falco --validate da error de sintaxis, arréglalo antes de reiniciar: un fichero inválido deja el servicio caído.


Tarea 8 — Política de admisión: solo registros permitidos (peso ~8 %, objetivo: 9 min)

Contexto: rutas-norte-pro. Kyverno está instalado. Crea una política que rechace en rutas-norte-pro cualquier Pod cuya imagen no provenga de registry.rutasnorte.es/. Demuestra que funciona.

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: solo-registro-corporativo
spec:
  validationFailureAction: Enforce
  background: false
  rules:
  - name: comprobar-registro
    match:
      any:
      - resources:
          kinds:
          - Pod
          namespaces:
          - rutas-norte-pro
    validate:
      message: "Solo se admiten imagenes de registry.rutasnorte.es/"
      pattern:
        spec:
          =(initContainers):
          - image: "registry.rutasnorte.es/*"
          containers:
          - image: "registry.rutasnorte.es/*"
kubectl apply -f politica-registro.yaml
kubectl get clusterpolicy solo-registro-corporativo

Comprobación:

# Debe ser rechazado
kubectl run prueba-ko --image=docker.io/nginx:1.27 -n rutas-norte-pro
Error from server: admission webhook "validate.kyverno.svc-fail" denied the request:

policy Pod/rutas-norte-pro/prueba-ko for resource violation:
solo-registro-corporativo:
  comprobar-registro: 'validation error: Solo se admiten imagenes de
  registry.rutasnorte.es/'
# Debe ser admitido
kubectl run prueba-ok --image=registry.rutasnorte.es/nginx:1.27 -n rutas-norte-pro

La trampa: validationFailureAction: Audit solo registra; para rechazar hace falta Enforce. Y el prefijo =() en initContainers significa "si existe este campo, valídalo"; sin él, un pod sin initContainers fallaría la validación.


Errores Comunes y Consejos

Errores que cuestan puntos

Error Consecuencia Prevención
Romper el apiserver sin copia de seguridad Pierdes todas las tareas de ese clúster cp a /root/ antes de editar
Copia de seguridad dentro de manifests/ Dos apiservers en conflicto Guárdala fuera de ese directorio
Argumentos de auditoría sin volúmenes El apiserver no arranca Argumentos y volumeMounts y volumes
Regla catch-all al principio de la política de auditoría Las demás no se aplican El orden importa: de específico a general
identity: {} antes del proveedor de cifrado No se cifra nada El cifrador va primero
No recifrar los Secrets existentes La mitad de la tarea sin puntuar kubectl get secrets -A -o json | kubectl replace -f -
Denegar todo y olvidar el DNS El namespace queda inservible Política de DNS siempre junto a la de denegación
Ruta absoluta en localhostProfile (seccomp) El pod no arranca Ruta relativa a /var/lib/kubelet/seccomp/
Editar falco_rules.yaml Cambio en el fichero equivocado Usar falco_rules.local.yaml
Olvidar systemctl restart tras cambiar config El cambio no aplica Reiniciar y verificar con status
Modificar securityContext con kubectl edit Campos inmutables: falla Exportar, borrar, recrear
Borrar pods gestionados por un Deployment Se recrean idénticos Escalar a 0 o corregir la plantilla

Consejos

  1. Repite las tareas de apiserver hasta automatizarlas. Auditoría y cifrado son las que más puntos valen y las que más tiempo consumen si dudas.
  2. Ten un until kubectl get nodes a mano para esperar a que la API vuelva sin quedarte mirando.
  3. Comprueba qué herramientas hay instaladas nada más empezar una tarea que las requiera: which trivy kube-bench falco.
  4. La documentación de Falco y Trivy está permitida. Ten localizada la sección de campos de Falco (%evt.time, %container.name, ...) y la de flags de Trivy.
  5. Verifica siempre con una prueba negativa. No basta con que lo permitido funcione: hay que ver que lo prohibido falla.
  6. Piensa en defensa, no en ataque. El examen mide endurecer, detectar y responder. Todas las respuestas correctas van en esa dirección.
  7. Recuerda el requisito: sin CKA vigente, no hay CKS.

Ejercicios

Ejercicio 1 — Ciclo completo de endurecimiento del apiserver

En un clúster de práctica montado con kubeadm, y cronometrando:

  1. Ejecuta kube-bench run --targets=master y anota todos los [FAIL] de la sección 1.2.
  2. Corrige al menos tres de ellos editando /etc/kubernetes/manifests/kube-apiserver.yaml.
  3. Verifica que la API vuelve y que kube-bench ahora marca [PASS] en esos controles.
  4. Haz copia de seguridad del manifiesto antes de empezar y demuestra que sabes restaurarla.

Objetivo: 20 minutos, con el clúster funcionando al final.

Ejercicio 2 — Respuesta ante un pod sospechoso

Simula el procedimiento completo de contención en rutas-norte-pro:

  1. Crea un Deployment api-reservas con 3 réplicas y un Service que lo seleccione.
  2. Elige uno de los pods como "sospechoso".
  3. Aíslalo del Service sin matarlo.
  4. Córtale toda la comunicación de red con una NetworkPolicy dirigida, dejando el resto del namespace funcionando.
  5. Preserva logs, describe y eventos en /opt/evidencias/.
  6. Comprueba que el Service sigue sirviendo con 2 endpoints y que el pod aislado no tiene conectividad.

Ejercicio 3 — Cadena de suministro de extremo a extremo

Para la imagen nginx:1.27-alpine:

  1. Escanéala con Trivy filtrando por CRITICAL y HIGH con parche disponible.
  2. Crea una política de Kyverno que exija que todos los pods de rutas-norte-pro tengan runAsNonRoot: true y allowPrivilegeEscalation: false.
  3. Demuestra que un pod que no cumple es rechazado y que uno que cumple es admitido.
  4. Activa además Pod Security Admission a nivel restricted en modo warn sobre rutas-norte-pre y comprueba el aviso.

Soluciones

Solución al Ejercicio 1

# 0. Copia de seguridad SIEMPRE primero
sudo cp /etc/kubernetes/manifests/kube-apiserver.yaml /root/apiserver.bak

# 1. Diagnóstico
kube-bench run --targets=master | grep -E '^\[FAIL\]' | head
[FAIL] 1.2.20 Ensure that the --profiling argument is set to false
[FAIL] 1.2.21 Ensure that the --audit-log-path argument is set
[FAIL] 1.2.22 Ensure that the --audit-log-maxage argument is set to 30 or as appropriate
# 2. Corrección
sudo vim /etc/kubernetes/manifests/kube-apiserver.yaml
    - --profiling=false
    - --audit-log-path=/var/log/kubernetes/audit/audit.log
    - --audit-log-maxage=30

Recordando que --audit-log-path necesita su volumen:

    volumeMounts:
    - mountPath: /var/log/kubernetes/audit
      name: audit-log
  volumes:
  - name: audit-log
    hostPath:
      path: /var/log/kubernetes/audit
      type: DirectoryOrCreate
# 3. Esperar y verificar
until kubectl get nodes >/dev/null 2>&1; do sleep 3; done
kubectl get nodes
kube-bench run --targets=master --check=1.2.20,1.2.21,1.2.22
[PASS] 1.2.20 Ensure that the --profiling argument is set to false
[PASS] 1.2.21 Ensure that the --audit-log-path argument is set
[PASS] 1.2.22 Ensure that the --audit-log-maxage argument is set to 30

4. Restauración (para practicar la recuperación):

sudo cp /root/apiserver.bak /etc/kubernetes/manifests/kube-apiserver.yaml
until kubectl get nodes >/dev/null 2>&1; do sleep 3; done

El error habitual en este ejercicio es añadir --audit-log-path sin el volumen: el apiserver arranca y muere en bucle, y el diagnóstico es sudo crictl logs <id>.

Solución al Ejercicio 2

# 1. Montaje
kubectl create deployment api-reservas --image=nginx:1.27-alpine \
  --replicas=3 -n rutas-norte-pro
kubectl expose deployment api-reservas --port=80 -n rutas-norte-pro
kubectl get endpoints api-reservas -n rutas-norte-pro
NAME           ENDPOINTS
api-reservas   10.244.1.4:80,10.244.1.5:80,10.244.2.3:80
# 2-3. Elegir y aislar del Service
SOSPECHOSO=$(kubectl get pods -n rutas-norte-pro -l app=api-reservas \
  -o jsonpath='{.items[0].metadata.name}')
kubectl label pod $SOSPECHOSO -n rutas-norte-pro app-
kubectl label pod $SOSPECHOSO -n rutas-norte-pro cuarentena=true

Ojo: al quitar la etiqueta app, el ReplicaSet lo considera perdido y crea un pod nuevo. Eso es lo deseable: el servicio se recupera solo mientras el sospechoso sigue vivo para el análisis.

# 4. Corte total de red al pod en cuarentena
cat <<'EOF' | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: cuarentena
  namespace: rutas-norte-pro
spec:
  podSelector:
    matchLabels:
      cuarentena: "true"
  policyTypes: [Ingress, Egress]
EOF

# 5. Evidencias
mkdir -p /opt/evidencias
kubectl logs $SOSPECHOSO -n rutas-norte-pro > /opt/evidencias/logs.txt
kubectl describe pod $SOSPECHOSO -n rutas-norte-pro > /opt/evidencias/describe.txt
kubectl get events -n rutas-norte-pro --sort-by=.lastTimestamp > /opt/evidencias/eventos.txt

# 6. Verificación
kubectl get endpoints api-reservas -n rutas-norte-pro    # 3 endpoints, ninguno el sospechoso
kubectl exec $SOSPECHOSO -n rutas-norte-pro -- \
  timeout 3 wget -qO- http://api-reservas || echo "aislado correctamente"
aislado correctamente

La NetworkPolicy sin reglas ingress ni egress es un corte total. El pod sigue existiendo, sus logs se pueden leer (eso pasa por la API, no por su red) y el kubectl exec funciona por el mismo motivo.

Solución al Ejercicio 3

# 1. Escaneo
trivy image --severity CRITICAL,HIGH --ignore-unfixed nginx:1.27-alpine
nginx:1.27-alpine (alpine 3.20.3)
Total: 0 (HIGH: 0, CRITICAL: 0)

--ignore-unfixed es clave: filtra el ruido de vulnerabilidades sin parche disponible, que no puedes remediar actualizando.

# 2. Política de Kyverno
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: exigir-no-root
spec:
  validationFailureAction: Enforce
  background: false
  rules:
  - name: comprobar-securitycontext
    match:
      any:
      - resources:
          kinds: ["Pod"]
          namespaces: ["rutas-norte-pro"]
    validate:
      message: "Los pods deben definir runAsNonRoot=true y allowPrivilegeEscalation=false"
      pattern:
        spec:
          =(securityContext):
            runAsNonRoot: true
          containers:
          - securityContext:
              allowPrivilegeEscalation: false
kubectl apply -f exigir-no-root.yaml

# 3. Prueba negativa
kubectl run malo --image=nginx:1.27-alpine -n rutas-norte-pro
# Error from server: admission webhook denied the request: ...

# 3bis. Prueba positiva
cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: bueno
  namespace: rutas-norte-pro
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 10001
  containers:
  - name: c
    image: nginx:1.27-alpine
    securityContext:
      allowPrivilegeEscalation: false
EOF
kubectl get pod bueno -n rutas-norte-pro
# 4. Pod Security Admission en modo aviso
kubectl label namespace rutas-norte-pre \
  pod-security.kubernetes.io/warn=restricted \
  pod-security.kubernetes.io/warn-version=v1.30 --overwrite

kubectl run avisado --image=nginx:1.27-alpine -n rutas-norte-pre
Warning: would violate PodSecurity "restricted:v1.30": allowPrivilegeEscalation != false,
unrestricted capabilities, runAsNonRoot != true, seccompProfile
pod/avisado created

Fíjate en la diferencia: Kyverno con Enforce rechaza; PSA en modo warn avisa y crea. Saber elegir el mecanismo y el modo según lo que pide el enunciado es exactamente lo que mide este dominio.

Recordatorio: estas políticas son ejercicios de estudio. Antes de aplicar algo parecido a un entorno real, que lo revise un profesional de seguridad con conocimiento del contexto de la organización.


Conclusión

El CKS es la certificación que cierra el triángulo: el CKA demuestra que sabes mantener el clúster en pie, el CKAD que sabes construir sobre él, y el CKS que sabes defenderlo. Es el más exigente de los tres porque te saca de kubectl y te lleva a los ficheros del nodo, a herramientas externas y a decisiones donde equivocarse deja el plano de control caído.

Lo esencial de esta lección:

  • El CKS exige CKA vigente para presentarse. No hay atajo posible.
  • El examen permite, además de kubernetes.io, la documentación de Trivy, Falco y AppArmor. Aprovéchalo.
  • Los tres dominios de mayor peso —microservicios, cadena de suministro y runtime, ~60 % entre los tres— son los del módulo 8 de este curso.
  • Nueve herramientas hay que manejarlas sin dudar: kube-bench, Trivy, Falco, AppArmor, seccomp, audit-policy, Pod Security Admission, Kyverno/OPA y gVisor con RuntimeClass, más las NetworkPolicies de denegación por defecto.
  • Seis procedimientos hay que tenerlos automatizados: RBAC mínimo, desactivar el automontaje del token, cifrar etcd, endurecer un pod, aislar un pod sospechoso y leer el registro de auditoría.
  • Antes de tocar el apiserver: copia de seguridad fuera de manifests/. Después: verificar que la API vuelve.
  • Las trampas que más suspenden: volúmenes olvidados en la auditoría, identity primero en el cifrado, Secrets sin recifrar, DNS bloqueado por la denegación total y perfiles de AppArmor en el nodo equivocado.
  • Consulta siempre el temario oficial vigente en la web de la Linux Foundation / CNCF, incluida la lista de documentación permitida, que ha cambiado entre revisiones.
  • Y recuerda: cualquier configuración de seguridad destinada a un entorno real debe revisarla un profesional de seguridad.

Ya conoces las tres certificaciones, sus temarios y su correspondencia con lo que has estudiado. Lo que falta es la parte que ninguna de las tres enseña y que decide más aprobados de lo que parece: la técnica de examen. En la próxima lección veremos cómo se prepara el entorno de supervisión, qué teclear en los primeros sesenta segundos del examen, cómo usar la documentación permitida sin perder tiempo, cómo repartir los minutos entre tareas, qué errores cuestan más puntos y qué hacer —antes, durante y después— para que el día del examen no haya sorpresas.

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