A la lliçó anterior vam endurir cada component de Rutas Norte: botiga-web corre sense root amb el sistema de fitxers en només lectura, api-reserves no té ni una capacitat de Linux, postgres-reserves escriu al seu volum gràcies a fsGroup i tots porten seccompProfile: RuntimeDefault. La feina està feta i verificada.

I tot i així, hi ha un forat enorme: tot això depèn que algú se'n recordi. Demà un company afegeix un sidecar i oblida el drop: ["ALL"]. Un desenvolupador copia un manifest d'un blog que porta privileged: true perquè "així funcionava". Un operador de tercers s'instal·la amb un DaemonSet que munta / del node. Cap d'aquestes coses no trenca res visible, ningú no rep una alerta, i l'enduriment que vam construir amb tanta cura deixa d'aplicar-se al punt exacte on importa.

Aquesta lliçó fa el salt que ho canvia tot: passar de "cada equip posa bé el seu securityContext" a "el clúster rebutja un pod que no el tingui". És la tercera porta que vam veure a 08-01, el control d'admissió, i és la diferència entre una norma escrita i una norma aplicada.

Advertiment important. Les polítiques d'admissió són el mecanisme que fa complir el disseny de seguretat, i precisament per això un error en elles pot bloquejar desplegaments legítims o, pitjor, donar una falsa sensació de protecció. El conjunt de polítiques d'un clúster de producció l'ha de dissenyar i revisar un professional de seguretat. Quan les càrregues afectades tracten dades personals —postgres-reserves, api-reserves i informes-ocupacio a Rutas Norte— el responsable de compliment normatiu ha de conèixer i aprovar què s'exigeix i quines excepcions existeixen.

Contingut

  1. De la norma escrita a la norma aplicada
  2. Context històric: les PodSecurityPolicy i per què van desaparèixer
  3. Els Pod Security Standards: privileged, baseline i restricted
  4. Pod Security Admission: activar-lo amb etiquetes al namespace
  5. Aplicació progressiva a Rutas Norte
  6. Càrregues que legítimament necessiten privilegis
  7. Les limitacions del Pod Security Admission
  8. Motors de política general: Kyverno i OPA Gatekeeper
  9. Polítiques de Kyverno per a Rutas Norte
  10. ValidatingAdmissionPolicy: CEL natiu sense instal·lar res
  11. Errors comuns i consells
  12. Exercicis
  13. Conclusió

  1. De la norma escrita a la norma aplicada

Recordem el recorregut d'una petició a l'API de 08-01:

flowchart LR
    A["kubectl apply -f pod.yaml"] --> B["Autenticació<br/>qui ets?"]
    B --> C["Autorització RBAC<br/>pots crear pods?"]
    C --> D["Admissió mutant<br/>modifica l'objecte"]
    D --> E["Admissió validant<br/>l'objecte compleix?<br/>AQUESTA LLIÇÓ"]
    E -->|No compleix| X["Rebutjat<br/>l'objecte MAI arriba a etcd"]
    E -->|Compleix| F[("etcd")]
    style E fill:#d5e8f9,stroke:#36c

La diferència amb RBAC és fonamental i convé tenir-la claríssima:

RBAC (08-01) Control d'admissió (aquesta lliçó)
Pregunta Tens dret a fer aquesta operació? L'objecte que envies és acceptable?
Mira Subjecte, verb, recurs, namespace El contingut de l'objecte
Exemple de decisió "L'Anna pot crear pods a dev" "Aquest pod no pot ser privilegiat"
Si falla 403 Forbidden 400 Bad Request o 422 amb el motiu

Algú de plataforma té permís RBAC per crear pods a rutas-norte-pro. Això no canvia. El que hi afegim és que, encara que tingui permís, el pod concret que envia ha de complir unes regles.

I hi ha una propietat molt valuosa: l'objecte rebutjat mai no arriba a etcd. No es crea i cal esborrar-lo després; senzillament no existeix. La resposta arriba a l'instant a qui va fer el kubectl apply, amb el motiu escrit.

Els dos tipus de controlador d'admissió

Tipus Què fa Exemples
Mutant (mutating) Modifica l'objecte abans de desar-lo Injectar la ServiceAccount per defecte, afegir el sidecar d'una malla de serveis, aplicar els valors d'un LimitRange (03-05)
Validant (validating) Només accepta o rebutja, no modifica Pod Security Admission, ResourceQuota (03-04), polítiques de Kyverno de tipus validate

Els mutants s'executen abans que els validants, cosa que té sentit: primer es completa l'objecte, després es comprova el resultat final.

  1. Context històric: les PodSecurityPolicy i per què van desaparèixer

Si busques informació sobre aquest tema trobaràs moltíssim material sobre PodSecurityPolicy (PSP). És important que sàpigues què eren, perquè apareixen constantment en documentació antiga, en respostes de fòrums i en manifests heretats. Però també que tinguis molt clar això:

Les PodSecurityPolicy van quedar obsoletes a Kubernetes 1.21 i es van eliminar del tot a la 1.25. En un clúster 1.30 no existeixen. Si un tutorial et diu que creïs una PodSecurityPolicy, aquell tutorial té com a mínim cinc anys.

Què eren

Un recurs a nivell de clúster que descrivia què es permetia en un pod: si podia ser privilegiat, quins usuaris podia fer servir, quins volums, quines capacitats. La seva forma s'assemblava a això:

# NO FER SERVIR: recurs eliminat a Kubernetes 1.25. Només com a referència històrica.
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  name: restrictiva
spec:
  privileged: false
  allowPrivilegeEscalation: false
  requiredDropCapabilities: ["ALL"]
  runAsUser:
    rule: MustRunAsNonRoot
  seLinux:
    rule: RunAsAny
  fsGroup:
    rule: MustRunAs
    ranges: [{min: 1, max: 65535}]
  volumes: ["configMap", "emptyDir", "secret", "persistentVolumeClaim"]

Conceptualment la idea era bona. La implementació tenia tres problemes greus que van resultar insalvables.

Problema 1: RBAC contraintuïtiu

Una PSP no s'aplicava a ningú per si sola. S'"activava" concedint el verb use sobre ella mitjançant RBAC, i —això és el rar— al subjecte que creava el pod, que moltes vegades no era la persona sinó un controlador. Quan un Deployment creava pods, el subjecte rellevant era la ServiceAccount del controlador de ReplicaSets, no l'usuari que va fer el kubectl apply.

El resultat: ningú no sabia amb certesa quina política se li estava aplicant a un pod concret, i respondre a "per què s'ha rebutjat això?" era un exercici d'arqueologia.

Problema 2: ordre d'aplicació impredictible

Si a un subjecte se li aplicaven diverses PSP —cosa habitual— Kubernetes en triava una segons un algorisme complicat: primer les que no mutaven el pod, i si totes mutaven, la primera per ordre alfabètic. L'ordre alfabètic. Reanomenar una política canviava quina política s'aplicava.

Problema 3: mutació

Les PSP no només validaven: també modificaven el pod (omplien runAsUser, afegien fsGroup, treien capacitats). Això significava que el pod que s'executava no era el que hi havia al teu manifest, i que aplicar el mateix YAML a dos clústers podia donar resultats diferents sense cap avís.

I un problema de fons: la usabilitat

A la pràctica, activar PSP en un clúster existent trencava gairebé tot, i com que no hi havia un mode "avisa'm però no bloquegis", el camí habitual era crear una PSP permissiva "temporal" que es quedava per sempre. La conclusió de la comunitat va ser clara: un mecanisme de seguretat que ningú no aconsegueix activar no protegeix ningú.

Què se'n va aprendre

El substitut, el Pod Security Admission, es va dissenyar explícitament per corregir cadascun d'aquells defectes:

Problema de PSP Com el resol el PSA
RBAC contraintuïtiu S'activa amb etiquetes al namespace. Sense RBAC pel mig
Ordre impredictible Tres perfils fixos definits pel projecte. No cal triar
Mutació Mai no muta. Només valida
Tot o res Tres modes: warn, audit i enforce. Es pot adoptar gradualment
Complexitat Una etiqueta en un namespace

La contrapartida és que el PSA és molt menys flexible: només tres perfils, sense possibilitat de personalitzar-los. Aquell buit el cobreixen els motors de política de l'apartat 8.

  1. Els Pod Security Standards: privileged, baseline i restricted

Els Pod Security Standards (PSS) són tres perfils definits pel projecte Kubernetes. No són un recurs ni un objecte: són una especificació, un acord sobre què significa "segur" en tres nivells.

Perfil Filosofia Per a què
privileged Sense restriccions Infraestructura del sistema: CNI, CSI, agents de node
baseline Impedeix les escalades de privilegis conegudes Aplicacions normals; adopció realista i compatible
restricted Bones pràctiques d'enduriment actuals L'objectiu per a tota aplicació

Taula detallada de què controla cada perfil

Control privileged baseline restricted
privileged: true Permès Prohibit Prohibit
hostNetwork Permès Prohibit Prohibit
hostPID / hostIPC Permès Prohibit Prohibit
hostPath Permès Prohibit Prohibit
hostPort Permès Prohibit Prohibit
Capacitats afegides Totes Només una llista curta (veure a sota) Només NET_BIND_SERVICE
capabilities.drop: ["ALL"] No exigit No exigit Obligatori
allowPrivilegeEscalation Lliure Lliure Ha de ser false
runAsNonRoot Lliure Lliure Ha de ser true
runAsUser: 0 Permès Permès Prohibit
seccompProfile Lliure No pot ser Unconfined RuntimeDefault o Localhost
Tipus de volum Tots Tots menys els d'host Llista restringida
sysctls no segurs Permesos Prohibits Prohibits
procMount: Unmasked Permès Prohibit Prohibit
SELinux: tipus perillosos Permesos Prohibits Prohibits
AppArmor Unconfined Permès Prohibit Prohibit
/dev/... com a hostPath Permès Prohibit Prohibit

Capacitats permeses a baseline

baseline permet afegir només aquestes, que són les del conjunt per defecte i totes relativament innòcues:

AUDIT_WRITE, CHOWN, DAC_OVERRIDE, FOWNER, FSETID, KILL,
MKNOD, NET_BIND_SERVICE, SETFCAP, SETGID, SETPCAP, SETUID, SYS_CHROOT

El que baseline prohibeix afegir és el veritablement perillós: NET_ADMIN, SYS_ADMIN, SYS_MODULE, SYS_PTRACE, BPF, NET_RAW...

restricted va molt més enllà: exigeix drop: ["ALL"] i només permet tornar a afegir NET_BIND_SERVICE. És exactament l'excepció de què vam parlar a 08-02 i per això dèiem que el port alt és preferible: amb port 8080 no necessites ni tan sols aquesta.

Tipus de volum permesos a restricted

configMap, csi, downwardAPI, emptyDir, ephemeral, persistentVolumeClaim,
projected, secret

Fixa't que hi són tots els que fa servir Rutas Norte —emptyDir per als directoris d'escriptura, persistentVolumeClaim per a les dades de PostgreSQL, configMap i secret per a la configuració— i no hi ha hostPath. És la formalització de l'advertiment de 05-01.

El camp securityContext mínim que compleix restricted

spec:
  securityContext:
    runAsNonRoot: true
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: app
      image: registry.rutasnorte.example/api-reserves:2.7.1
      securityContext:
        allowPrivilegeEscalation: false
        capabilities:
          drop: ["ALL"]

Compara'l amb el bloc de "consell d'or" de 08-02: és pràcticament el mateix. Els Pod Security Standards no són altra cosa que la codificació oficial del que ja vam fer a mà. Tot el de la lliçó anterior encaixa ara en una etiqueta.

Nota important: restricted no exigeix readOnlyRootFilesystem. És una omissió coneguda (era massa disruptiva per a moltes càrregues) i una de les raons per les quals un motor de política complementa el PSA.

  1. Pod Security Admission: activar-lo amb etiquetes al namespace

El Pod Security Admission (PSA) és el controlador que aplica els Pod Security Standards. Va integrat a l'apiserver des de 1.25: no cal instal·lar res, no hi ha cap pod que mantenir, no hi ha webhook que pugui caure.

S'activa posant etiquetes al Namespace:

apiVersion: v1
kind: Namespace
metadata:
  name: rutas-norte-pro
  labels:
    app.kubernetes.io/part-of: rutas-norte
    entorn: pro
    # --- Pod Security Admission ---
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: v1.30
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/audit-version: v1.30
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/warn-version: v1.30

La sintaxi de l'etiqueta és:

pod-security.kubernetes.io/<MODE>[-version]: <VALOR>

Els tres modes

Mode Què passa quan un pod incompleix On es veu
enforce El pod es rebutja. No es crea Error immediat en aplicar
audit Es crea, però s'anota al registre d'auditoria de l'apiserver Log d'auditoria (08-06)
warn Es crea, i el client rep un avís Sortida de kubectl

Els tres són independents i es poden combinar. Aquí hi ha la clau de l'adopció gradual: pots posar enforce: baseline (bloqueja el pitjor) juntament amb warn: restricted i audit: restricted (t'avisa del que encara no compleix, sense bloquejar).

Un detall essencial sobre enforce:

enforce només actua sobre pods que es creen o s'actualitzen. No afecta els que ja estan corrent. Si actives enforce: restricted en un namespace amb pods que no compleixen, aquests pods continuen funcionant tan tranquils... fins que el Deployment en creï un de nou, que fallarà. És una fallada diferida i desconcertant si no te l'esperes.

Per això warn i audit no bloquegen però sí que avaluen objectes que creen pods (Deployments, StatefulSets, CronJobs), avisant-te en el moment de l'apply. enforce, en canvi, només avalua el pod, no el Deployment. Hi tornarem a l'apartat 7.

La fixació de versió

pod-security.kubernetes.io/enforce-version: v1.30

Els Pod Security Standards evolucionen: en cada versió de Kubernetes s'hi poden afegir controls nous. Si no fixes la versió, el valor per defecte és latest, cosa que significa que una actualització del clúster pot començar a rebutjar pods que abans s'acceptaven, sense que ningú hagi tocat res.

Valor Comportament
latest (per defecte) Fa servir sempre els controls de la versió actual del clúster
v1.30 Congela els controls d'aquella versió

Fixa sempre la versió. I quan actualitzis el clúster, apuja l'etiqueta com un canvi explícit i revisat, amb warn primer. És la diferència entre una actualització planificada i un incident el dilluns al matí.

Configurar valors per defecte per a tot el clúster

Es pot indicar a l'apiserver un perfil per defecte per als namespaces que no portin etiquetes, mitjançant AdmissionConfiguration:

# /etc/kubernetes/admissio/pod-security.yaml (als nodes del pla de control)
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
  - name: PodSecurity
    configuration:
      apiVersion: pod-security.admission.config.k8s.io/v1
      kind: PodSecurityConfiguration
      defaults:
        enforce: "baseline"          # línia base per a namespaces sense etiquetar
        enforce-version: "v1.30"
        audit: "restricted"
        audit-version: "v1.30"
        warn: "restricted"
        warn-version: "v1.30"
      exemptions:
        usernames: []
        runtimeClasses: []
        namespaces:
          - kube-system              # el sistema necessita privilegis

Això és molt potent perquè cobreix els namespaces futurs: si demà algú crea rutas-norte-experiments sense etiquetes, arrenca ja en baseline. Sense aquesta configuració, un namespace sense etiquetar equival a privileged, és a dir, sense cap restricció.

Requereix accés al pla de control, així que a Kubernetes gestionat (10-06) pot no estar disponible; en aquest cas, l'alternativa és una política de Kyverno que exigeixi les etiquetes a tot namespace nou (ho veurem a l'apartat 9).

El mode warn en acció

kubectl label namespace rutas-norte-pre \
  pod-security.kubernetes.io/warn=restricted \
  pod-security.kubernetes.io/warn-version=v1.30

kubectl apply -f k8s/entorns/pre/botiga-web.yaml
Warning: would violate PodSecurity "restricted:v1.30": allowPrivilegeEscalation != false
(container "nginx" must set securityContext.allowPrivilegeEscalation=false),
unrestricted capabilities (container "nginx" must set securityContext.capabilities.drop=["ALL"]),
runAsNonRoot != true (pod or container "nginx" must set securityContext.runAsNonRoot=true),
seccompProfile (pod or container "nginx" must set securityContext.seccompProfile.type
to "RuntimeDefault" or "Localhost")
deployment.apps/botiga-web configured

Llegeix bé aquella sortida: el Deployment s'ha aplicat (configured), però l'avís enumera exactament els quatre camps que falten, contenidor per contenidor. És la millor documentació possible de què cal arreglar. Anota com de ben redactat està el missatge: cada línia et diu el camp, el contenidor i el valor esperat.

  1. Aplicació progressiva a Rutas Norte

No activis mai enforce: restricted de cop en un namespace de producció. El camí correcte té quatre fases i pot durar setmanes. El recorrerem.

flowchart TD
    A["Fase 0<br/>Sense etiquetes<br/>= privileged"] --> B["Fase 1<br/>warn + audit: restricted<br/>NO bloqueja"]
    B --> C["Analitzar avisos<br/>i arreglar components"]
    C --> D["Fase 2<br/>enforce: baseline<br/>+ warn/audit: restricted"]
    D --> E["Arreglar el que falta<br/>per a restricted"]
    E --> F["Fase 3<br/>enforce: restricted<br/>en els tres modes"]
    style F fill:#d5f9d5,stroke:#3a3

Fase 1: veure què es trencaria, sense trencar res

kubectl label namespace rutas-norte-pro \
  pod-security.kubernetes.io/warn=restricted \
  pod-security.kubernetes.io/warn-version=v1.30 \
  pod-security.kubernetes.io/audit=restricted \
  pod-security.kubernetes.io/audit-version=v1.30

Aquí no es bloqueja res: el clúster continua funcionant igual. Ara cal provocar l'avaluació de totes les càrregues existents. Un truc molt útil és reaplicar els manifests sense canvis:

kubectl apply -f k8s/entorns/pro/ --dry-run=server 2>&1 | grep -i warning

--dry-run=server envia l'objecte a l'apiserver, que el passa per tota la cadena d'admissió i retorna el resultat sense desar-lo. És la forma segura d'avaluar la situació completa.

Warning: would violate PodSecurity "restricted:v1.30": allowPrivilegeEscalation != false
  (container "postgres" ...), runAsNonRoot != true (pod or container "postgres" ...)
Warning: would violate PodSecurity "restricted:v1.30": host namespaces (hostPath volume "varlog"),
  restricted volume types (volume "varlog" uses restricted volume type "hostPath")

I per veure el que avalua el mode audit, la font és el registre d'auditoria de l'apiserver, amb l'anotació pod-security.kubernetes.io/audit-violations. Ho veurem en detall a 08-06.

Una eina que estalvia molt de temps en aquesta fase:

# Avalua tot el clúster contra un perfil sense aplicar res
kubectl krew install score   # o fes servir pod-security-admission-checker

També es pot consultar directament quins namespaces estan sense protecció:

kubectl get namespaces -o custom-columns=\
'NOM:.metadata.name,ENFORCE:.metadata.labels.pod-security\.kubernetes\.io/enforce,WARN:.metadata.labels.pod-security\.kubernetes\.io/warn'
NOM                 ENFORCE       WARN
default             <none>        <none>
kube-system         privileged    <none>
rutas-norte-dev     <none>        <none>
rutas-norte-pre     <none>        restricted
rutas-norte-pro     <none>        restricted

Els <none> a enforce són namespaces on ara mateix s'hi pot desplegar qualsevol cosa, inclòs default. Aquella taula és una diapositiva excel·lent per a una reunió de seguretat.

Fase 2: enforce: baseline

baseline bloqueja el veritablement perillós —privilegiats, namespaces d'host, hostPath— i gairebé cap aplicació normal no l'incompleix. És un pas amb molt poc risc i molt valor.

# k8s/entorns/pro/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: rutas-norte-pro
  labels:
    app.kubernetes.io/part-of: rutas-norte
    entorn: pro
    pod-security.kubernetes.io/enforce: baseline     # bloqueja el pitjor
    pod-security.kubernetes.io/enforce-version: v1.30
    pod-security.kubernetes.io/audit: restricted     # continua avisant de l'objectiu
    pod-security.kubernetes.io/audit-version: v1.30
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/warn-version: v1.30

Ara, si algú intenta desplegar un pod privilegiat:

kubectl run prova --image=nginx --privileged -n rutas-norte-pro
Error from server (Forbidden): pods "prova" is forbidden: violates PodSecurity
"baseline:v1.30": privileged (container "prova" must not set securityContext.privileged=true)

Aquest és el moment en què la feina de 08-02 deixa de dependre de la memòria de ningú.

Fase 3: enforce: restricted i el missatge de rebuig exacte

Abans de fer aquest pas, tots els components han de complir. Provem primer amb un manifest sense endurir per veure el missatge complet:

# /tmp/pod-sense-endurir.yaml
apiVersion: v1
kind: Pod
metadata:
  name: prova-restricted
  namespace: rutas-norte-pro
spec:
  containers:
    - name: app
      image: registry.rutasnorte.example/utilitats:1.4.2
kubectl label namespace rutas-norte-pro \
  pod-security.kubernetes.io/enforce=restricted \
  pod-security.kubernetes.io/enforce-version=v1.30 --overwrite

kubectl apply -f /tmp/pod-sense-endurir.yaml
Error from server (Forbidden): error when creating "/tmp/pod-sense-endurir.yaml":
pods "prova-restricted" is forbidden: violates PodSecurity "restricted:v1.30":
allowPrivilegeEscalation != false (container "app" must set
securityContext.allowPrivilegeEscalation=false),
unrestricted capabilities (container "app" must set securityContext.capabilities.drop=["ALL"]),
runAsNonRoot != true (pod or container "app" must set securityContext.runAsNonRoot=true),
seccompProfile (pod or container "app" must set securityContext.seccompProfile.type
to "RuntimeDefault" or "Localhost")

El missatge és una llista de tasques. Corregim:

# /tmp/pod-endurit.yaml
apiVersion: v1
kind: Pod
metadata:
  name: prova-restricted
  namespace: rutas-norte-pro
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 10001
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: app
      image: registry.rutasnorte.example/utilitats:1.4.2
      securityContext:
        allowPrivilegeEscalation: false
        capabilities:
          drop: ["ALL"]
kubectl apply -f /tmp/pod-endurit.yaml
pod/prova-restricted created

El component que no complia: postgres-reserves

A 08-02 vam deixar readOnlyRootFilesystem: false a PostgreSQL com a excepció documentada. Bona notícia: restricted no exigeix aquell camp, així que aquell punt no bloqueja. El que sí que exigeix és runAsNonRoot: true i drop: ["ALL"], que ja teníem. postgres-reserves passa restricted sense canvis.

Comprovem-ho abans de tocar el namespace:

kubectl apply -f k8s/entorns/pro/postgres-reserves.yaml --dry-run=server
statefulset.apps/postgres-reserves configured

Sense avisos. Perfecte.

El component que no compleix: el recol·lector de logs

kubectl apply -f k8s/entorns/pro/logs-daemonset.yaml --dry-run=server
Warning: would violate PodSecurity "restricted:v1.30": hostPath volumes (volume "varlog"),
runAsNonRoot != true (pod or container "recollector" must set securityContext.runAsNonRoot=true),
restricted volume types (volume "varlog" uses restricted volume type "hostPath")

I aquí no hi ha res per arreglar: el recol·lector necessita legítimament llegir /var/log del node i necessita ser root per fer-ho. És el tema de l'apartat següent.

Estat final dels tres entorns

Namespace enforce audit / warn Raó
rutas-norte-dev baseline restricted Els desenvolupadors necessiten marge per depurar
rutas-norte-pre restricted restricted Ha de ser idèntic a producció
rutas-norte-pro restricted restricted Objectiu
rutas-norte-sistema privileged Agents de node (apartat 6)

Que rutas-norte-pre sigui igual d'estricte que producció és innegociable: si a preproducció es permet el que a producció no, la promoció falla just en el pitjor moment. Aquest és tot el sentit de tenir un entorn intermedi.

rutas-norte-dev en baseline és una concessió deliberada: bloqueja el perillós però permet, per exemple, córrer com a root per provar alguna cosa. Amb warn: restricted, el desenvolupador veu a cada apply exactament què li faltarà per promocionar. És una decisió d'equilibri que cal documentar.

  1. Càrregues que legítimament necessiten privilegis

Hi ha programari que no pot complir restricted, i no per deixadesa. El plugin CNI configura les interfícies de xarxa del node. El driver CSI munta volums al sistema de fitxers de l'amfitrió. El recol·lector de logs llegeix /var/log. L'agent de detecció en temps d'execució (Falco, que veurem a 08-06) observa crides al sistema.

La resposta correcta no és relaxar el perfil de rutas-norte-pro. És aïllar aquelles càrregues.

Solució 1: namespace a part amb perfil privileged

apiVersion: v1
kind: Namespace
metadata:
  name: rutas-norte-sistema
  labels:
    app.kubernetes.io/part-of: rutas-norte
    pod-security.kubernetes.io/enforce: privileged
    pod-security.kubernetes.io/enforce-version: v1.30
    # audit i warn en restricted: ens deixa rastre de tot el que se salta el perfil
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/audit-version: v1.30
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/warn-version: v1.30
  annotations:
    seguretat.rutasnorte.example/justificacio: >-
      Namespace per a agents de node que requereixen accés a l'amfitrió:
      recol·lector de logs (lectura de /var/log) i agent de detecció en
      temps d'execució. Revisió trimestral per l'equip de plataforma
      i per seguretat. Cap aplicació de negoci no s'hi pot desplegar.

Tres decisions importants en aquell manifest:

  1. audit i warn continuen en restricted. Encara que enforce sigui privileged, cada pod que se salti el perfil deixa rastre al registre d'auditoria. Així el namespace privilegiat no és un forat negre: sabem exactament què s'hi executa i amb quins privilegis.
  2. L'anotació de justificació. Un namespace amb perfil privileged és una excepció de seguretat i ha d'estar documentada al mateix objecte, amb qui la revisa i quan.
  3. RBAC estricte al damunt. Recordant 08-01: qui pot crear pods aquí pot crear pods privilegiats. Només plataforma ha de tenir permís, i a 08-01 vam veure que crear pods en un namespace equival a poder fer servir qualsevol ServiceAccount d'aquell namespace: una altra raó perquè les aplicacions no hi visquin.

I el complement imprescindible a RBAC:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: plataforma-sistema
  namespace: rutas-norte-sistema
subjects:
  - kind: Group
    name: plataforma         # NOMÉS plataforma. Ni desenvolupament, ni CI, ni suport
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: admin
  apiGroup: rbac.authorization.k8s.io

Solució 2: exempcions a la configuració de l'apiserver

El PSA admet exempcions globals per tres criteris:

apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
  - name: PodSecurity
    configuration:
      apiVersion: pod-security.admission.config.k8s.io/v1
      kind: PodSecurityConfiguration
      defaults:
        enforce: "baseline"
        enforce-version: "v1.30"
      exemptions:
        # Per usuari: el subjecte que crea el pod queda exempt
        usernames:
          - "system:serviceaccount:kube-system:daemon-set-controller"
        # Per classe d'execució: els pods amb aquest RuntimeClass queden exempts
        runtimeClasses:
          - "gvisor"
        # Per namespace: res del namespace no s'avalua
        namespaces:
          - "kube-system"
          - "rutas-norte-sistema"
Criteri Quan fer-lo servir Risc
namespaces Namespaces d'infraestructura complets Mitjà: cal vigilar qui hi pot desplegar
usernames Un controlador concret que crea pods de sistema Alt: aquella identitat queda exempta a tot el clúster
runtimeClasses Pods que ja corren aïllats (gVisor, Kata) Baix: l'aïllament l'aporta l'entorn d'execució

Un avís sobre usernames: eximeix el subjecte a tots els namespaces, no només on t'interessa. És un permís molt ampli. L'exempció per namespace és més fàcil de raonar i d'auditar.

I l'advertiment general: cada exempció és una excepció de seguretat. Ha de tenir justificació escrita, responsable, i una revisió periòdica que confirmi que continua sent necessària. A Rutas Norte, la llista d'exempcions es revisa cada trimestre juntament amb el RBAC.

  1. Les limitacions del Pod Security Admission

El PSA és excel·lent en el seu terreny, i el seu terreny és bastant estret. Conèixer-ne els límits evita confiar-hi per a coses que no fa.

Limitació 1: només mira el pod

El PSA avalua objectes Pod. Punt. No mira Services, ni Ingressos, ni PVCs, ni ConfigMaps, ni NetworkPolicies.

Això té un efecte pràctic incòmode amb enforce:

# Un Deployment amb un pod que NO compleix restricted
kubectl apply -f deployment-dolent.yaml
deployment.apps/aplicacio-dolenta created
kubectl get pods -n rutas-norte-pro -l app=aplicacio-dolenta
No resources found in rutas-norte-pro namespace.
kubectl describe replicaset -n rutas-norte-pro -l app=aplicacio-dolenta | tail -5
Events:
  Type     Reason        Age   From                   Message
  ----     ------        ----  ----                   -------
  Warning  FailedCreate  12s   replicaset-controller  Error creating: pods
  "aplicacio-dolenta-6c5f9b8d4-" is forbidden: violates PodSecurity "restricted:v1.30":
  allowPrivilegeEscalation != false (container "app" must set
  securityContext.allowPrivilegeEscalation=false), ...

El Deployment es crea correctament, el ReplicaSet es crea, i és el ReplicaSet el que no aconsegueix crear pods. Qui va fer l'apply veu un èxit i ha d'anar a mirar els esdeveniments per descobrir el problema.

Per això warn és tan valuós: sí que avalua els objectes que creen pods i avisa en el moment de l'apply. Mantén sempre warn actiu encara que tinguis enforce, precisament per aquest motiu.

Limitació 2: no pot exigir res que no sigui del securityContext

Coses que el PSA no pot exigir:

No pot exigir Per què importa a Rutas Norte
Etiquetes (app.kubernetes.io/part-of) Sense elles es trenquen els selectors i els quadres de comandament del mòdul 7
resources.requests i limits Sense ells, la QoS és BestEffort (03-05) i el pod és el primer a ser desallotjat
Un registre d'imatges concret Qualsevol pot desplegar des de Docker Hub sense revisió
Que la imatge no faci servir latest Desplegaments irreproduïbles (08-05)
Sondes de salut (liveness, readiness) Sense elles no funciona el del mòdul 7
readOnlyRootFilesystem Ni tan sols restricted ho exigeix
Que existeixi una NetworkPolicy La microsegmentació de 08-04
automountServiceAccountToken: false El que vam treballar a 03-06

Aquella llista és exactament el buit que cobreixen Kyverno i Gatekeeper.

Limitació 3: els tres perfils no són configurables

No pots crear un perfil "restricted però permetent hostPath en només lectura" ni "restricted més readOnlyRootFilesystem". Els perfils els defineix el projecte Kubernetes i són immutables.

Limitació 4: no muta

És una virtut de disseny (evita el problema 3 de les PSP), però significa que el PSA mai no t'ajudarà omplint camps. Si vols que a tot pod se li afegeixi automàticament seccompProfile: RuntimeDefault, necessites un motor de política mutant.

Resum

Necessitat Eina
Impedir pods privilegiats, hostPath, root PSA (integrat, sense instal·lar res)
Exigir etiquetes, límits, registre d'imatges Kyverno o Gatekeeper
Omplir camps automàticament Kyverno (mutació)
Verificar signatures d'imatges Kyverno o el policy-controller de Sigstore (08-05)
Regles senzilles sense instal·lar res ValidatingAdmissionPolicy (apartat 10)

La recomanació pràctica: PSA sempre, com a línia base que no pot fallar (és a l'apiserver, no depèn de cap pod), i al damunt un motor de política per a tota la resta.

  1. Motors de política general: Kyverno i OPA Gatekeeper

Un motor de política és un webhook d'admissió: l'apiserver li envia cada objecte i ell respon si l'accepta, el rebutja o el modifica.

flowchart LR
    A["kubectl apply"] --> B["apiserver"]
    B --> C["PSA<br/>(integrat)"]
    C --> D["Webhook mutant<br/>Kyverno"]
    D --> E["Webhook validant<br/>Kyverno / Gatekeeper"]
    E -->|Rebutja| X["Error amb el motiu"]
    E -->|Accepta| F[("etcd")]

Això comporta una consideració operativa molt seriosa: el motor de política es converteix en una dependència de l'apiserver. Si el webhook està configurat amb failurePolicy: Fail i els pods del motor han caigut, ningú no pot crear res al clúster. S'han tombat clústers sencers així.

failurePolicy Si el webhook no respon Quan fer-lo servir
Ignore L'objecte s'accepta Adopció inicial, polítiques no crítiques
Fail L'objecte es rebutja Polítiques de seguretat, amb alta disponibilitat del motor

Les mitigacions habituals: diverses rèpliques del motor amb antiafinitat (06-05), un PodDisruptionBudget (09-05), i excloure kube-system dels webhooks per poder recuperar el clúster.

Comparació: Kyverno i OPA Gatekeeper

Kyverno OPA Gatekeeper
Llenguatge de polítiques YAML (com Kubernetes) Rego (llenguatge propi d'OPA)
Corba d'aprenentatge Baixa: si saps YAML, saps escriure polítiques Alta: Rego és un llenguatge declaratiu diferent
Validar
Mutar , molt ben resolt Sí, més limitat
Generar objectes (crear una NetworkPolicy amb cada namespace) No
Verificar signatures d'imatges Sí, integrat amb Cosign Requereix feina addicional
Neteja de recursos Sí (CleanupPolicy) No
Informes de compliment PolicyReport com a CRD Objectes de violació
Abast Només Kubernetes Genèric: també CI, Terraform, APIs
Polítiques prefetes Catàleg ampli i mantingut Biblioteca de plantilles
Comunitat i maduresa CNCF, creixement ràpid CNCF, més veterà
Rendiment amb moltes polítiques Bo Molt bo

Recomanació per a Rutas Norte: Kyverno. Les raons són concretes:

  • L'equip ja escriu YAML tot el dia. Aprendre Rego seria una barrera real perquè les polítiques les escriguin també els desenvolupadors.
  • La verificació de signatures amb Cosign està integrada, i és just el que necessitarem a 08-05.
  • La capacitat de generar objectes permet, per exemple, crear automàticament la NetworkPolicy deny-all de 04-06 en cada namespace nou.
  • La mutació resol el "afegeix seccompProfile a tot" sense tocar cent manifests.

Gatekeeper és la millor opció si l'organització ja fa servir OPA per a altres coses (control d'accés a APIs, polítiques a Terraform) i vol un únic llenguatge de polítiques per a tot.

Instal·lació de Kyverno

helm repo add kyverno https://kyverno.github.io/kyverno/
helm repo update

helm install kyverno kyverno/kyverno \
  --namespace kyverno --create-namespace \
  --set admissionController.replicas=3 \
  --set admissionController.podDisruptionBudget.minAvailable=2

Tres rèpliques i un PDB: el motor de política és infraestructura crítica i cal tractar-la com a tal.

  1. Polítiques de Kyverno per a Rutas Norte

Una política de Kyverno és un ClusterPolicy (tot el clúster) o Policy (un namespace) amb una llista de regles.

Política 1: totes les imatges des del registre de l'empresa

Aquesta és probablement la política de més valor per línia escrita. Impedeix que es desplegui res que no hagi passat pel registre de l'empresa, on a 08-05 i 08-06 posarem l'escaneig i la signatura.

# k8s/politiques/registre-obligatori.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: registre-obligatori
  annotations:
    policies.kyverno.io/title: Registre d'imatges obligatori
    policies.kyverno.io/category: Seguretat de la cadena de subministrament
    policies.kyverno.io/severity: high
    policies.kyverno.io/description: >-
      Tota imatge desplegada als namespaces de Rutas Norte ha de venir de
      registry.rutasnorte.example. Les imatges públiques es repliquen allà
      prèviament, on s'escanegen i es signen. Veure lliçons 08-05 i 08-06.
spec:
  # Fail: si Kyverno no pot avaluar, es rebutja. És una política de seguretat.
  validationFailureAction: Enforce
  background: true          # també avalua objectes existents, per als informes
  rules:
    - name: comprovar-registre
      match:
        any:
          - resources:
              kinds:
                - Pod
              namespaces:
                - rutas-norte-dev
                - rutas-norte-pre
                - rutas-norte-pro
      validate:
        message: >-
          La imatge "{{ request.object.spec.containers[0].image }}" no prové de
          registry.rutasnorte.example. Totes les imatges s'han de replicar al
          registre de l'empresa, on s'escanegen i es signen.
          Consulta la guia d'imatges de la plataforma.
        pattern:
          spec:
            # El signe = indica "si el camp existeix, ha de complir"
            =(ephemeralContainers):
              - image: "registry.rutasnorte.example/*"
            =(initContainers):
              - image: "registry.rutasnorte.example/*"
            containers:
              - image: "registry.rutasnorte.example/*"

Desgranem el manifest:

  • validationFailureAction: Enforce rebutja. L'alternativa és Audit, que només genera un informe. Igual que amb el PSA, es comença en Audit i es passa a Enforce quan ja no hi ha incompliments.
  • background: true fa que Kyverno avaluï també els objectes que ja existeixen, produint PolicyReport. Sense això només veuries els objectes nous.
  • match.any.resources defineix l'àmbit. Aquí, pods dels tres namespaces de Rutas Norte. kyverno, kube-system i rutas-norte-sistema queden fora perquè fan servir imatges d'infraestructura externes.
  • =(initContainers) i =(ephemeralContainers): el prefix =() significa "si aquest camp existeix, ha de complir el patró". Sense ell, un pod sense initContainers fallaria la validació. No oblidis els contenidors efímers: són la via de kubectl debug (07-06) i sense aquesta línia algú podria injectar una imatge arbitrària en un pod de producció.
  • El missatge apareix literalment al terminal de qui desplega. Escriure'l bé —què falla, per què, i què fer— estalvia moltíssimes preguntes.

Provant:

kubectl run prova --image=nginx:latest -n rutas-norte-pro
Error from server: admission webhook "validate.kyverno.svc-fail" denied the request:

resource Pod/rutas-norte-pro/prova was blocked due to the following policies

registre-obligatori:
  comprovar-registre: 'validation error: La imatge "nginx:latest" no prové de
    registry.rutasnorte.example. Totes les imatges s'han de replicar al registre
    de l'empresa, on s'escanegen i es signen. Consulta la guia d'imatges de
    la plataforma. rule comprovar-registre failed at path /spec/containers/0/image/'

Política 2: exigir les etiquetes de l'esquema oficial

A 02-07 vam definir l'esquema d'etiquetes de Rutas Norte, i al mòdul 7 els quadres de comandament i les alertes en depenen. Una càrrega sense app.kubernetes.io/part-of: rutas-norte és invisible per al monitoratge.

# k8s/politiques/etiquetes-obligatories.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: etiquetes-obligatories
  annotations:
    policies.kyverno.io/title: Esquema d'etiquetes de Rutas Norte
    policies.kyverno.io/category: Govern
    policies.kyverno.io/severity: medium
spec:
  validationFailureAction: Enforce
  background: true
  rules:
    - name: etiquetes-en-carregues
      match:
        any:
          - resources:
              kinds:
                - Deployment
                - StatefulSet
                - DaemonSet
                - CronJob
                - Job
              namespaces:
                - rutas-norte-dev
                - rutas-norte-pre
                - rutas-norte-pro
      validate:
        message: >-
          Falten etiquetes obligatòries. Tota càrrega de Rutas Norte ha de portar,
          a metadata i a la plantilla de pod: "app" (nom del component),
          "app.kubernetes.io/part-of: rutas-norte" i "entorn" (dev|pre|pro).
          Sense elles, la càrrega no apareix als quadres de comandament ni a les alertes.
        pattern:
          metadata:
            labels:
              app: "?*"                                  # ?* = no buit
              app.kubernetes.io/part-of: "rutas-norte"   # valor exacte
              entorn: "dev | pre | pro"                  # una de les tres
          spec:
            template:
              metadata:
                labels:
                  app: "?*"
                  app.kubernetes.io/part-of: "rutas-norte"
                  entorn: "dev | pre | pro"

    - name: coherencia-entorn-namespace
      match:
        any:
          - resources:
              kinds: [Deployment, StatefulSet, DaemonSet, CronJob]
              namespaces: [rutas-norte-pro]
      validate:
        message: >-
          A rutas-norte-pro l'etiqueta "entorn" ha de valer "pro".
          Una etiqueta incorrecta fa que les alertes de producció
          s'encaminin al canal equivocat (veure 07-04).
        pattern:
          metadata:
            labels:
              entorn: "pro"

Sintaxi de patrons de Kyverno que apareix aquí:

Sintaxi Significat
"?*" Qualsevol valor no buit
"rutas-norte" Aquell valor exacte
"dev | pre | pro" Un d'aquells tres (el | és un "o")
"registry.rutasnorte.example/*" Comodí al final
=(camp) Si el camp existeix, ha de complir
X(camp) El camp no ha d'existir

La segona regla és un detall que sembla menor i no ho és: una etiqueta entorn: dev en un manifest copiat a producció fa que Alertmanager encamini les alertes al canal de desenvolupament i ningú no s'assabenti d'una caiguda real. La política ho impedeix.

Política 3: exigir límits de recursos

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: recursos-obligatoris
spec:
  validationFailureAction: Enforce
  background: true
  rules:
    - name: requests-i-limits
      match:
        any:
          - resources:
              kinds: [Pod]
              namespaces: [rutas-norte-pre, rutas-norte-pro]
      validate:
        message: >-
          Tot contenidor ha de declarar requests i limits de CPU i memòria.
          Sense ells la classe de QoS és BestEffort (veure 03-05) i el pod és el
          primer a ser desallotjat quan el node va just de recursos.
        pattern:
          spec:
            containers:
              - resources:
                  requests:
                    cpu: "?*"
                    memory: "?*"
                  limits:
                    memory: "?*"

Fixa't que exigim limits.memory però no limits.cpu. És deliberat i és una decisió discutida: limitar la CPU provoca estrangulament (throttling) que pot degradar la latència d'api-reserves sense que es vegi clar per què, mentre que no limitar la memòria pot tombar el node. És un bon exemple de política que codifica una decisió tècnica de l'equip; ho tractarem a fons a 09-06.

Política 4: mutar per afegir la línia base de seguretat

Aquí brilla Kyverno davant del PSA:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: afegir-linia-base-seguretat
spec:
  rules:
    - name: seccomp-per-defecte
      match:
        any:
          - resources:
              kinds: [Pod]
              namespaces: [rutas-norte-dev, rutas-norte-pre, rutas-norte-pro]
      mutate:
        patchStrategicMerge:
          spec:
            # +() significa "afegeix aquest valor NOMÉS si el camp no existeix"
            +(securityContext):
              seccompProfile:
                type: RuntimeDefault

Amb +(), si el manifest ja defineix securityContext, Kyverno no el toca; si no el defineix, l'afegeix. És una xarxa de seguretat, no una substitució de la feina de 08-02: el correcte continua sent declarar-ho explícitament al manifest, perquè qui el llegeixi sàpiga què s'està executant.

Un advertiment sobre mutar: és exactament el problema 3 de les PodSecurityPolicy. Fes-ho servir amb moderació i només per a valors per defecte segurs, mai per "arreglar" manifests incorrectes, perquè aleshores el YAML del repositori deixa de descriure el que corre.

Política 5: generar la NetworkPolicy deny-all en cada namespace nou

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: generar-deny-all
spec:
  rules:
    - name: deny-all-en-namespaces-rutas-norte
      match:
        any:
          - resources:
              kinds: [Namespace]
              selector:
                matchLabels:
                  app.kubernetes.io/part-of: rutas-norte
      generate:
        apiVersion: networking.k8s.io/v1
        kind: NetworkPolicy
        name: deny-all
        namespace: "{{request.object.metadata.name}}"
        synchronize: true      # si algú la esborra, Kyverno la recrea
        data:
          spec:
            podSelector: {}
            policyTypes: [Ingress, Egress]

Això garanteix que cap namespace de Rutas Norte no pugui existir sense la denegació per defecte que vam construir a 04-06. I synchronize: true fa que, si algú l'esborra, torni sola. És una capacitat que Gatekeeper no té i que resol un problema real.

Consultar els informes

kubectl get policyreport -n rutas-norte-pro
NAME                                   PASS   FAIL   WARN   ERROR   SKIP   AGE
polr-ns-rutas-norte-pro                 47     2      0      0       0      6d
kubectl get policyreport -n rutas-norte-pro -o json | jq -r '
  .items[].results[] | select(.result == "fail")
  | "\(.policy)/\(.rule): \(.resources[0].kind)/\(.resources[0].name)\n    \(.message)"'
etiquetes-obligatories/etiquetes-en-carregues: Deployment/exportador-heretat
    validation error: Falten etiquetes obligatòries...
recursos-obligatoris/requests-i-limits: Pod/depuracio-temporal-x8k2p
    validation error: Tot contenidor ha de declarar requests i limits...

Aquells informes són la llista de deures pendents i, amb background: true, es mantenen al dia sols. A 08-06 els convertirem en evidència per a auditories de compliment.

  1. ValidatingAdmissionPolicy: CEL natiu sense instal·lar res

Des de Kubernetes 1.30, la ValidatingAdmissionPolicy permet escriure regles d'admissió dins de l'apiserver, sense webhook, fent servir expressions CEL (Common Expression Language).

Avantatges davant d'un motor extern:

ValidatingAdmissionPolicy Webhook (Kyverno/Gatekeeper)
Instal·lació Cap Helm, pods, certificats
Disponibilitat La de l'apiserver Pot caure i bloquejar el clúster
Latència Sense salt de xarxa Una crida HTTP per objecte
Mutació No (hi ha MutatingAdmissionPolicy en alfa)
Generar objectes No Kyverno sí
Verificar signatures No Kyverno sí
Expressivitat CEL: bona per a regles de camps Molt alta

Exemple: prohibir l'etiqueta latest

Es compon de dos objectes: la política (què es comprova) i el binding (on s'aplica).

# k8s/politiques/vap-prohibir-latest.yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: prohibir-etiqueta-latest
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE"]
        resources: ["pods"]
  validations:
    - expression: >-
        object.spec.containers.all(c,
          !c.image.endsWith(":latest") && c.image.contains(":"))
      message: >-
        Les imatges no poden fer servir l'etiqueta "latest" i han de portar una
        etiqueta explícita. Fes servir una etiqueta immutable o, millor, un digest
        (imatge@sha256:...). Veure la lliçó 08-05.
      reason: Invalid
    - expression: >-
        !has(object.spec.initContainers) ||
        object.spec.initContainers.all(c,
          !c.image.endsWith(":latest") && c.image.contains(":"))
      message: 'La mateixa regla s''aplica als initContainers.'
      reason: Invalid
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: prohibir-etiqueta-latest
spec:
  policyName: prohibir-etiqueta-latest
  validationActions: [Deny]       # Deny | Warn | Audit, combinables
  matchResources:
    namespaceSelector:
      matchLabels:
        app.kubernetes.io/part-of: rutas-norte

Punts a observar:

  • validationActions compleix el mateix paper que els modes del PSA: [Warn, Audit] per provar, [Deny] quan n'estàs segur. Es poden combinar: [Deny, Audit].
  • namespaceSelector aplica la política només als namespaces etiquetats com a part de Rutas Norte. Els de sistema queden fora.
  • CEL té funcions molt llegibles: all(), exists(), has(), endsWith(), contains(), matches() per a expressions regulars.
  • Separar política i binding permet escriure la regla una vegada i aplicar-la amb distinta severitat en distints entorns: [Warn] a dev i [Deny] a pro, amb dos bindings.

Prova:

kubectl run prova --image=registry.rutasnorte.example/utilitats:latest -n rutas-norte-pro
The pods "prova" is invalid: ValidatingAdmissionPolicy 'prohibir-etiqueta-latest'
with binding 'prohibir-etiqueta-latest' denied request: Les imatges no poden fer
servir l'etiqueta "latest" i han de portar una etiqueta explícita. Fes servir una
etiqueta immutable o, millor, un digest (imatge@sha256:...). Veure la lliçó 08-05.

Exemple amb paràmetres

Les VAP poden llegir configuració d'un recurs extern, cosa que les fa reutilitzables:

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: limitar-repliques
spec:
  failurePolicy: Fail
  paramKind:
    apiVersion: v1
    kind: ConfigMap
  matchConstraints:
    resourceRules:
      - apiGroups: ["apps"]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE"]
        resources: ["deployments"]
  validations:
    - expression: >-
        object.spec.replicas <= int(params.data.maxReplicas)
      messageExpression: >-
        "El nombre de rèpliques (" + string(object.spec.replicas) +
        ") supera el màxim permès (" + params.data.maxReplicas + ")."

messageExpression construeix el missatge amb els valors reals, cosa que s'agraeix molt en depurar.

Criteri d'ús

Fes servir VAP per a regles senzilles sobre camps: prohibir latest, exigir una etiqueta, limitar un valor numèric, comprovar un prefix. Són gratis en disponibilitat i no afegeixen cap peça per mantenir.

Fes servir Kyverno per a el que VAP no pot: verificar signatures d'imatges (08-05), generar objectes, mutar, o polítiques que necessiten consultar altres recursos del clúster.

I fes servir el PSA sempre com a línia base de securityContext, perquè està integrat i no pot fallar.

A Rutas Norte fem servir els tres, en capes:

flowchart TD
    A["Tot pod que es crea"] --> B["PSA: restricted<br/>línia base de securityContext<br/>(integrat, infal·lible)"]
    B --> C["VAP: regles simples<br/>sense latest, camps obligatoris<br/>(integrat)"]
    C --> D["Kyverno: registre, signatures,<br/>etiquetes, recursos, generació<br/>(webhook)"]
    D --> E["Pod admès"]
    style B fill:#d5f9d5,stroke:#3a3
    style C fill:#d5e8f9,stroke:#36c
    style D fill:#f9f0d5,stroke:#ca3

Errors Comuns i Consells

Buscar tutorials de PodSecurityPolicy. Es van eliminar a la 1.25. Si un recurs et parla de kind: PodSecurityPolicy, està desactualitzat i probablement tota la resta també.

Activar enforce: restricted directament a producció. Els pods existents continuen corrent, però el primer reinici o la primera actualització falla. Recorre sempre les fases: warn/audit, després baseline, després restricted.

No fixar -version a les etiquetes. Sense ella es fa servir latest i una actualització del clúster pot rebutjar pods que abans passaven. Fixa la versió i puja-la com a canvi explícit.

Creure que enforce avisa en aplicar un Deployment. No: el PSA només avalua pods. El Deployment es crea i és el ReplicaSet el que falla, en un esdeveniment que cal anar a buscar. Mantén sempre warn actiu, que sí que avalua els objectes que creen pods.

Deixar namespaces sense etiquetar. Un namespace sense etiquetes de PSA equival a privileged. Comprova default i qualsevol namespace creat a mà; configura el valor per defecte a l'apiserver o exigeix les etiquetes amb Kyverno.

Relaxar el perfil de tot un namespace per una càrrega. Si el recol·lector de logs necessita hostPath, va a rutas-norte-sistema amb perfil privileged, no es baixa rutas-norte-pro a baseline.

Oblidar el RBAC del namespace privilegiat. Un namespace amb perfil privileged on desenvolupament pugui crear pods és pitjor que no tenir PSA: dona una falsa sensació de seguretat. I recorda de 08-01 que crear pods allà permet fer servir qualsevol ServiceAccount d'aquell namespace.

Fer servir failurePolicy: Fail sense alta disponibilitat del motor. Si Kyverno cau, ningú no pot crear res. Tres rèpliques, PDB i antiafinitat com a mínim, i exclou kube-system.

Oblidar initContainers i ephemeralContainers a les polítiques. Un initContainer amb una imatge sense verificar se salta tota la política de registre. Els contenidors efímers són la via de kubectl debug.

Posar validationFailureAction: Enforce des del primer dia. Igual que amb el PSA: primer Audit, mira els PolicyReport, arregla, i després Enforce.

Abusar de la mutació. Si Kyverno "arregla" els manifests, el YAML del repositori deixa de descriure el que corre. Fes-la servir només per a valors per defecte segurs.

Missatges d'error inútils. El missatge el llegeix una persona que està intentant desplegar. Digues-li què falla, per què existeix la regla i què ha de fer. Un bon missatge estalvia una interrupció a l'equip de plataforma.

Consell d'or: les polítiques són codi. Van a k8s/politiques/ del repositori, es revisen en petició de canvi, i es proven a rutas-norte-dev abans d'arribar a producció. Una política aplicada a mà a producció és exactament el mateix problema que un RoleBinding aplicat a mà.

Exercicis

Exercici 1: preparar un namespace per a restricted

L'equip crea rutas-norte-analitica per desplegar un servei d'agregació d'estadístiques d'ocupació. Escriu:

  1. El manifest del Namespace a la fase 1 (avisar sense bloquejar) amb la versió fixada.
  2. L'ordre que avaluaria un manifest existent contra el perfil sense desplegar-lo.
  3. El manifest del Namespace en el seu estat final, atès que el servei ja està endurit.

Exercici 2: interpretar i corregir un rebuig

En desplegar un component nou a rutas-norte-pro, que té enforce: restricted, obtens:

Error from server (Forbidden): error when creating "exportador.yaml":
pods "exportador-horaris" is forbidden: violates PodSecurity "restricted:v1.30":
non-default capabilities (container "exportador" must not include "NET_RAW" in
securityContext.capabilities.add), restricted volume types (volume "config-node"
uses restricted volume type "hostPath"), runAsNonRoot != true (pod or container
"exportador" must set securityContext.runAsNonRoot=true), seccompProfile
(pod or container "exportador" must set securityContext.seccompProfile.type
to "RuntimeDefault" or "Localhost")

El manifest original:

apiVersion: v1
kind: Pod
metadata:
  name: exportador-horaris
  namespace: rutas-norte-pro
spec:
  containers:
    - name: exportador
      image: registry.rutasnorte.example/exportador-horaris:2.1.0
      securityContext:
        allowPrivilegeEscalation: false
        capabilities:
          drop: ["ALL"]
          add: ["NET_RAW"]
      volumeMounts:
        - name: config-node
          mountPath: /etc/horaris
  volumes:
    - name: config-node
      hostPath:
        path: /etc/rutasnorte/horaris
        type: Directory
  1. Enumera les quatre violacions i explica què significa cadascuna.
  2. Reescriu el manifest perquè compleixi restricted, sabent que NET_RAW es va afegir "per si de cas" i que el fitxer de /etc/rutasnorte/horaris és en realitat un fitxer de configuració estàtic.
  3. Què hauries fet si NET_RAW fos realment imprescindible?

Exercici 3: política de Kyverno per a automountServiceAccountToken

A 03-06 vam establir que les càrregues que no parlen amb l'API han de portar automountServiceAccountToken: false. Escriu un ClusterPolicy de Kyverno que:

  • S'apliqui a Pods de rutas-norte-pre i rutas-norte-pro.
  • Rebutgi els pods que no declarin automountServiceAccountToken explícitament (ni a true ni a false), obligant que sigui una decisió conscient.
  • Exceptuï els pods la ServiceAccount dels quals sigui api-reserves o agent-inventari, que sí que necessiten el token.
  • Inclogui un missatge que expliqui què fer.

Indica també com el desplegaries de forma segura.

Solucions

Solució 1

1. Fase 1: avisar sense bloquejar

# k8s/entorns/analitica/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: rutas-norte-analitica
  labels:
    app.kubernetes.io/part-of: rutas-norte
    entorn: pro
    # Fase 1: NO es posa enforce. Només observem.
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/warn-version: v1.30
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/audit-version: v1.30
  annotations:
    seguretat.rutasnorte.example/fase-psa: >-
      Fase 1 (warn+audit) des de 2026-08-06. Objectiu: enforce:restricted
      abans de 2026-09-01. Responsable: equip de plataforma.

L'anotació amb data objectiu evita que "temporal" es converteixi en "permanent", que és com acaben gairebé totes les fases 1.

2. Avaluar sense desplegar

kubectl apply -f k8s/entorns/analitica/agregador.yaml --dry-run=server
Warning: would violate PodSecurity "restricted:v1.30": allowPrivilegeEscalation != false
(container "agregador" must set securityContext.allowPrivilegeEscalation=false),
unrestricted capabilities (container "agregador" must set
securityContext.capabilities.drop=["ALL"])
deployment.apps/agregador created (server dry run)

--dry-run=server passa l'objecte per tota la cadena d'admissió —PSA, VAP i Kyverno inclosos— i retorna el veredicte sense desar res. És diferent de --dry-run=client, que només valida el YAML localment i no detectaria res d'això.

3. Estat final

apiVersion: v1
kind: Namespace
metadata:
  name: rutas-norte-analitica
  labels:
    app.kubernetes.io/part-of: rutas-norte
    entorn: pro
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: v1.30
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/audit-version: v1.30
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/warn-version: v1.30

Es mantenen audit i warn encara que enforce ja bloquegi: warn continua avaluant els Deployments (que enforce no veu) i audit deixa constància al registre d'auditoria.

Solució 2

1. Les quatre violacions:

Violació Significat
non-default capabilities: NET_RAW restricted només permet tornar a afegir NET_BIND_SERVICE. NET_RAW (sockets en brut) està prohibit
restricted volume types: hostPath hostPath no és a la llista de volums permesos per restricted. És la formalització de l'advertiment de 05-01
runAsNonRoot != true No es va declarar; restricted exigeix la declaració explícita, no n'hi ha prou que la imatge tingui un USER
seccompProfile Falta RuntimeDefault. Recorda de 08-02 que sense declarar-lo probablement no hi ha cap filtre

2. Manifest corregit:

# k8s/entorns/pro/exportador-horaris.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: horaris-config
  namespace: rutas-norte-pro
  labels:
    app: exportador-horaris
    app.kubernetes.io/part-of: rutas-norte
    entorn: pro
data:
  horaris.yaml: |
    linies:
      - codi: N-101
        origen: Bilbao
        desti: Santander
        sortides: ["07:00", "10:30", "15:00", "19:45"]
---
apiVersion: v1
kind: Pod
metadata:
  name: exportador-horaris
  namespace: rutas-norte-pro
  labels:
    app: exportador-horaris
    app.kubernetes.io/part-of: rutas-norte
    entorn: pro
spec:
  automountServiceAccountToken: false
  securityContext:
    runAsNonRoot: true                # violació 3
    runAsUser: 10005
    runAsGroup: 10005
    seccompProfile:
      type: RuntimeDefault            # violació 4
  containers:
    - name: exportador
      image: registry.rutasnorte.example/exportador-horaris:2.1.0
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true  # no ho exigeix restricted, però és el correcte
        capabilities:
          drop: ["ALL"]               # violació 1: sense l'add de NET_RAW
      volumeMounts:
        - name: horaris               # violació 2: ConfigMap en lloc de hostPath
          mountPath: /etc/horaris
          readOnly: true
        - name: temporal
          mountPath: /tmp
      resources:
        requests: { cpu: 50m, memory: 64Mi }
        limits:   { memory: 128Mi }
  volumes:
    - name: horaris
      configMap:
        name: horaris-config
    - name: temporal
      emptyDir: { sizeLimit: 32Mi }

Els quatre canvis i la seva justificació:

  1. NET_RAW eliminat. Era "per si de cas": permet crear sockets en brut, cosa que serveix per fer ping i per inspeccionar trànsit. Un exportador d'horaris no ho necessita. Si el procés falla en arrencar, el log ho dirà i aleshores investigarem per què; no es concedeixen capacitats preventivament.
  2. hostPath substituït per un ConfigMap. El fitxer era configuració estàtica, exactament per al que serveix un ConfigMap (03-01). A més de complir la política, això hi guanya: el fitxer deixa de dependre que algú l'hagi copiat a cada node, queda versionat a Git i és idèntic als tres entorns. La correcció de la política ha millorat el disseny, que és el que sol passar.
  3. runAsNonRoot: true + runAsUser: 10005 declarats explícitament.
  4. seccompProfile: RuntimeDefault al pod, heretat pel contenidor.

Extres que no exigia restricted però sí les polítiques de Kyverno de l'apartat 9: etiquetes de l'esquema, resources i automountServiceAccountToken: false. I readOnlyRootFilesystem: true, que cap política no exigeix però és la línia base de 08-02.

3. Si NET_RAW fos imprescindible:

El procés seria aquest, en ordre:

  1. Qüestionar el requisit. Per què necessita sockets en brut un exportador d'horaris? En el 90 % dels casos hi ha una alternativa (una comprovació TCP en lloc d'ICMP, per exemple).
  2. Si és real, fer servir una biblioteca que no ho necessiti o canviar el disseny.
  3. Si no hi ha alternativa, no relaxar rutas-norte-pro. Es desplega en un namespace propi amb enforce: baseline (que sí que permet NET_RAW), amb RBAC restringit, amb justificació anotada, amb revisió trimestral i amb aprovació del professional de seguretat.
  4. Documentar l'excepció al registre d'excepcions de seguretat, amb data de caducitat. Tornarem a aquest registre a 08-06.

El que mai no es fa és baixar rutas-norte-pro a baseline: això converteix una excepció d'un component en una excepció de tota la plataforma.

Solució 3

# k8s/politiques/token-serviceaccount-explicit.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: token-serviceaccount-explicit
  annotations:
    policies.kyverno.io/title: Decisió explícita sobre el token de ServiceAccount
    policies.kyverno.io/category: Seguretat
    policies.kyverno.io/severity: medium
    policies.kyverno.io/description: >-
      Tot pod ha de declarar automountServiceAccountToken de forma explícita.
      Muntar el token sense necessitar-lo lliura una credencial de l'API a un
      procés que no la fa servir (veure 03-06). Les càrregues que sí que parlen
      amb l'API estan exceptuades per nom de ServiceAccount.
spec:
  validationFailureAction: Enforce
  background: true
  rules:
    - name: exigir-declaracio-explicita
      match:
        any:
          - resources:
              kinds: [Pod]
              namespaces:
                - rutas-norte-pre
                - rutas-norte-pro
      exclude:
        any:
          # Càrregues que legítimament necessiten el token
          - resources:
              subjects:
                - kind: ServiceAccount
                  name: api-reserves
                - kind: ServiceAccount
                  name: agent-inventari
      preconditions:
        all:
          # No aplicar als pods que crea el mateix sistema
          - key: "{{ request.object.metadata.namespace }}"
            operator: AnyIn
            value: ["rutas-norte-pre", "rutas-norte-pro"]
      validate:
        message: >-
          El pod ha de declarar spec.automountServiceAccountToken de forma
          explícita. Si la càrrega NO parla amb l'API de Kubernetes (l'habitual),
          posa "automountServiceAccountToken: false". Si sí que ho fa, posa'l a true
          i assegura't que la seva ServiceAccount té un Role amb el mínim
          privilegi (veure 08-01 i 03-06).
        pattern:
          spec:
            automountServiceAccountToken: "false | true"

Notes sobre la solució:

  • El patró "false | true" obliga que el camp existeixi amb un d'aquells dos valors. Si el camp no hi és, la validació falla, que és just el que demanava l'enunciat: convertir una omissió en una decisió.
  • exclude.any.resources.subjects permet exceptuar per ServiceAccount. Una alternativa més mantenible a llarg termini seria exceptuar per una etiqueta (seguretat.rutasnorte.example/usa-api: "true"), per no haver d'editar la política cada vegada que una càrrega nova necessiti l'API.
  • És una política de govern, no de bloqueig d'un atac: el seu valor està a forçar que algú hi pensi en cada desplegament.

Desplegament segur, en quatre passos:

# 1. Aplicar en mode Audit: no bloqueja res
sed 's/validationFailureAction: Enforce/validationFailureAction: Audit/' \
  k8s/politiques/token-serviceaccount-explicit.yaml | kubectl apply -f -

# 2. Esperar que l'anàlisi en segon pla completi l'informe i revisar-lo
kubectl get policyreport -A -o json | jq -r '
  .items[].results[]
  | select(.policy == "token-serviceaccount-explicit" and .result == "fail")
  | "\(.resources[0].namespace)/\(.resources[0].name)"'
rutas-norte-pro/worker-notificacions-6d4f8b9c7-k2m4x
rutas-norte-pro/informes-ocupacio-29187360-9wzqt
rutas-norte-pre/botiga-web-7c8d9f6b5-p3n8v
# 3. Corregir els manifests d'aquelles càrregues (afegir el camp explícit)
#    i verificar que l'informe queda net.

# 4. Només aleshores, passar a Enforce
kubectl apply -f k8s/politiques/token-serviceaccount-explicit.yaml

I la verificació final:

kubectl run prova --image=registry.rutasnorte.example/utilitats:1.4.2 -n rutas-norte-pro
Error from server: admission webhook "validate.kyverno.svc-fail" denied the request:

resource Pod/rutas-norte-pro/prova was blocked due to the following policies

token-serviceaccount-explicit:
  exigir-declaracio-explicita: 'validation error: El pod ha de declarar
    spec.automountServiceAccountToken de forma explícita...'

Una precaució addicional important: abans de passar a Enforce, comprova que la política no bloqueja els controladors del sistema que creen pods en aquells namespaces (el controlador de ReplicaSets, el de Jobs). Si un controlador crea pods sense el camp, els teus desplegaments deixaran de funcionar. Provar-ho primer a rutas-norte-dev amb Enforce durant uns dies és la manera barata de descobrir-ho.

Conclusió

Hem convertit l'enduriment de 08-02 en una cosa que el clúster fa complir per si sol:

  • El control d'admissió és la tercera porta: RBAC decideix si tens dret a l'operació, l'admissió decideix si l'objecte és acceptable. L'objecte rebutjat mai no arriba a etcd.
  • Les PodSecurityPolicy són història: obsoletes a la 1.21, eliminades a la 1.25. Els seus tres defectes —RBAC contraintuïtiu, ordre alfabètic impredictible i mutació— expliquen el disseny del seu substitut.
  • Els Pod Security Standards defineixen tres perfils: privileged (sense restriccions), baseline (bloqueja les escalades conegudes) i restricted (l'objectiu per a tota aplicació), que és essencialment la codificació oficial del que vam fer a mà a 08-02.
  • El Pod Security Admission va integrat a l'apiserver i s'activa amb etiquetes al namespace, amb tres modes —enforce, audit, warn— que permeten l'adopció gradual. Fixa sempre -version perquè una actualització del clúster no trenqui res.
  • L'adopció correcta és per fases: warn+audit, després enforce: baseline, després enforce: restricted. Mai de cop a producció. I rutas-norte-pre ha de ser tan estricte com rutas-norte-pro.
  • Les càrregues que legítimament necessiten privilegis —CNI, CSI, recol·lector de logs— van a un namespace a part amb perfil privileged, RBAC restringit i justificació anotada. Mai no es relaxa el perfil de producció per una càrrega.
  • El PSA té límits clars: només mira el pod, no pot exigir etiquetes, límits de recursos ni registres d'imatges, i no muta.
  • Aquell buit el cobreixen Kyverno (YAML, muta, genera, verifica signatures: l'elecció de Rutas Norte) i OPA Gatekeeper (Rego, genèric més enllà de Kubernetes), i les ValidatingAdmissionPolicy amb CEL cobreixen les regles senzilles sense instal·lar res.
  • Rutas Norte fa servir els tres en capes: PSA com a base infal·lible, VAP per a regles simples, Kyverno per al registre obligatori, les etiquetes de l'esquema, els límits de recursos i la generació automàtica de la NetworkPolicy deny-all.

Ara el clúster rebutja per si mateix un pod privilegiat, un que munti hostPath, un que corri com a root o un la imatge del qual no vingui del registre de l'empresa. Ningú no s'ho pot saltar per oblit.

Però tornem a mirar el conjunt. Hem protegit qui pot fer què (08-01) i què pot fer un contenidor (08-02 i 08-03). Falta una dimensió sencera: què pot parlar amb què. A 04-06 vam posar una NetworkPolicy deny-all a rutas-norte-pro i vam autoritzar les converses una a una, i ja aleshores vam assenyalar dos límits incòmodes: les polítiques treballen a L3/L4 —no entenen de rutes HTTP ni de mètodes— i no registren res, així que un intent de connexió denegat és invisible. A més, dins del clúster tot el trànsit viatja sense xifrar un cop passat el TLS de l'Ingress: qui pugui observar la xarxa del node veu les consultes a postgres-reserves en clar, amb les dades personals incloses. I el trànsit sortint continua pràcticament sense restringir, que és la via natural per la qual s'exfiltra una base de dades de clients.

La lliçó següent, 08-04, Seguretat de Xarxa, construeix l'estratègia completa sobre el que ja saps: microsegmentació, control del trànsit de sortida i el problema de les IP davant dels noms de domini, xifratge en trànsit amb mTLS i malles de serveis —Istio, Linkerd i Cilium comparades, amb el criteri honest de quan en cal una i quan no—, protecció del perímetre i del pla de control, i la visibilitat del trànsit que és just el que li falta a NetworkPolicy.

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