El CKS (Certified Kubernetes Security Specialist) és la tercera i més exigent de les certificacions de Kubernetes de la CNCF. Certifica que saps endurir un clúster, detectar el que hi passa a dins i respondre quan alguna cosa va malament. És la certificació que acredita el perfil que, a Rutas Norte, s'assegura que postgres-reserves —que desa dades personals dels viatgers— estigui xifrat, aïllat, auditat i vigilat.

Té una particularitat que el separa dels altres dos exàmens: per presentar-t'hi necessites tenir el CKA vigent. No és una recomanació, és un requisit administratiu. I hi ha una raó: el CKS assumeix que ja saps administrar el clúster i es dedica exclusivament a assegurar-lo, amb eines externes que no apareixen als altres exàmens.

Tot el contingut d'aquesta lliçó té un enfocament estrictament defensiu: endurir, detectar i respondre. No s'ensenyen ni es descriuen tècniques ofensives.

Avís imprescindible. Preu, durada, nombre de tasques, percentatge d'aprovat, versió de Kubernetes avaluada i repartiment de dominis canvien amb el temps. El que segueix és orientatiu. Consulta sempre el temari oficial vigent al web de la Linux Foundation / CNCF (training.linuxfoundation.org, cncf.io/certification/cks) abans de matricular-te.

Avís de seguretat. Les configuracions que apareixen aquí són material d'estudi orientat a un examen. Abans d'aplicar qualsevol política de seguretat a un entorn real, l'ha de revisar un professional de seguretat amb coneixement del context, del model d'amenaces i dels requisits regulatoris de l'organització.

Contingut

  1. Què certifica el CKS i el requisit del CKA vigent
  2. El format de l'examen i per què és el més exigent
  3. Els dominis del temari i el seu pes orientatiu
  4. Mapa complet: cada objectiu del CKS i la seva lliçó en aquest curs
  5. Les eines que el CKS exigeix manejar
  6. Procediments que cal executar sense dubtar
  7. Pla d'estudi i trampes de l'examen
  8. Vuit tasques tipus CKS resoltes

  1. Què certifica el CKS i el requisit del CKA vigent

1.1 El perfil

El CKS acredita competència per assegurar aplicacions i plataformes basades en contenidors durant tot el seu cicle de vida: construcció, desplegament i execució. En termes de Rutas Norte:

Fase Què es certifica Exemple
Construcció Que les imatges que entren al clúster són netes i verificables Escanejar rutas-norte/api-reserves:2.4 amb Trivy i signar-la abans de publicar-la
Desplegament Que el que s'admet al clúster compleix una política Impedir que un pod privilegiat arribi a rutas-norte-pro
Execució Que es detecta i es respon al que és anòmal Detectar una shell oberta dins d'un contenidor de producció i aïllar el pod
Plataforma Que el clúster en si està endurit Xifrar Secrets a etcd, auditar l'API, restringir RBAC

1.2 El requisit de partida

Has de tenir un CKA vigent (no caducat) en el moment de presentar-te al CKS. Conseqüències pràctiques:

  • No pots fer el CKS primer. L'ordre és forçós: CKA → CKS.
  • Si el teu CKA caduca (la validesa ronda els dos anys) abans que facis l'examen del CKS, no t'hi podràs presentar fins a renovar-lo.
  • La certificació CKS, un cop obtinguda, té la seva pròpia validesa independent.
  • Verifica al handbook oficial com es comprova aquest requisit i amb quanta antelació.

Consell de planificació: no deixis passar massa temps entre el CKA i el CKS. A més del risc de caducitat, el CKS reutilitza molt del CKA (RBAC, kubeadm, pods estàtics, diagnòstic) i és una llàstima perdre aquest estat de forma.

1.3 En què es diferencia del CKA i del CKAD

Aspecte CKA CKAD CKS
Pregunta central Funciona el clúster? Funciona la meva aplicació? És segur tot això?
Eines externes Cap Cap kube-bench, Trivy, Falco, AppArmor, seccomp, Kyverno/OPA, gVisor
Documentació permesa kubernetes.io kubernetes.io kubernetes.io + docs de Trivy, Falco i AppArmor
Prerequisit Cap Cap CKA vigent
Nre. de tasques orientatiu 15-20 15-20 15-16 (menys, però més llargues)
Dificultat percebuda Mitjana-alta Mitjana Alta

  1. El format de l'examen i per què és el més exigent

2.1 Característiques

Aspecte Descripció orientativa
Tipus 100 % pràctic, terminal al navegador, supervisat
Durada Al voltant de dues hores
Nombre de tasques Aproximadament 15-16
Aprovat Aproximadament un 67 %
Clústers Uns quants, amb canvi de context per tasca
Puntuació Per tasca, amb puntuacions parcials
Documentació permesa kubernetes.io/docs, més els webs oficials de Trivy, Falco i AppArmor
Prerequisit CKA vigent
Validesa Al voltant de dos anys

Orientatiu. Verifica-ho al web oficial, inclosa la llista exacta de dominis de documentació permesos, que ha variat entre revisions del programa.

2.2 Per què es considera el més dur

Quatre raons concretes:

  1. Eines fora de Kubernetes. Has de saber invocar kube-bench, llegir-ne la sortida i arreglar el que assenyala; executar trivy image i interpretar severitats; escriure una regla de Falco i saber on viu el seu fitxer de configuració. Res d'això és a kubectl.
  2. Tasques llargues. Amb ~15 tasques en 120 minuts són ~8 minuts de mitjana, però una sola tasca pot demanar-te editar el manifest de l'apiserver, reiniciar el pla de control i esperar que torni. Els temps morts compten.
  3. Feina al node, no només a l'API. AppArmor, seccomp, audit-policy, xifratge d'etcd i kube-bench es configuren amb ssh i sudo sobre fitxers del sistema.
  4. La fallada és catastròfica si t'equivoques. Un error a /etc/kubernetes/manifests/kube-apiserver.yaml deixa l'API caiguda i bloqueja la resta de les tasques d'aquell clúster. Cal saber-se'n recuperar.

2.3 La regla que salva l'examen: còpia de seguretat abans de tocar

Abans d'editar qualsevol fitxer crític del pla de control:

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

Si l'API no torna després del canvi, restaures i ho tornes a intentar:

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

Compte amb on deses la còpia. Mai dins de /etc/kubernetes/manifests/: el kubelet intentaria arrencar el fitxer .bak com un pod estàtic addicional i tindries dos apiservers barallant-se pel port. Desa-la a /root/ o /tmp/.

2.4 Com comprovar que l'apiserver ha tornat

# El pla de control triga entre 20 i 60 segons a recuperar-se
sudo crictl ps | grep kube-apiserver
kubectl get nodes

Si kubectl respon connection refused, mira els logs del contenidor:

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

  1. Els dominis del temari i el seu pes orientatiu

Repartiment publicat en el moment d'escriure; verifica'l al web oficial.

Domini Pes orientatiu De què va
Configuració del clúster (Cluster Setup) ~15 % NetworkPolicies, CIS benchmark, Ingress amb TLS, accés al node, verificació de binaris
Enduriment del clúster (Cluster Hardening) ~15 % RBAC mínim, ServiceAccounts, restricció de l'accés a l'API, actualitzacions freqüents
Enduriment del sistema (System Hardening) ~10 % Minimitzar la superfície del SO, IAM, restringir accés de xarxa, AppArmor, seccomp
Minimitzar vulnerabilitats en microserveis ~20 % Contextos de seguretat, OS-level security domains, Secrets, sandboxing (gVisor), mTLS
Cadena de subministrament de la imatge (Supply Chain) ~20 % Imatges base mínimes, llistes de registres permesos, signatura i validació, anàlisi estàtica, escaneig
Monitoratge, registre i seguretat en temps d'execució ~20 % Anàlisi de comportament, detecció d'amenaces, immutabilitat, logs d'auditoria

Els tres dominis de més pes (microserveis, cadena de subministrament i runtime, ~60 % entre els tres) són precisament els que vas treballar a les lliçons 08-02, 08-05 i 08-06.


  1. Mapa complet: cada objectiu del CKS i la seva lliçó en aquest curs

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

Objectiu oficial Lliçó del curs
Fer servir seguretat de xarxa a nivell de xarxa (NetworkPolicies) 04-06-politiques-de-xarxa, 08-04-seguretat-de-xarxa
Fer servir el CIS benchmark per revisar la configuració dels components 08-06-auditoria-escaneig-i-vulnerabilitats
Configurar objectes Ingress correctament amb seguretat TLS 04-04-controladors-dingress, 04-05-tls-i-certificats-amb-cert-manager
Protegir les metadades i els endpoints del node 08-04-seguretat-de-xarxa
Minimitzar l'ús i l'accés als elements de la interfície gràfica 08-01-control-dacces-basat-en-rols
Verificar binaris de plataforma abans del desplegament 08-05-seguretat-dimatges

4.2 Enduriment del clúster (~15 %)

Objectiu oficial Lliçó del curs
Restringir l'accés a l'API de Kubernetes 08-01-control-dacces-basat-en-rols, 03-06-serviceaccounts-i-acces-a-la-api
Fer servir RBAC per minimitzar l'exposició 08-01-control-dacces-basat-en-rols
Anar amb compte amb les ServiceAccounts (deshabilitar la default, tokens) 03-06-serviceaccounts-i-acces-a-la-api
Actualitzar Kubernetes amb freqüència 10-02-kubeadm

4.3 Enduriment del sistema (~10 %)

Objectiu oficial Lliçó del curs
Minimitzar la petjada del sistema operatiu amfitrió 08-02-contextos-de-seguretat-i-enduriment
Minimitzar els rols d'IAM 08-01-control-dacces-basat-en-rols, 10-06-kubernetes-gestionat-eks-aks-gke
Minimitzar l'accés de xarxa extern 04-06-politiques-de-xarxa, 08-04-seguretat-de-xarxa
Fer servir eines d'enduriment del nucli: AppArmor, seccomp 08-02-contextos-de-seguretat-i-enduriment, 08-03-politiques-de-seguretat-de-pods

4.4 Minimitzar vulnerabilitats en microserveis (~20 %)

Objectiu oficial Lliçó del curs
Aplicar dominis de seguretat a nivell de SO (PSA, securityContext) 08-02-contextos-de-seguretat-i-enduriment, 08-03-politiques-de-seguretat-de-pods
Gestionar els Secrets de Kubernetes 03-02-secrets, 08-06-auditoria-escaneig-i-vulnerabilitats
Fer servir entorns d'execució en sandbox (gVisor, kata) 08-02-contextos-de-seguretat-i-enduriment
Implementar xifratge pod-a-pod amb mTLS 08-04-seguretat-de-xarxa

4.5 Cadena de subministrament de la imatge (~20 %)

Objectiu oficial Lliçó del curs
Minimitzar la petjada de la imatge base 08-05-seguretat-dimatges
Restringir els registres permesos 08-05-seguretat-dimatges, 08-03-politiques-de-seguretat-de-pods
Signar i validar imatges (Cosign) 08-05-seguretat-dimatges
Fer servir anàlisi estàtica de les càrregues de treball 08-06-auditoria-escaneig-i-vulnerabilitats, 11-03-cicd-amb-kubernetes
Escanejar imatges a la recerca de vulnerabilitats conegudes 08-06-auditoria-escaneig-i-vulnerabilitats

4.6 Monitoratge, registre i seguretat en temps d'execució (~20 %)

Objectiu oficial Lliçó del curs
Anàlisi de comportament a nivell de syscall i procés 08-06-auditoria-escaneig-i-vulnerabilitats (Falco)
Detectar amenaces en infraestructura, apps, xarxes i emmagatzematge 08-06-auditoria-escaneig-i-vulnerabilitats, 07-04-visualitzacio-i-alertes-amb-grafana
Detectar totes les fases d'un atac, allà on passi 08-06-auditoria-escaneig-i-vulnerabilitats, 11-06-operacio-en-produccio
Fer anàlisi forense profunda d'activitats sospitoses 07-06-depuracio-i-esdeveniments-del-cluster, 08-06-auditoria-escaneig-i-vulnerabilitats
Assegurar la immutabilitat dels contenidors en execució 08-02-contextos-de-seguretat-i-enduriment
Fer servir els logs d'auditoria per monitorar l'accés 08-06-auditoria-escaneig-i-vulnerabilitats

  1. Les eines que el CKS exigeix manejar

Aquestes nou peces van aparèixer al mòdul 8. Aquí les repassem orientades a l'examen: la comanda exacta, el fitxer exacte i la comprovació exacta.

5.1 kube-bench: l'estàndard CIS

kube-bench comprova la configuració del clúster contra el CIS Kubernetes Benchmark i et diu què està malament i com arreglar-ho.

# Executar-lo sobre el node de control
kube-bench run --targets=master

# Sobre un node treballador
kube-bench run --targets=node

# Només un control concret
kube-bench run --targets=master --check=1.2.20

Sortida 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

El que cal saber fer: llegir el [FAIL], anar a la secció == Remediations ==, aplicar exactament el que diu al fitxer que indica, i verificar tornant a executar kube-bench amb --check.

Els FAIL més habituals i el seu arranjament:

Control Arranjament a /etc/kubernetes/manifests/kube-apiserver.yaml
--profiling - --profiling=false
--anonymous-auth - --anonymous-auth=false
--authorization-mode - --authorization-mode=Node,RBAC (mai AlwaysAllow)
--audit-log-path - --audit-log-path=/var/log/kubernetes/audit.log
--kubelet-certificate-authority Apuntar al CA del clúster
--insecure-port Ja no existeix en versions modernes; si apareix, eliminar-lo

I al kubelet (/var/lib/kubelet/config.yaml):

authentication:
  anonymous:
    enabled: false          # mai true
  webhook:
    enabled: true
authorization:
  mode: Webhook             # mai AlwaysAllow
readOnlyPort: 0             # tancar el port 10255
protectKernelDefaults: true

Després d'editar-lo: sudo systemctl restart kubelet.

5.2 Trivy: escaneig d'imatges

# Escanejar una imatge
trivy image rutas-norte/api-reserves:2.4

# Només vulnerabilitats crítiques i altes
trivy image --severity CRITICAL,HIGH rutas-norte/api-reserves:2.4

# Només les que tenen pedaç disponible
trivy image --ignore-unfixed --severity CRITICAL nginx:1.27-alpine

# Sortida compacta, ideal per a l'examen
trivy image --severity CRITICAL --format table --quiet postgres:16

# Escanejar manifests de Kubernetes (anàlisi estàtica)
trivy config /ruta/a/manifests/

# Escanejar un clúster complet
trivy k8s --report summary cluster

Sortida:

rutas-norte/api-reserves: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      │
└────────────┴────────────────┴──────────┴───────────────────┴───────────────┘

Tasca típica de l'examen: "d'aquests quatre pods, elimina els que facin servir imatges amb vulnerabilitats CRITICAL". Procediment:

# 1. Treure les imatges dels pods del namespace
kubectl get pods -n rutas-norte-pro \
  -o custom-columns='POD:.metadata.name,IMATGE:.spec.containers[*].image'

# 2. Escanejar-les una a 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 els que correspongui
kubectl delete pod <nom> -n rutas-norte-pro

5.3 Falco: detecció en temps d'execució

Falco observa les crides al sistema i dispara alertes quan el comportament coincideix amb una regla.

Fitxer Contingut
/etc/falco/falco.yaml Configuració general: sortides, format, fitxers de regles carregats
/etc/falco/falco_rules.yaml Regles per defecte (no editar)
/etc/falco/falco_rules.local.yaml Les teves regles i sobreescriptures
/etc/falco/rules.d/ Directori de regles addicionals
# Estat i logs
sudo systemctl status falco
sudo journalctl -u falco -f
sudo journalctl -u falco --since "10 minutes ago" | grep Warning

# Recarregar després de canviar regles
sudo systemctl restart falco

# Validar la sintaxi de les regles sense arrencar
falco --validate /etc/falco/falco_rules.local.yaml

Una sortida 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-reserves
   shell=sh parent=runc cmdline=sh)

Modificar el format de sortida d'una regla existent (tasca molt freqüent): se sobreescriu a 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

Escriure una regla nova:

- rule: Escriptura sospitosa a /etc dins de contenidor
  desc: Detecta escriptures a /etc dins d un contenidor
  condition: >
    container and
    evt.type in (open, openat) and
    evt.is_open_write=true and
    fd.name startswith /etc
  output: >
    Escriptura a /etc detectada (usuari=%user.name contenidor=%container.name
    fitxer=%fd.name comanda=%proc.cmdline)
  priority: WARNING
  tags: [filesystem, container]

I desar la sortida on demani l'enunciat:

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

5.4 AppArmor aplicat a un pod

Flux complet, que cal saber de memòria:

# 1. Al NODE on 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 tota escriptura
}
EOF

# 2. Carregar-lo en mode enforce
sudo apparmor_parser -q /etc/apparmor.d/k8s-rutas-norte-deny-write

# 3. Comprovar que està carregat
sudo aa-status | grep rutas-norte
   k8s-rutas-norte-deny-write

4. Aplicar-lo al pod. Des de Kubernetes 1.30 hi ha un camp natiu a securityContext (abans només existia l'anotació):

apiVersion: v1
kind: Pod
metadata:
  name: worker-notificacions
  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 antiga, encara acceptada i la que apareix en molts enunciats:

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

Comprovació:

kubectl exec worker-notificacions -n rutas-norte-pro -- touch /tmp/prova
# touch: /tmp/prova: Permission denied

Trampes: el perfil ha d'estar carregat al node on es planifiqui el pod (fes servir nodeName o etiquetes si el clúster en té uns quants); el nom a localhostProfile és el nom del perfil, no la ruta del fitxer; type: RuntimeDefault aplica el perfil per defecte del runtime i type: Unconfined desactiva AppArmor.

5.5 seccomp aplicat a un pod

apiVersion: v1
kind: Pod
metadata:
  name: api-reserves-seccomp
  namespace: rutas-norte-pro
spec:
  securityContext:
    seccompProfile:
      type: RuntimeDefault          # el perfil per defecte del runtime: el més comú
  containers:
  - name: api
    image: nginx:1.27-alpine

Amb un perfil propi, el fitxer ha de ser a /var/lib/kubelet/seccomp/ del node:

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 Significat
RuntimeDefault Perfil per defecte del runtime (bloqueja syscalls perilloses). La resposta correcta el 80 % de les vegades.
Localhost Perfil propi a /var/lib/kubelet/seccomp/<localhostProfile>
Unconfined Sense restricció. Mai és la resposta que busquen.

Trampa clàssica: posar la ruta absoluta a localhostProfile. És relativa al directori seccomp del kubelet.

5.6 Política d'auditoria de l'apiserver

És la tasca llarga per excel·lència. Dues parts: escriure la política i connectar-la a l'apiserver.

Part 1 — la política, a /etc/kubernetes/audit/policy.yaml:

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

# Secrets i ConfigMaps: només metadades, mai el contingut
- level: Metadata
  resources:
  - group: ""
    resources: ["secrets", "configmaps"]

# Pods del namespace crític: cos complet de petició i resposta
- level: RequestResponse
  namespaces: ["rutas-norte-pro"]
  resources:
  - group: ""
    resources: ["pods"]

# Tota la resta, a nivell de metadades
- level: Metadata

Els quatre nivells, que cal distingir:

Nivell Què es registra
None Res
Metadata Qui, què, quan, sobre quin recurs. Sense cossos.
Request Metadades + cos de la petició
RequestResponse Metadades + cos de petició i de resposta

Part 2 — connectar-la a l'apiserver, a /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 suspèn aquesta tasca: afegir els arguments i oblidar els volums. L'apiserver corre en un contenidor: si no muntes els directoris de l'amfitrió, no veu ni la política ni pot escriure el log, i entra en bucle de reinici.

Comprovació:

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

5.7 Pod Security Admission

El controlador d'admissió integrat que va substituir les PodSecurityPolicies. S'activa etiquetant el namespace.

Nivell Què permet
privileged Tot. Sense restriccions.
baseline Impedeix el que és òbviament perillós: privilegiats, hostNetwork, hostPID, capacitats afegides perilloses
restricted Enduriment fort: runAsNonRoot, allowPrivilegeEscalation: false, drop: ALL, seccomp RuntimeDefault
Mode Efecte
enforce Rebutja els pods que no compleixen
audit Els permet, però ho anota al log d'auditoria
warn Els permet i avisa l'usuari al 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

Comprovació (un pod que no compleix ha de ser rebutjat):

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

Trampa: si el namespace ja té pods que no compleixen, enforce no els expulsa; només bloqueja els nous. Fes servir warn i audit per detectar els existents.

5.8 Kyverno o OPA Gatekeeper

Quan la política que demanen va més enllà del que PSA cobreix (per exemple, "només imatges del registre corporatiu"), es fa servir un motor de polítiques.

Kyverno (sintaxi YAML, més directa):

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: nomes-registre-corporatiu
spec:
  validationFailureAction: Enforce
  background: false
  rules:
  - name: comprovar-registre
    match:
      any:
      - resources:
          kinds:
          - Pod
          namespaces:
          - rutas-norte-pro
    validate:
      message: "Es permeten nomes imatges de registry.rutasnorte.es"
      pattern:
        spec:
          containers:
          - image: "registry.rutasnorte.es/*"
# Exigir etiquetes obligatòries
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: exigir-etiqueta-propietari
spec:
  validationFailureAction: Enforce
  rules:
  - name: comprovar-propietari
    match:
      any:
      - resources:
          kinds: ["Deployment", "StatefulSet"]
    validate:
      message: "Falta l'etiqueta 'propietari'"
      pattern:
        metadata:
          labels:
            propietari: "?*"

OPA Gatekeeper (dos objectes: la plantilla amb Rego i la restricció):

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("imatge no permesa: %v", [container.image])
      }
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
metadata:
  name: registres-permesos
spec:
  match:
    kinds:
    - apiGroups: [""]
      kinds: ["Pod"]
    namespaces: ["rutas-norte-pro"]
  parameters:
    repos:
    - "registry.rutasnorte.es/"

Comprovació en tots dos casos:

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

5.9 gVisor mitjançant RuntimeClass

gVisor (runtime runsc) intercepta les crides al sistema en espai d'usuari, i aïlla el contenidor del nucli de l'amfitrió. Es fa servir quan la càrrega és poc fiable.

apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: gvisor
handler: runsc              # ha de coincidir amb el handler configurat a containerd
apiVersion: v1
kind: Pod
metadata:
  name: carrega-no-fiable
  namespace: rutas-norte-pre
spec:
  runtimeClassName: gvisor
  containers:
  - name: app
    image: nginx:1.27-alpine

Comprovació (el nucli vist des de dins és el de gVisor, no el de l'amfitrió):

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

Trampa: la RuntimeClass no instal·la res. El handler runsc ha d'estar ja configurat a /etc/containerd/config.toml del node. A l'examen, normalment ja hi és i només cal crear la RuntimeClass i assignar-la.

5.10 NetworkPolicies de denegació per defecte

El patró que cal escriure de memòria:

# 1. Denegar TOT al namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: denegar-tot
  namespace: rutas-norte-pro
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress
# 2. Permetre DNS (imprescindible, s'oblida sempre)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: permetre-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. Permetre només el que cal
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-a-postgres
  namespace: rutas-norte-pro
spec:
  podSelector:
    matchLabels:
      app: api-reserves
  policyTypes:
  - Egress
  egress:
  - to:
    - podSelector:
        matchLabels:
          app: postgres-reserves
    ports:
    - protocol: TCP
      port: 5432

Trampa mortal: aplicar la denegació total i oblidar el DNS. Tot el namespace deixa de resoldre noms i els símptomes semblen una altra cosa completament diferent.


  1. Procediments que cal executar sense dubtar

Sis coreografies que cauen gairebé segur. Practica-les fins que surtin sense documentació.

6.1 ServiceAccount amb RBAC mínim

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

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

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

# 4. Assignar-la al Deployment
kubectl set serviceaccount deployment/api-reserves app-reserves -n rutas-norte-pro

Comprovació:

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

L'ús de --resource-name és la clau del "mínim privilegi" i una resposta molt valorada.

6.2 Desactivar el muntatge automàtic del token

Tres nivells, i cal saber quin demana l'enunciat:

# A) A la ServiceAccount: afecta tots els pods que la facin servir
apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-reserves
  namespace: rutas-norte-pro
automountServiceAccountToken: false
# B) Al Pod: guanya sobre la ServiceAccount
apiVersion: v1
kind: Pod
metadata:
  name: api-reserves
  namespace: rutas-norte-pro
spec:
  serviceAccountName: app-reserves
  automountServiceAccountToken: false
  containers:
  - name: api
    image: rutas-norte/api-reserves:2.4
# C) Apedaçar la ServiceAccount "default" del namespace (tasca molt típica)
kubectl patch serviceaccount default -n rutas-norte-pro \
  -p '{"automountServiceAccountToken": false}'

Comprovació:

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

6.3 Xifrar Secrets en repòs a etcd

Pas 1 — el fitxer de configuració, a /etc/kubernetes/enc/enc.yaml:

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

La clau es genera així:

head -c 32 /dev/urandom | base64

Ordre dels proveïdors (això és el que es pregunta):

  • El primer de la llista és el que es fa servir per xifrar.
  • Tots es fan servir per desxifrar, en ordre.
  • identity: {} significa "sense xifrar". Si va primer, tot s'escriu en clar.

Pas 2 — connectar-lo a l'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

Pas 3 — rexifrar els Secrets ja existents (no es xifren sols):

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

Comprovació (el valor a etcd ja no està en clar):

sudo ETCDCTL_API=3 etcdctl get /registry/secrets/rutas-norte-pro/credencials-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à xifrat. Sense xifratge veuries el valor llegible.

6.4 Endurir un pod insegur que et donen fet

Aquesta és la tasca CKS per antonomàsia: et donen un manifest i et demanen deixar-lo conforme. La llista de comprovació mental:

Què buscar Què posar
privileged: true privileged: false (o eliminar)
hostNetwork, hostPID, hostIPC en true Eliminar-los
hostPath a rutes del sistema Substituir per emptyDir o eliminar
Falta runAsNonRoot runAsNonRoot: true + runAsUser: <no-0>
Falta allowPrivilegeEscalation allowPrivilegeEscalation: false
Capacitats afegides (SYS_ADMIN, NET_ADMIN...) capabilities: {drop: ["ALL"]}
Sistema de fitxers escrivible readOnlyRootFilesystem: true
Sense perfil seccomp seccompProfile: {type: RuntimeDefault}
Token muntat innecessàriament automountServiceAccountToken: false
Ports privilegiats sense necessitat Revisar

Resultat tipus, conforme al nivell restricted:

apiVersion: v1
kind: Pod
metadata:
  name: api-reserves-endurit
  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-reserves:2.4
    securityContext:
      privileged: false
      allowPrivilegeEscalation: false
      readOnlyRootFilesystem: true
      capabilities:
        drop: ["ALL"]
    volumeMounts:
    - name: tmp
      mountPath: /tmp
  volumes:
  - name: tmp
    emptyDir: {}

Nota pràctica: amb readOnlyRootFilesystem: true, gairebé qualsevol aplicació necessita un emptyDir a /tmp per funcionar.

6.5 Aïllar un pod sospitós i aturar l'anàlisi

Procediment de resposta davant d'un pod amb comportament anòmal. Enfocament defensiu: contenir, preservar evidències, restaurar.

# 1. Aïllar-lo del Service (traient l'etiqueta que el selecciona)
kubectl label pod api-reserves-abc123 -n rutas-norte-pro app-

# 2. Tallar-li la xarxa completament amb una NetworkPolicy dirigida
cat <<'EOF' | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: quarantena-api-abc123
  namespace: rutas-norte-pro
spec:
  podSelector:
    matchLabels:
      quarantena: "true"
  policyTypes:
  - Ingress
  - Egress
EOF

kubectl label pod api-reserves-abc123 -n rutas-norte-pro quarantena=true

Sense regles ingress ni egress, el pod queda completament aïllat però continua viu per a l'anàlisi. Aquest és el punt: no l'esborris encara.

# 3. Preservar evidències
kubectl logs api-reserves-abc123 -n rutas-norte-pro > /opt/evidencies/logs.txt
kubectl describe pod api-reserves-abc123 -n rutas-norte-pro > /opt/evidencies/describe.txt
kubectl get events -n rutas-norte-pro --sort-by=.lastTimestamp > /opt/evidencies/esdeveniments.txt
sudo journalctl -u falco --since "1 hour ago" > /opt/evidencies/falco.txt

# 4. Quan l'anàlisi ha acabat: eliminar-lo
kubectl delete pod api-reserves-abc123 -n rutas-norte-pro --force --grace-period=0

El Deployment recrearà un pod net. Si la causa era la imatge, cal corregir-la abans.

6.6 Trobar qui va fer què al registre d'auditoria

# Buscar accions d'un usuari concret
sudo grep '"username":"suport"' /var/log/kubernetes/audit/audit.log | tail -5

# Buscar accessos 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 eliminacions 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}'

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

Sortida esperada:

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

Els camps que cal conèixer del registre d'auditoria:

Camp Contingut
user.username Qui
verb Quina acció (get, list, create, delete, patch)
objectRef.resource Sobre quin tipus de recurs
objectRef.namespace / .name Sobre quin objecte exacte
sourceIPs Des d'on
responseStatus.code Si va tenir èxit (200/201) o va ser denegat (403)
requestReceivedTimestamp Quan
level Nivell amb què es va registrar

  1. Pla d'estudi i trampes de l'examen

7.1 Pla de quatre setmanes (partint del CKA aprovat)

Setmana Focus Què fer
1 Enduriment del clúster i del sistema Repassar 08-01, 08-02, 08-03. Executar kube-bench i arreglar tots els FAIL. Practicar AppArmor i seccomp deu vegades cadascun.
2 Microserveis i xarxa Repassar 04-06, 08-04. Escriure de memòria: denegar-tot, permetre-DNS, permetre-selectiu. Pod Security Admission en els tres nivells. Kyverno amb tres polítiques diferents.
3 Cadena de subministrament Repassar 08-05, 08-06, 11-03. Escanejar amb Trivy deu imatges diferents. Signar i verificar amb Cosign. Política de registres permesos. Anàlisi estàtica de manifests.
4 Runtime, auditoria i simulacres Repassar 08-06, 07-06. Escriure tres regles de Falco. Configurar auditoria des de zero cinc vegades. Xifrar etcd des de zero tres vegades. Simulacres cronometrats.

Regla clau: les tasques d'"editar l'apiserver" (auditoria, xifratge, kube-bench) cal repetir-les fins que el cicle complet (editar → esperar → verificar → recuperar-se si falla) baixi de 8 minuts.

7.2 Les trampes de l'examen

Trampa Com evitar-la
Editar l'apiserver i deixar-lo trencat sense adonar-te'n Còpia de seguretat a /root/ abans, i kubectl get nodes després
Desar la còpia dins de /etc/kubernetes/manifests/ S'arrenca com a pod estàtic addicional. Desa-la fora.
Configurar auditoria sense muntar els volums L'apiserver no arrenca. Arguments i volums, sempre.
Xifrar etcd i no rexifrar els Secrets existents Els antics continuen en clar. kubectl get secrets -A -o json | kubectl replace -f -
identity: {} primer a la llista de proveïdors No xifra res. El proveïdor de xifratge va primer.
Denegar tot el trànsit i oblidar el DNS El namespace deixa de funcionar sencer
Perfil d'AppArmor carregat al node equivocat Fixar el node amb nodeName o carregar-lo a tots
Ruta absoluta a localhostProfile de seccomp És relativa a /var/lib/kubelet/seccomp/
Aplicar PSA restricted i esperar que expulsi pods existents Només bloqueja els nous
Editar falco_rules.yaml en lloc de falco_rules.local.yaml Es perd a la següent actualització; i l'examen sol demanar el local
No reiniciar el servei després de canviar configuració systemctl restart falco / restart kubelet
Perdre temps instal·lant eines A l'examen ja hi són instal·lades. Busca-les amb which trivy, which kube-bench.

7.3 Recursos oficials

Recurs Per a què
Pàgina oficial del CKS (Linux Foundation) Temari vigent, requisits, preu, dominis de documentació permesos
cncf/curriculum a GitHub El PDF amb els objectius exactes
kubernetes.io/docs/concepts/security/ La secció de seguretat completa
falco.org/docs/ Permesa a l'examen: sintaxi de regles i camps
aquasecurity.github.io/trivy/ Permesa: flags i formats de sortida
apparmor.net / manual d'AppArmor Permesa: sintaxi de perfils
Simulador inclòs amb la matrícula Pràctica en entorn equivalent

  1. Vuit tasques tipus CKS resoltes

Escenaris de Rutas Norte. Enfocament exclusivament defensiu.


Tasca 1 — Endurir un pod insegur (pes ~7 %, objectiu: 6 min)

Context: rutas-norte-pro. El Pod worker-notificacions corre privilegiat, amb hostPID i amb capacitats afegides. Modifica'l perquè compleixi l'estàndard restricted: sense privilegis, sense escalada, sense capacitats, arrel de només lectura, usuari no root i perfil seccomp per defecte.

kubectl get pod worker-notificacions -n rutas-norte-pro -o yaml > worker.yaml
vim worker.yaml
apiVersion: v1
kind: Pod
metadata:
  name: worker-notificacions
  namespace: rutas-norte-pro
spec:
  # hostPID: true  ← ELIMINAT
  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-notificacions -n rutas-norte-pro
kubectl apply -f worker.yaml

Comprovació:

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

La trampa: privileged, capabilities i readOnlyRootFilesystem van al securityContext del contenidor; runAsNonRoot i seccompProfile poden anar al del pod. I cal esborrar i recrear el pod: gairebé tot el securityContext és immutable.


Tasca 2 — Política d'auditoria (pes ~9 %, objectiu: 12 min)

Context: rutas-norte-pro. Configura l'auditoria de l'apiserver per registrar a nivell Metadata tot accés a Secrets, a nivell RequestResponse tot canvi sobre Pods a rutas-norte-pro, i res per a peticions de lectura d'esdeveniments. El log ha d'anar a /var/log/kubernetes/audit/audit.log i conservar-se 15 dies.

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

Comprovació:

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: les regles s'avaluen en ordre i guanya la primera que casa. Si poses - level: Metadata (el catch-all) al principi, la resta no s'aplica mai. I sense els volums, l'apiserver no arrenca.


Tasca 3 — Xifrar Secrets a etcd (pes ~9 %, objectiu: 12 min)

Context: rutas-norte-pro. Xifra els Secrets en repòs fent servir aescbc. Assegura't que el Secret credencials-postgres, que ja existeix, queda xifrat.

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

CLAU=$(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: clau1
        secret: ${CLAU}
  - 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 que l'API torni
until kubectl get nodes >/dev/null 2>&1; do sleep 3; done

# Rexifrar els Secrets existents
kubectl get secrets -n rutas-norte-pro -o json | kubectl replace -f -

Comprovació:

sudo ETCDCTL_API=3 etcdctl get /registry/secrets/rutas-norte-pro/credencials-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: sense el kubectl replace, els Secrets antics continuen en clar i la tasca no puntua. I identity: {} ha d'anar després d'aescbc, mai abans.


Tasca 4 — NetworkPolicy de denegació per defecte (pes ~7 %, objectiu: 7 min)

Context: rutas-norte-pro. Aplica denegació per defecte de tot el trànsit d'entrada i sortida al namespace. Després, permet que els pods continuïn resolent DNS i que api-reserves arribi a postgres-reserves al 5432.

cat <<'EOF' | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: denegar-tot
  namespace: rutas-norte-pro
spec:
  podSelector: {}
  policyTypes: [Ingress, Egress]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: permetre-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-reserves
  policyTypes: [Egress]
  egress:
  - to:
    - podSelector:
        matchLabels:
          app: postgres-reserves
    ports:
    - protocol: TCP
      port: 5432
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: postgres-des-de-api
  namespace: rutas-norte-pro
spec:
  podSelector:
    matchLabels:
      app: postgres-reserves
  policyTypes: [Ingress]
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: api-reserves
    ports:
    - protocol: TCP
      port: 5432
EOF

Comprovació:

kubectl exec deploy/api-reserves -n rutas-norte-pro -- nslookup postgres-reserves
kubectl exec deploy/api-reserves -n rutas-norte-pro -- nc -zv postgres-reserves 5432
kubectl exec deploy/botiga-web -n rutas-norte-pro -- nc -zv -w 3 postgres-reserves 5432   # ha de fallar

La trampa: cal obrir les dues direccions. L'egress d'api-reserves no n'hi ha prou si postgres-reserves té l'ingress denegat. I sense la política de DNS, ni tan sols es resol el nom.


Tasca 5 — Escaneig amb Trivy i retirada d'imatges vulnerables (pes ~6 %, objectiu: 6 min)

Context: rutas-norte-pre. Escaneja les imatges dels pods del namespace. Elimina els pods la imatge dels quals tingui vulnerabilitats de severitat CRITICAL. Desa a /opt/vulnerables.txt la llista d'imatges descartades.

kubectl get pods -n rutas-norte-pre \
  -o custom-columns='POD:.metadata.name,IMG:.spec.containers[*].image' --no-headers
botiga-web-1     nginx:1.27-alpine
api-reserves-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-reserves-1 worker-1 -n rutas-norte-pre
cat /opt/vulnerables.txt

La trampa: si els pods pertanyen a un Deployment, esborrar-los no serveix: es recreen amb la mateixa imatge. Cal escalar a 0 o esborrar el Deployment, segons demani l'enunciat. Llegeix-lo amb cura.


Tasca 6 — AppArmor sobre un pod (pes ~7 %, objectiu: 8 min)

Context: rutas-norte-pro. Al node node-1 hi ha un perfil d'AppArmor sense carregar a /opt/perfils/deny-write. Carrega'l en mode enforce i crea el Pod auditor (imatge busybox:1.36, comanda sleep 3600) en aquell node amb aquell perfil aplicat.

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

El nom que apareix a aa-status és el que cal fer servir, no el del fitxer.

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

Comprovació:

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: node-1 és imprescindible; si el pod es planifica en un altre node on el perfil no està carregat, queda en Blocked amb l'esdeveniment cannot enforce AppArmor profile. I si el pod queda en Pending, comprova que node-1 no estigui acordonat.


Tasca 7 — Regla de Falco i captura de l'evidència (pes ~8 %, objectiu: 9 min)

Context: rutas-norte-pro. Falco està instal·lat a node-1. Modifica la sortida de la regla que detecta shells en contenidors perquè mostri exactament hora,usuari,id-contenidor,nom-contenidor,proces, i desa a /opt/shells.log les deteccions dels últims 10 minuts.

ssh node-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ó per verificar (des d'un altre terminal)
kubectl exec -it deploy/api-reserves -n rutas-norte-pro -- sh -c "echo prova"

# Capturar l'evidència
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-reserves,sh

La trampa: cal editar falco_rules.local.yaml, mai falco_rules.yaml. I sense systemctl restart falco el canvi no fa efecte. Si falco --validate dona error de sintaxi, arregla'l abans de reiniciar: un fitxer invàlid deixa el servei caigut.


Tasca 8 — Política d'admissió: només registres permesos (pes ~8 %, objectiu: 9 min)

Context: rutas-norte-pro. Kyverno està instal·lat. Crea una política que rebutgi a rutas-norte-pro qualsevol Pod la imatge del qual no provingui de registry.rutasnorte.es/. Demostra que funciona.

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: nomes-registre-corporatiu
spec:
  validationFailureAction: Enforce
  background: false
  rules:
  - name: comprovar-registre
    match:
      any:
      - resources:
          kinds:
          - Pod
          namespaces:
          - rutas-norte-pro
    validate:
      message: "Es permeten nomes imatges de registry.rutasnorte.es/"
      pattern:
        spec:
          =(initContainers):
          - image: "registry.rutasnorte.es/*"
          containers:
          - image: "registry.rutasnorte.es/*"
kubectl apply -f politica-registre.yaml
kubectl get clusterpolicy nomes-registre-corporatiu

Comprovació:

# Ha de ser rebutjat
kubectl run prova-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/prova-ko for resource violation:
nomes-registre-corporatiu:
  comprovar-registre: 'validation error: Es permeten nomes imatges de
  registry.rutasnorte.es/'
# Ha de ser admès
kubectl run prova-ok --image=registry.rutasnorte.es/nginx:1.27 -n rutas-norte-pro

La trampa: validationFailureAction: Audit només registra; per rebutjar cal Enforce. I el prefix =() a initContainers significa "si existeix aquest camp, valida'l"; sense ell, un pod sense initContainers fallaria la validació.


Errors Comuns i Consells

Errors que costen punts

Error Conseqüència Prevenció
Trencar l'apiserver sense còpia de seguretat Perds totes les tasques d'aquell clúster cp a /root/ abans d'editar
Còpia de seguretat dins de manifests/ Dos apiservers en conflicte Desa-la fora d'aquell directori
Arguments d'auditoria sense volums L'apiserver no arrenca Arguments i volumeMounts i volumes
Regla catch-all al principi de la política d'auditoria Les altres no s'apliquen L'ordre importa: d'específic a general
identity: {} abans del proveïdor de xifratge No es xifra res El xifrador va primer
No rexifrar els Secrets existents La meitat de la tasca sense puntuar kubectl get secrets -A -o json | kubectl replace -f -
Denegar tot i oblidar el DNS El namespace queda inservible Política de DNS sempre al costat de la de denegació
Ruta absoluta a localhostProfile (seccomp) El pod no arrenca Ruta relativa a /var/lib/kubelet/seccomp/
Editar falco_rules.yaml Canvi al fitxer equivocat Fer servir falco_rules.local.yaml
Oblidar systemctl restart després de canviar config El canvi no s'aplica Reiniciar i verificar amb status
Modificar securityContext amb kubectl edit Camps immutables: falla Exportar, esborrar, recrear
Esborrar pods gestionats per un Deployment Es recreen idèntics Escalar a 0 o corregir la plantilla

Consells

  1. Repeteix les tasques d'apiserver fins a automatitzar-les. Auditoria i xifratge són les que més punts valen i les que més temps consumeixen si dubtes.
  2. Tingues un until kubectl get nodes a mà per esperar que l'API torni sense quedar-te mirant.
  3. Comprova quines eines hi ha instal·lades tot just començar una tasca que les requereixi: which trivy kube-bench falco.
  4. La documentació de Falco i Trivy està permesa. Tingues localitzada la secció de camps de Falco (%evt.time, %container.name, ...) i la de flags de Trivy.
  5. Verifica sempre amb una prova negativa. No n'hi ha prou que el que està permès funcioni: cal veure que el que està prohibit falla.
  6. Pensa en defensa, no en atac. L'examen mesura endurir, detectar i respondre. Totes les respostes correctes van en aquesta direcció.
  7. Recorda el requisit: sense CKA vigent, no hi ha CKS.

Exercicis

Exercici 1 — Cicle complet d'enduriment de l'apiserver

En un clúster de pràctica muntat amb kubeadm, i cronometrant:

  1. Executa kube-bench run --targets=master i anota tots els [FAIL] de la secció 1.2.
  2. Corregeix-ne almenys tres editant /etc/kubernetes/manifests/kube-apiserver.yaml.
  3. Verifica que l'API torna i que kube-bench ara marca [PASS] en aquests controls.
  4. Fes còpia de seguretat del manifest abans de començar i demostra que saps restaurar-la.

Objectiu: 20 minuts, amb el clúster funcionant al final.

Exercici 2 — Resposta davant d'un pod sospitós

Simula el procediment complet de contenció a rutas-norte-pro:

  1. Crea un Deployment api-reserves amb 3 rèpliques i un Service que el seleccioni.
  2. Tria un dels pods com a "sospitós".
  3. Aïlla'l del Service sense matar-lo.
  4. Talla-li tota la comunicació de xarxa amb una NetworkPolicy dirigida, i deixa la resta del namespace funcionant.
  5. Preserva logs, describe i esdeveniments a /opt/evidencies/.
  6. Comprova que el Service continua servint amb 2 endpoints i que el pod aïllat no té connectivitat.

Exercici 3 — Cadena de subministrament d'extrem a extrem

Per a la imatge nginx:1.27-alpine:

  1. Escaneja-la amb Trivy filtrant per CRITICAL i HIGH amb pedaç disponible.
  2. Crea una política de Kyverno que exigeixi que tots els pods de rutas-norte-pro tinguin runAsNonRoot: true i allowPrivilegeEscalation: false.
  3. Demostra que un pod que no compleix és rebutjat i que un que compleix és admès.
  4. Activa a més Pod Security Admission a nivell restricted en mode warn sobre rutas-norte-pre i comprova l'avís.

Solucions

Solució a l'Exercici 1

# 0. Còpia de seguretat SEMPRE primer
sudo cp /etc/kubernetes/manifests/kube-apiserver.yaml /root/apiserver.bak

# 1. Diagnòstic
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ó
sudo vim /etc/kubernetes/manifests/kube-apiserver.yaml
    - --profiling=false
    - --audit-log-path=/var/log/kubernetes/audit/audit.log
    - --audit-log-maxage=30

Recordant que --audit-log-path necessita el seu volum:

    volumeMounts:
    - mountPath: /var/log/kubernetes/audit
      name: audit-log
  volumes:
  - name: audit-log
    hostPath:
      path: /var/log/kubernetes/audit
      type: DirectoryOrCreate
# 3. Esperar i 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ó (per practicar la recuperació):

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

L'error habitual en aquest exercici és afegir --audit-log-path sense el volum: l'apiserver arrenca i mor en bucle, i el diagnòstic és sudo crictl logs <id>.

Solució a l'Exercici 2

# 1. Muntatge
kubectl create deployment api-reserves --image=nginx:1.27-alpine \
  --replicas=3 -n rutas-norte-pro
kubectl expose deployment api-reserves --port=80 -n rutas-norte-pro
kubectl get endpoints api-reserves -n rutas-norte-pro
NAME           ENDPOINTS
api-reserves   10.244.1.4:80,10.244.1.5:80,10.244.2.3:80
# 2-3. Triar i aïllar del Service
SOSPITOS=$(kubectl get pods -n rutas-norte-pro -l app=api-reserves \
  -o jsonpath='{.items[0].metadata.name}')
kubectl label pod $SOSPITOS -n rutas-norte-pro app-
kubectl label pod $SOSPITOS -n rutas-norte-pro quarantena=true

Compte: en treure l'etiqueta app, el ReplicaSet el considera perdut i crea un pod nou. Això és el desitjable: el servei es recupera sol mentre el sospitós continua viu per a l'anàlisi.

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

# 5. Evidències
mkdir -p /opt/evidencies
kubectl logs $SOSPITOS -n rutas-norte-pro > /opt/evidencies/logs.txt
kubectl describe pod $SOSPITOS -n rutas-norte-pro > /opt/evidencies/describe.txt
kubectl get events -n rutas-norte-pro --sort-by=.lastTimestamp > /opt/evidencies/esdeveniments.txt

# 6. Verificació
kubectl get endpoints api-reserves -n rutas-norte-pro    # 3 endpoints, cap el sospitós
kubectl exec $SOSPITOS -n rutas-norte-pro -- \
  timeout 3 wget -qO- http://api-reserves || echo "aillat correctament"
aillat correctament

La NetworkPolicy sense regles ingress ni egress és un tall total. El pod continua existint, els seus logs es poden llegir (això passa per l'API, no per la seva xarxa) i el kubectl exec funciona pel mateix motiu.

Solució a l'Exercici 3

# 1. Escaneig
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 és clau: filtra el soroll de vulnerabilitats sense pedaç disponible, que no pots remeiar actualitzant.

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

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

# 3bis. Prova positiva
cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: bo
  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 bo -n rutas-norte-pro
# 4. Pod Security Admission en mode avís
kubectl label namespace rutas-norte-pre \
  pod-security.kubernetes.io/warn=restricted \
  pod-security.kubernetes.io/warn-version=v1.30 --overwrite

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

Fixa't en la diferència: Kyverno amb Enforce rebutja; PSA en mode warn avisa i crea. Saber triar el mecanisme i el mode segons el que demana l'enunciat és exactament el que mesura aquest domini.

Recordatori: aquestes polítiques són exercicis d'estudi. Abans d'aplicar alguna cosa semblant a un entorn real, que ho revisi un professional de seguretat amb coneixement del context de l'organització.


Conclusió

El CKS és la certificació que tanca el triangle: el CKA demostra que saps mantenir el clúster dret, el CKAD que saps construir-hi a sobre, i el CKS que saps defensar-lo. És el més exigent dels tres perquè et treu de kubectl i et porta als fitxers del node, a eines externes i a decisions on equivocar-se deixa el pla de control caigut.

L'essencial d'aquesta lliçó:

  • El CKS exigeix CKA vigent per presentar-s'hi. No hi ha drecera possible.
  • L'examen permet, a més de kubernetes.io, la documentació de Trivy, Falco i AppArmor. Aprofita-ho.
  • Els tres dominis de més pes —microserveis, cadena de subministrament i runtime, ~60 % entre els tres— són els del mòdul 8 d'aquest curs.
  • Nou eines cal manejar-les sense dubtar: kube-bench, Trivy, Falco, AppArmor, seccomp, audit-policy, Pod Security Admission, Kyverno/OPA i gVisor amb RuntimeClass, més les NetworkPolicies de denegació per defecte.
  • Sis procediments cal tenir-los automatitzats: RBAC mínim, desactivar l'automuntatge del token, xifrar etcd, endurir un pod, aïllar un pod sospitós i llegir el registre d'auditoria.
  • Abans de tocar l'apiserver: còpia de seguretat fora de manifests/. Després: verificar que l'API torna.
  • Les trampes que més suspenen: volums oblidats a l'auditoria, identity primer al xifratge, Secrets sense rexifrar, DNS bloquejat per la denegació total i perfils d'AppArmor al node equivocat.
  • Consulta sempre el temari oficial vigent al web de la Linux Foundation / CNCF, inclosa la llista de documentació permesa, que ha canviat entre revisions.
  • I recorda: qualsevol configuració de seguretat destinada a un entorn real l'ha de revisar un professional de seguretat.

Ja coneixes les tres certificacions, els seus temaris i la seva correspondència amb el que has estudiat. El que falta és la part que cap de les tres no ensenya i que decideix més aprovats del que sembla: la tècnica d'examen. A la propera lliçó veurem com es prepara l'entorn de supervisió, què teclejar als primers seixanta segons de l'examen, com fer servir la documentació permesa sense perdre temps, com repartir els minuts entre tasques, quins errors costen més punts i què fer —abans, durant i després— perquè el dia de l'examen no hi hagi sorpreses.

Curs de Kubernetes

Mòdul 1: Introducció a Kubernetes

Mòdul 2: Components Principals de Kubernetes

Mòdul 3: Gestió de Configuració i Secrets

Mòdul 4: Xarxes a Kubernetes

Mòdul 5: Emmagatzematge a Kubernetes

Mòdul 6: Conceptes Avançats de Kubernetes

Mòdul 7: Monitoratge i Registre

Mòdul 8: Seguretat a Kubernetes

Mòdul 9: Escalat i Rendiment

Mòdul 10: Ecosistema i Eines de Kubernetes

Mòdul 11: Estudis de Cas i Aplicacions del Món Real

Mòdul 12: Preparació per a la Certificació de Kubernetes

© Copyright 2026. Tots els drets reservats