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-reservesiinformes-ocupacioa Rutas Norte— el responsable de compliment normatiu ha de conèixer i aprovar què s'exigeix i quines excepcions existeixen.
Contingut
- De la norma escrita a la norma aplicada
- Context històric: les PodSecurityPolicy i per què van desaparèixer
- Els Pod Security Standards:
privileged,baselineirestricted - Pod Security Admission: activar-lo amb etiquetes al namespace
- Aplicació progressiva a Rutas Norte
- Càrregues que legítimament necessiten privilegis
- Les limitacions del Pod Security Admission
- Motors de política general: Kyverno i OPA Gatekeeper
- Polítiques de Kyverno per a Rutas Norte
- ValidatingAdmissionPolicy: CEL natiu sense instal·lar res
- Errors comuns i consells
- Exercicis
- Conclusió
- 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.
- 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.
- Els Pod Security Standards:
privileged, baseline i restricted
privileged, baseline i restrictedEls 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_CHROOTEl 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
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.
- 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.30La sintaxi de l'etiqueta és:
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:
enforcenomés actua sobre pods que es creen o s'actualitzen. No afecta els que ja estan corrent. Si activesenforce: restricteden 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ó
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 privilegisAixò é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.yamlWarning: 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 configuredLlegeix 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.
- 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.30Aquí 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:
--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-checkerTambé 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> restrictedEls <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.30Ara, si algú intenta desplegar un pod privilegiat:
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.2kubectl 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.yamlError 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"]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:
Sense avisos. Perfecte.
El component que no compleix: el recol·lector de logs
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.
- 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:
auditiwarncontinuen enrestricted. Encara queenforcesiguiprivileged, 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.- 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. - RBAC estricte al damunt. Recordant 08-01: qui pot crear pods aquí pot crear pods privilegiats. Només
plataformaha 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.ioSolució 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.
- 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:
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.
- 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 | Sí | Sí |
| Mutar | Sí, molt ben resolt | Sí, més limitat |
| Generar objectes | Sí (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-allde 04-06 en cada namespace nou. - La mutació resol el "afegeix
seccompProfilea 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=2Tres rèpliques i un PDB: el motor de política és infraestructura crítica i cal tractar-la com a tal.
- 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: Enforcerebutja. L'alternativa ésAudit, que només genera un informe. Igual que amb el PSA, es comença enAuditi es passa aEnforcequan ja no hi ha incompliments.background: truefa que Kyverno avaluï també els objectes que ja existeixen, produintPolicyReport. Sense això només veuries els objectes nous.match.any.resourcesdefineix l'àmbit. Aquí, pods dels tres namespaces de Rutas Norte.kyverno,kube-systemirutas-norte-sistemaqueden 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 dekubectl 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:
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: RuntimeDefaultAmb +(), 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 -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.
- 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) |
Sí |
| 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-nortePunts a observar:
validationActionscompleix el mateix paper que els modes del PSA:[Warn, Audit]per provar,[Deny]quan n'estàs segur. Es poden combinar:[Deny, Audit].namespaceSelectoraplica 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]adevi[Deny]apro, amb dos bindings.
Prova:
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:
- El manifest del
Namespacea la fase 1 (avisar sense bloquejar) amb la versió fixada. - L'ordre que avaluaria un manifest existent contra el perfil sense desplegar-lo.
- El manifest del
Namespaceen 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- Enumera les quatre violacions i explica què significa cadascuna.
- Reescriu el manifest perquè compleixi
restricted, sabent queNET_RAWes va afegir "per si de cas" i que el fitxer de/etc/rutasnorte/horarisés en realitat un fitxer de configuració estàtic. - Què hauries fet si
NET_RAWfos 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-preirutas-norte-pro. - Rebutgi els pods que no declarin
automountServiceAccountTokenexplícitament (ni atrueni afalse), obligant que sigui una decisió conscient. - Exceptuï els pods la ServiceAccount dels quals sigui
api-reservesoagent-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
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.30Es 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ó:
NET_RAWeliminat. Era "per si de cas": permet crear sockets en brut, cosa que serveix per ferpingi 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.hostPathsubstituït per unConfigMap. 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.runAsNonRoot: true+runAsUser: 10005declarats explícitament.seccompProfile: RuntimeDefaultal 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:
- 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).
- Si és real, fer servir una biblioteca que no ho necessiti o canviar el disseny.
- Si no hi ha alternativa, no relaxar
rutas-norte-pro. Es desplega en un namespace propi ambenforce: baseline(que sí que permetNET_RAW), amb RBAC restringit, amb justificació anotada, amb revisió trimestral i amb aprovació del professional de seguretat. - 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.subjectspermet 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.yamlI la verificació final:
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) irestricted(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-versionperquè una actualització del clúster no trenqui res. - L'adopció correcta és per fases:
warn+audit, desprésenforce: baseline, desprésenforce: restricted. Mai de cop a producció. Irutas-norte-preha de ser tan estricte comrutas-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
- Què és Kubernetes?
- Arquitectura de Kubernetes
- Conceptes i Terminologia Clau
- Configuració d'un Clúster de Kubernetes
- La CLI de Kubernetes: kubectl
- Objectes, Manifests YAML i el Model Declaratiu
- El Projecte del Curs: la Plataforma Rutas Norte
Mòdul 2: Components Principals de Kubernetes
- Pods
- ReplicaSets
- Deployments
- Actualitzacions, Rollbacks i Estratègies de Desplegament
- Serveis
- Namespaces
- Etiquetes, Selectors i Anotacions
Mòdul 3: Gestió de Configuració i Secrets
- ConfigMaps
- Secrets
- Variables d'Entorn
- Quotes i Límits de Recursos
- LimitRanges i Classes de Qualitat de Servei (QoS)
- ServiceAccounts i Accés a l'API des dels Pods
Mòdul 4: Xarxes a Kubernetes
- Xarxes de Clúster
- Tipus de Serveis
- DNS Intern i Descobriment de Serveis
- Controladors d'Ingress
- TLS i Gestió de Certificats amb cert-manager
- Polítiques de Xarxa
Mòdul 5: Emmagatzematge a Kubernetes
- Volums
- Volums Persistents
- Reclamacions de Volums Persistents
- Classes d'Emmagatzematge
- Aprovisionament Dinàmic, Expansió i Snapshots
- Còpies de Seguretat i Restauració de Dades
Mòdul 6: Conceptes Avançats de Kubernetes
- StatefulSets
- DaemonSets
- Treballs i CronJobs
- Init Containers, Sidecars i Patrons Multicontenidor
- Planificació: Afinitat, Taints i Toleracions
- Definicions de Recursos Personalitzats (CRDs)
- Operadors i el Patró Controlador
Mòdul 7: Monitoratge i Registre
- Verificacions de Salut i Sondes
- Servidor de Mètriques i kubectl top
- Monitoratge amb Prometheus
- Visualització i Alertes amb Grafana i Alertmanager
- Registre Centralitzat amb Elasticsearch, Fluentd i Kibana (EFK)
- Depuració d'Aplicacions i Esdeveniments del Clúster
Mòdul 8: Seguretat a Kubernetes
- Control d'Accés Basat en Rols (RBAC)
- Contextos de Seguretat i Enduriment del Contenidor
- Polítiques de Seguretat de Pods i Pod Security Standards
- Seguretat de Xarxa
- Seguretat d'Imatges
- Auditoria, Escaneig i Gestió de Vulnerabilitats
Mòdul 9: Escalat i Rendiment
- Autoescalat Horitzontal de Pods
- Autoescalat Vertical de Pods
- Autoescalat de Clúster
- Escalat per Esdeveniments i Mètriques Personalitzades amb KEDA
- Alta Disponibilitat: PodDisruptionBudgets i Topologia
- Ajust de Rendiment
Mòdul 10: Ecosistema i Eines de Kubernetes
- Minikube i Entorns Locals amb kind
- Kubeadm
- Helm
- Kustomize
- GitOps amb Argo CD i Flux
- Kubernetes Gestionat: EKS, AKS i GKE
Mòdul 11: Estudis de Cas i Aplicacions del Món Real
- Desplegament d'una Aplicació Web
- Execució d'Aplicacions amb Estat
- CI/CD amb Kubernetes
- Estratègies de Desplegament: Blue-Green i Canary
- Gestió Multi-Clúster
- Operació en Producció: Incidències, Runbooks i Costos
