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
- Qué certifica el CKS y el requisito del CKA vigente
- El formato del examen y por qué es el más exigente
- Los dominios del temario y su peso orientativo
- Mapa completo: cada objetivo del CKS y su lección en este curso
- Las herramientas que el CKS exige manejar
- Procedimientos que hay que ejecutar sin dudar
- Plan de estudio y trampas del examen
- Ocho tareas tipo CKS resueltas
- 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 |
- 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:
- Herramientas fuera de Kubernetes. Tienes que saber invocar
kube-bench, leer su salida y arreglar lo que señala; ejecutartrivy imagee interpretar severidades; escribir una regla de Falco y saber dónde vive su fichero de configuración. Nada de eso está enkubectl. - 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.
- Trabajo en el nodo, no solo en la API. AppArmor, seccomp,
audit-policy, cifrado de etcd ykube-benchse configuran consshysudosobre ficheros del sistema. - El fallo es catastrófico si te equivocas. Un error en
/etc/kubernetes/manifests/kube-apiserver.yamldeja 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:
Si la API no vuelve tras el cambio, restauras y vuelves a intentarlo:
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 nodesSi kubectl responde connection refused, mira los logs del contenedor:
- 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.
- 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 |
- 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.20Salida 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=falseLo 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: trueTras 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 clusterSalida:
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-pro5.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.yamlUna 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: replaceEscribir 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:
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-norte4. 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-writeForma 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-writeComprobación:
kubectl exec worker-notificaciones -n rutas-norte-pro -- touch /tmp/prueba
# touch: /tmp/prueba: Permission deniedTrampas: 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-alpineCon 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: MetadataLos 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: DirectoryOrCreateLa 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:
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 \
--overwriteComprobación (un pod que no cumple debe ser rechazado):
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, seccompProfileTrampa: 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 containerdapiVersion: v1
kind: Pod
metadata:
name: carga-no-confiable
namespace: rutas-norte-pre
spec:
runtimeClassName: gvisor
containers:
- name: app
image: nginx:1.27-alpineComprobación (el kernel visto desde dentro es el de gVisor, no el del host):
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: 5432Trampa 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.
- 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-proComprobació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 # noEl 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 directory6.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í:
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: DirectoryOrCreatePaso 3 — recifrar los Secrets ya existentes (no se cifran solos):
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 -300000000 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=trueSin 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=0El 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 -3Salida 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ó |
- 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 |
- 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 Podworker-notificacionescorre privilegiado, conhostPIDy con capacidades añadidas. Modifícalo para que cumpla el estándarrestricted: sin privilegios, sin escalada, sin capacidades, raíz de solo lectura, usuario no root y perfil seccomp por defecto.
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: {}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 /raizLa 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 nivelMetadatatodo acceso a Secrets, a nivelRequestResponsetodo cambio sobre Pods enrutas-norte-pro, y nada para peticiones de lectura de eventos. El log debe ir a/var/log/kubernetes/audit/audit.logy 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: DirectoryOrCreateComprobació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}'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 usandoaescbc. Asegúrate de que el Secretcredenciales-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 -2La 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 queapi-reservasalcance apostgres-reservasen 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
EOFComprobació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 fallarLa 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.txtla lista de imágenes descartadas.
kubectl get pods -n rutas-norte-pre \
-o custom-columns='POD:.metadata.name,IMG:.spec.containers[*].image' --no-headerstienda-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
donenginx:1.27-alpine -> CRITICAL: 0
node:18.0.0 -> CRITICAL: 7
redis:7-alpine -> CRITICAL: 0
ubuntu:20.04 -> CRITICAL: 3La 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 nodonodo-1hay un perfil de AppArmor sin cargar en/opt/perfiles/deny-write. Cárgalo en modo enforce y crea el Podauditor(imagenbusybox:1.36, comandosleep 3600) en ese nodo con ese perfil aplicado.
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-writeComprobación:
kubectl get pod auditor -n rutas-norte-pro -o wide
kubectl exec auditor -n rutas-norte-pro -- touch /tmp/xLa 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 ennodo-1. Modifica la salida de la regla que detecta shells en contenedores para que muestre exactamentehora,usuario,id-contenedor,nombre-contenedor,proceso, y guarda en/opt/shells.loglas 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.logLa 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 enrutas-norte-procualquier Pod cuya imagen no provenga deregistry.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/*"Comprobación:
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-proLa 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
- 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.
- Ten un
until kubectl get nodesa mano para esperar a que la API vuelva sin quedarte mirando. - Comprueba qué herramientas hay instaladas nada más empezar una tarea que las requiera:
which trivy kube-bench falco. - 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. - Verifica siempre con una prueba negativa. No basta con que lo permitido funcione: hay que ver que lo prohibido falla.
- Piensa en defensa, no en ataque. El examen mide endurecer, detectar y responder. Todas las respuestas correctas van en esa dirección.
- 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:
- Ejecuta
kube-bench run --targets=mastery anota todos los[FAIL]de la sección 1.2. - Corrige al menos tres de ellos editando
/etc/kubernetes/manifests/kube-apiserver.yaml. - Verifica que la API vuelve y que
kube-benchahora marca[PASS]en esos controles. - 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:
- Crea un Deployment
api-reservascon 3 réplicas y un Service que lo seleccione. - Elige uno de los pods como "sospechoso".
- Aíslalo del Service sin matarlo.
- Córtale toda la comunicación de red con una NetworkPolicy dirigida, dejando el resto del namespace funcionando.
- Preserva logs,
describey eventos en/opt/evidencias/. - 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:
- Escanéala con Trivy filtrando por CRITICAL y HIGH con parche disponible.
- Crea una política de Kyverno que exija que todos los pods de
rutas-norte-protenganrunAsNonRoot: trueyallowPrivilegeEscalation: false. - Demuestra que un pod que no cumple es rechazado y que uno que cumple es admitido.
- Activa además Pod Security Admission a nivel
restricteden modowarnsobrerutas-norte-prey 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 appropriateRecordando 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 304. 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; doneEl 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# 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=trueOjo: 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"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
--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: falsekubectl 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-preWarning: would violate PodSecurity "restricted:v1.30": allowPrivilegeEscalation != false,
unrestricted capabilities, runAsNonRoot != true, seccompProfile
pod/avisado createdFí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,
identityprimero 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
- ¿Qué es Kubernetes?
- Arquitectura de Kubernetes
- Conceptos y Terminología Clave
- Configuración de un Clúster de Kubernetes
- La CLI de Kubernetes: kubectl
- Objetos, Manifiestos YAML y el Modelo Declarativo
- El Proyecto del Curso: la Plataforma Rutas Norte
Módulo 2: Componentes Principales de Kubernetes
- Pods
- ReplicaSets
- Deployments
- Actualizaciones, Rollbacks y Estrategias de Despliegue
- Servicios
- Namespaces
- Etiquetas, Selectores y Anotaciones
Módulo 3: Gestión de Configuración y Secretos
- ConfigMaps
- Secrets
- Variables de Entorno
- Cuotas y Límites de Recursos
- LimitRanges y Clases de Calidad de Servicio (QoS)
- ServiceAccounts y Acceso a la API desde los Pods
Módulo 4: Redes en Kubernetes
- Redes de Clúster
- Tipos de Servicios
- DNS Interno y Descubrimiento de Servicios
- Controladores de Ingress
- TLS y Gestión de Certificados con cert-manager
- Políticas de Red
Módulo 5: Almacenamiento en Kubernetes
- Volúmenes
- Volúmenes Persistentes
- Reclamaciones de Volúmenes Persistentes
- Clases de Almacenamiento
- Aprovisionamiento Dinámico, Expansión y Snapshots
- Copias de Seguridad y Restauración de Datos
Módulo 6: Conceptos Avanzados de Kubernetes
- StatefulSets
- DaemonSets
- Trabajos y CronJobs
- Init Containers, Sidecars y Patrones Multi-Contenedor
- Planificación: Afinidad, Taints y Tolerations
- Definiciones de Recursos Personalizados (CRDs)
- Operadores y el Patrón Controlador
Módulo 7: Monitoreo y Registro
- Verificaciones de Salud y Sondas
- Servidor de Métricas y kubectl top
- Monitoreo con Prometheus
- Visualización y Alertas con Grafana y Alertmanager
- Registro Centralizado con Elasticsearch, Fluentd y Kibana (EFK)
- Depuración de Aplicaciones y Eventos del Clúster
Módulo 8: Seguridad en Kubernetes
- Control de Acceso Basado en Roles (RBAC)
- Contextos de Seguridad y Endurecimiento del Contenedor
- Políticas de Seguridad de Pods y Pod Security Standards
- Seguridad de Red
- Seguridad de Imágenes
- Auditoría, Escaneo y Gestión de Vulnerabilidades
Módulo 9: Escalado y Rendimiento
- Autoescalado Horizontal de Pods
- Autoescalado Vertical de Pods
- Autoescalado de Clúster
- Escalado por Eventos y Métricas Personalizadas con KEDA
- Alta Disponibilidad: PodDisruptionBudgets y Topología
- Ajuste de Rendimiento
Módulo 10: Ecosistema y Herramientas de Kubernetes
- Minikube y Entornos Locales con kind
- Kubeadm
- Helm
- Kustomize
- GitOps con Argo CD y Flux
- Kubernetes Gestionado: EKS, AKS y GKE
Módulo 11: Estudios de Caso y Aplicaciones del Mundo Real
- Despliegue de una Aplicación Web
- Ejecución de Aplicaciones con Estado
- CI/CD con Kubernetes
- Estrategias de Despliegue: Blue-Green y Canary
- Gestión Multi-Clúster
- Operación en Producción: Incidencias, Runbooks y Costes
