Lesson  in  Kubernetes per Ingegneri — Dal Container al Cluster

Scheduling, RBAC e Sicurezza

RBAC con Role, ClusterRole, RoleBinding e ServiceAccount (principio del minimo privilegio). TopologySpreadConstraints, Taints/Tolerations. Pod Security Standards e SecurityContext.

Playground per questo incontro: usa il playground Kubernetes multi-nodo su iximiuz Labs: https://labs.iximiuz.com/playgrounds/k8s-omni


Obiettivi dell'incontro

Al termine di questo incontro i partecipanti saranno in grado di:

  • Progettare un sistema RBAC con Role, ClusterRole, RoleBinding e ServiceAccount
  • Applicare il principio del minimo privilegio e verificarlo con kubectl auth can-i
  • Distribuire workload in più zone con TopologySpreadConstraints
  • Configurare Taints e Tolerations per nodi dedicati
  • Usare PriorityClass per gestire la preemption dei Pod
  • Applicare ResourceQuota e LimitRange per la governance multi-tenant
  • Fare hardening di un Pod usando SecurityContext (runAsNonRoot, readOnlyRootFilesystem, capabilities)
  • Applicare Pod Security Standards a un namespace

Teoria (50 min)

RBAC — Role-Based Access Control

Kubernetes RBAC risponde alla domanda: "chi può fare cosa su quali risorse?"

I 4 Oggetti RBAC

OggettoScopeFunzione
RoleNamespaceDefinisce permessi su risorse in un namespace
ClusterRoleClusterPermessi cluster-wide (nodi, PV, namespace, CRD)
RoleBindingNamespaceAssocia Role o ClusterRole a Subject nel namespace
ClusterRoleBindingClusterAssocia ClusterRole a Subject a livello cluster

Subjects (chi ha i permessi):

  • User — utente umano (da certificato o OIDC token)
  • Group — gruppo di utenti
  • ServiceAccount — identità per i Pod

Role e ClusterRole

# Role: permessi limitati a un namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: team-a
  name: pod-reader
rules:
- apiGroups: [""]            # "" = core API group (Pod, Service, ConfigMap...)
  resources: ["pods", "pods/log", "pods/exec"]
  # NOTA: "pods/log" e "pods/exec" sono sub-resource — richiedono permesso esplicito
  # separato dal permesso su "pods". Avere "get" su "pods" NON dà automaticamente
  # accesso a "pods/log" o "pods/exec". Devono essere dichiarati esplicitamente.
  verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
  resources: ["deployments"]
  verbs: ["get", "list"]
---
# ClusterRole: per risorse cluster-wide o da riusare in più namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: monitoring-reader
rules:
- apiGroups: [""]
  resources: ["pods", "nodes", "endpoints", "services"]
  verbs: ["get", "list", "watch"]
- apiGroups: ["metrics.k8s.io"]
  resources: ["pods", "nodes"]
  verbs: ["get", "list"]

Verbs disponibili:

VerbOperazione HTTPEquivalente kubectl
getGET /resource/:namekubectl get pod myapp
listGET /resourcekubectl get pods
watchGET /resource?watch=truekubectl get pods -w
createPOST /resourcekubectl create / apply
updatePUT /resource/:namekubectl apply (modifica)
patchPATCH /resource/:namekubectl patch
deleteDELETE /resource/:namekubectl delete
deletecollectionDELETE /resourcekubectl delete pods --all
escalatespecialeCreare Role con permessi superiori ai propri
bindspecialeCreare RoleBinding per un Role

RoleBinding e ClusterRoleBinding

# RoleBinding: assegna un Role (o ClusterRole) in un namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  namespace: team-a
  name: read-pods-binding
subjects:
# Associa a un ServiceAccount
- kind: ServiceAccount
  name: monitoring-sa
  namespace: monitoring
# Associa a un utente (da certificato/OIDC)
- kind: User
  name: alice@example.com
  apiGroup: rbac.authorization.k8s.io
# Associa a un gruppo
- kind: Group
  name: system:masters
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role           # o ClusterRole
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io

ServiceAccount per i Pod

Ogni Pod ha un ServiceAccount associato (default se non specificato). Il token viene automontato in /var/run/secrets/kubernetes.io/serviceaccount/.

# Crea un ServiceAccount dedicato
apiVersion: v1
kind: ServiceAccount
metadata:
  name: myapp-sa
  namespace: production
automountServiceAccountToken: false   # Non montare il token se non serve
---
# Deployment che usa il ServiceAccount
spec:
  serviceAccountName: myapp-sa
  automountServiceAccountToken: true  # Sovrascrive il default del SA
# Dentro un Pod, usa il token per chiamare l'apiserver
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
CACERT=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt

curl -s --cacert $CACERT \
  -H "Authorization: Bearer $TOKEN" \
  https://kubernetes.default.svc.cluster.local/api/v1/namespaces/default/pods

Verifica RBAC con kubectl auth can-i

# Verifica le proprie autorizzazioni
kubectl auth can-i create pods
kubectl auth can-i delete secrets -n production

# Impersona un ServiceAccount (richiede --as)
kubectl auth can-i create pods \
  --as=system:serviceaccount:team-a:team-a-sa \
  -n team-a
# → yes

kubectl auth can-i create pods \
  --as=system:serviceaccount:team-b:team-b-sa \
  -n team-a
# → no

# Vedi TUTTO ciò che può fare un ServiceAccount
kubectl auth can-i --list \
  --as=system:serviceaccount:production:myapp-sa \
  -n production

Principio del minimo privilegio: ogni ServiceAccount deve avere solo i verbi e le risorse strettamente necessarie per il funzionamento dell'applicazione. cluster-admin è quasi sempre sbagliato — e pericoloso.


Cordon, Drain e Uncordon: Manutenzione dei Nodi

Quando serve fare manutenzione su un nodo (upgrade OS, sostituzione hardware, patching kernel), Kubernetes offre tre comandi per gestire i workload in modo sicuro:

cordon    →  Marca il nodo come non schedulabile (SchedulingDisabled)
              I Pod esistenti continuano a girare, nessun nuovo Pod viene schedulato

drain     →  Cordon + evict di tutti i Pod (tranne DaemonSet)
              I Pod vengono ricreati su altri nodi dai controller (Deployment, StatefulSet)

uncordon  →  Rimuove il flag SchedulingDisabled
              Il nodo torna disponibile per nuovi Pod

Cordon: Blocca Nuovi Pod

# Marca il nodo come non schedulabile
kubectl cordon node-02
# → node/node-02 cordoned

# Verifica lo stato
kubectl get nodes
# NAME      STATUS                     ROLES    AGE
# node-01   Ready                      worker   5d
# node-02   Ready,SchedulingDisabled   worker   5d   ← cordoned

# I Pod esistenti su node-02 continuano a funzionare normalmente!

Drain: Evacua il Nodo

# Drain base: evict tutti i Pod (tranne DaemonSet)
kubectl drain node-02 --ignore-daemonsets

# Drain con Pod che usano emptyDir (i dati in emptyDir vengono persi)
kubectl drain node-02 --ignore-daemonsets --delete-emptydir-data

# Drain forzato: evict anche Pod non gestiti da controller (standalone Pod)
# ⚠️ ATTENZIONE: i Pod standalone vengono ELIMINATI senza ricrearsi!
kubectl drain node-02 --ignore-daemonsets --force

# Drain con timeout (utile in automazione)
kubectl drain node-02 --ignore-daemonsets --timeout=120s

# Drain con grace period ridotto (default: usa il terminationGracePeriodSeconds del Pod)
kubectl drain node-02 --ignore-daemonsets --grace-period=30

Cosa succede durante il drain:

1. kubectl cordon node-02             → nodo diventa SchedulingDisabled
2. Per ogni Pod sul nodo:
   a. Rispetta PodDisruptionBudget (se presente)
   b. Invia SIGTERM al container
   c. Attende terminationGracePeriodSeconds
   d. Se non termina → SIGKILL
3. Il controller (Deployment/ReplicaSet) crea il Pod sostitutivo su un altro nodo
4. StatefulSet: ricrea il Pod con lo stesso nome e PVC (ordinato)
5. DaemonSet: i Pod DaemonSet vengono ignorati (--ignore-daemonsets)

⚠️ kubectl drain e dati emptyDir: il flag --delete-emptydir-data distrugge permanentemente i dati nei volumi emptyDir. Prima di fare drain, verifica quali Pod usano emptyDir:

kubectl get pods --field-selector spec.nodeName=node-02 -o json | \
  jq '.items[] | select(.spec.volumes[]?.emptyDir) | .metadata.name'

Uncordon: Rimetti il Nodo in Servizio

# Dopo la manutenzione, riabilita lo scheduling
kubectl uncordon node-02
# → node/node-02 uncordoned

# Verifica
kubectl get nodes
# NAME      STATUS   ROLES    AGE
# node-02   Ready    worker   5d   ← tornato Ready

Nota: dopo uncordon i Pod evictati non tornano automaticamente sul nodo. Restano dove sono stati rischedulati. Il nodo riceverà solo nuovi Pod.

PodDisruptionBudget (PDB): Protezione durante il Drain

I PDB garantiscono che un certo numero di Pod resti sempre disponibile durante operazioni di disruption volontaria (drain, upgrade, scaling down):

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api-pdb
  namespace: production
spec:
  minAvailable: 2          # Almeno 2 Pod devono restare attivi
  # oppure:
  # maxUnavailable: 1      # Al massimo 1 Pod può essere down
  selector:
    matchLabels:
      app: api
# Se fai drain e il PDB non è soddisfatto:
# → "Cannot evict pod as it would violate the pod's disruption budget"
# Il drain si blocca finché il PDB non è rispettato (es. un nuovo Pod diventa Ready altrove)

# Controlla i PDB
kubectl get pdb -A
kubectl describe pdb api-pdb -n production

Workflow tipico di manutenzione nodo:

# 1. Verifica cosa gira sul nodo
kubectl get pods --field-selector spec.nodeName=node-02 -A

# 2. Controlla i PDB
kubectl get pdb -A

# 3. Drain (con pazienza)
kubectl drain node-02 --ignore-daemonsets --delete-emptydir-data --timeout=300s

# 4. Esegui la manutenzione (upgrade, patch, reboot...)
ssh node-02 'sudo apt update && sudo apt upgrade -y && sudo reboot'

# 5. Verifica che il nodo è tornato Ready
kubectl get nodes -w  # -w = watch

# 6. Riabilita lo scheduling
kubectl uncordon node-02

Scheduling Avanzato

TopologySpreadConstraints

Distribuisce Pod uniformemente su zone, nodi, o qualsiasi topology key.

spec:
  topologySpreadConstraints:
  # Constraint 1: max 1 Pod di differenza tra zone
  - maxSkew: 1
    topologyKey: topology.kubernetes.io/zone   # Label del nodo
    whenUnsatisfiable: DoNotSchedule           # o ScheduleAnyway
    labelSelector:
      matchLabels:
        app: myapp
  # Constraint 2: max 2 Pod di differenza tra nodi
  - maxSkew: 2
    topologyKey: kubernetes.io/hostname
    whenUnsatisfiable: ScheduleAnyway
    labelSelector:
      matchLabels:
        app: myapp
# Simula zone sui nodi del playground
kubectl label node node1 topology.kubernetes.io/zone=zone-a
kubectl label node node2 topology.kubernetes.io/zone=zone-b
kubectl label node node3 topology.kubernetes.io/zone=zone-c

# Crea Deployment con TopologySpreadConstraints
kubectl apply -f deployment-multizone.yaml

# Verifica la distribuzione
kubectl get pods -o wide
kubectl get pods -o custom-columns=NAME:.metadata.name,NODE:.spec.nodeName | \
  sort -k2 | uniq -f1 -c

whenUnsatisfiable values:

  • DoNotSchedule — il Pod rimane Pending finché il constraint non può essere soddisfatto
  • ScheduleAnyway — schedula comunque ma minimizza lo skew (best-effort)

Taints e Tolerations

I Taints su un nodo respingono i Pod che non hanno la toleration corrispondente.

# Aggiungi taint su un nodo (3 effetti possibili)
kubectl taint node node1 dedicated=gpu:NoSchedule    # Non schedula nuovi Pod
kubectl taint node node1 dedicated=gpu:NoExecute     # Evict i Pod esistenti senza toleration
kubectl taint node node1 dedicated=gpu:PreferNoSchedule  # Preferibilmente non schedulare

# Rimuovi un taint
kubectl taint node node1 dedicated=gpu:NoSchedule-
# Pod che può girare su nodi GPU (toleration corrispondente)
spec:
  tolerations:
  - key: dedicated
    operator: Equal
    value: gpu
    effect: NoSchedule
  # Toleration wildcard: tollera qualsiasi taint con questa key
  - key: dedicated
    operator: Exists
    effect: NoSchedule
  # Toleration per nodo non-ready (evict dopo 60s invece del default 300s)
  - key: node.kubernetes.io/not-ready
    operator: Exists
    effect: NoExecute
    tolerationSeconds: 60

Node Affinity

spec:
  affinity:
    nodeAffinity:
      # HARD: se non soddisfatto, Pod rimane Pending
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: kubernetes.io/arch
            operator: In
            values: ["amd64"]
          - key: node-type
            operator: In
            values: ["compute", "high-memory"]

      # SOFT: preferisce certi nodi ma accetta altri
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        preference:
          matchExpressions:
          - key: topology.kubernetes.io/zone
            operator: In
            values: ["zone-a"]

PriorityClass e Preemption

Le PriorityClass assegnano una priorità numerica ai Pod. Quando il cluster è saturo, lo scheduler può fare preemption: evictare Pod a bassa priorità per far posto a Pod a priorità più alta.

# Definizione di PriorityClass
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: high-priority
value: 1000000
globalDefault: false
preemptionPolicy: PreemptLowerPriority  # o Never per disabilitare preemption
description: "Per workload critici di produzione"
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: low-priority
value: 100
globalDefault: false
preemptionPolicy: Never
description: "Per batch job e workload non critici"
# Pod che usa una PriorityClass
apiVersion: v1
kind: Pod
metadata:
  name: critical-api
spec:
  priorityClassName: high-priority
  containers:
  - name: api
    image: myapp:v1
    resources:
      requests:
        cpu: "500m"
        memory: "512Mi"

PriorityClass di sistema (built-in):

PriorityClassValoreUso
system-cluster-critical2000000000Componenti del cluster (CoreDNS, kube-proxy)
system-node-critical2000001000Componenti del nodo (kubelet, CSI driver)
(default se nessuna PriorityClass)0Tutti i Pod senza priorityClassName

Come funziona la preemption:

1. Pod "critical-api" (priority=1000000) arriva ma il nodo è pieno
2. Lo scheduler cerca Pod con priorità < 1000000 sul nodo
3. Se trova Pod a bassa priorità sufficiente per liberare risorse:
   - Invia SIGTERM ai Pod a bassa priorità (graceful termination)
   - Dopo terminationGracePeriodSeconds → SIGKILL
   - Schedula il Pod ad alta priorità
4. Se preemptionPolicy: Never → Pod rimane Pending senza evictare nessuno

Best practice:

  • Definisci 3-4 livelli (es. critical=1M, standard=1000, batch=100, preemptible=10)
  • Non usare globalDefault: true su più di una PriorityClass
  • Imposta preemptionPolicy: Never su batch job per evitare che evictino altri batch
  • Monitora preemption con kubectl get events | grep Preempted
  • Combina con ResourceQuota per limitare quanti Pod high-priority un team può creare
# Lista PriorityClass nel cluster
kubectl get priorityclasses

# Vedi quali Pod usano una specifica PriorityClass
kubectl get pods -A -o custom-columns=\
  NS:.metadata.namespace,\
  NAME:.metadata.name,\
  PRIORITY:.spec.priorityClassName,\
  PRIO-VALUE:.spec.priority

# Eventi di preemption
kubectl get events -A --field-selector reason=Preempted

ResourceQuota e LimitRange: Governance Multi-Tenant

Questa sezione approfondisce i concetti introdotti nell'Incontro 4 (Workload e Configurazione).

ResourceQuota limita le risorse totali che un namespace può consumare. È lo strumento fondamentale per il multi-tenancy:

apiVersion: v1
kind: ResourceQuota
metadata:
  name: team-backend
  namespace: backend
spec:
  hard:
    # Limiti di compute
    requests.cpu: "8"
    requests.memory: 16Gi
    limits.cpu: "16"
    limits.memory: 32Gi
    
    # Limiti di oggetti
    pods: "50"
    services: "20"
    services.loadbalancers: "2"
    persistentvolumeclaims: "10"
    secrets: "30"
    configmaps: "30"
    
    # Limiti per PriorityClass (K8s 1.12+)
    # → limita quante risorse high-priority il team può usare
  scopeSelector:
    matchExpressions:
    - operator: In
      scopeName: PriorityClass
      values: ["high-priority"]

LimitRange — imposta default e limiti per singolo container/Pod:

apiVersion: v1
kind: LimitRange
metadata:
  name: default-limits
  namespace: backend
spec:
  limits:
  - type: Container
    default:          # Limits di default se non specificati
      cpu: "500m"
      memory: "512Mi"
    defaultRequest:   # Requests di default se non specificati
      cpu: "100m"
      memory: "128Mi"
    min:              # Minimo permesso
      cpu: "50m"
      memory: "64Mi"
    max:              # Massimo permesso
      cpu: "2"
      memory: "4Gi"
  - type: Pod
    max:
      cpu: "4"        # Nessun Pod può chiedere più di 4 CPU totali
      memory: "8Gi"
  - type: PersistentVolumeClaim
    min:
      storage: 1Gi
    max:
      storage: 50Gi

Interazione tra ResourceQuota e LimitRange:

LimitRange (per container)          ResourceQuota (per namespace)
├─ min: 50m CPU                     ├─ requests.cpu: 8 (totale)
├─ max: 2 CPU                       ├─ limits.cpu: 16 (totale)
├─ default: 500m CPU                └─ pods: 50 (conteggio)
└─ defaultRequest: 100m CPU

Quando un Pod senza requests/limits viene creato:
1. LimitRange inietta i default (100m request, 500m limit)
2. ResourceQuota verifica che il totale namespace non superi 8 CPU request
3. Se la quota è esaurita → errore "exceeded quota"
# Stato corrente della quota
kubectl describe resourcequota team-backend -n backend
# → mostra used/hard per ogni risorsa

# Limiti attivi
kubectl describe limitrange default-limits -n backend

# Chi sta consumando cosa
kubectl top pods -n backend --sort-by=cpu

Best practice multi-tenant: ogni namespace di team dovrebbe avere:

  1. ResourceQuota per il cap totale
  2. LimitRange per i default (evita Pod senza limiti)
  3. NetworkPolicy per l'isolamento di rete
  4. RBAC per i permessi

Pod Security Standards e SecurityContext

Linux Capabilities

I container Docker ereditano un sottoinsieme di capabilities Linux:

# Vedi le capabilities di un container
kubectl exec myapp -- cat /proc/1/status | grep Cap
# CapPrm: 00000000a80425fb (bitmap delle capabilities permesse)

# Decodifica (su Linux)
capsh --decode=00000000a80425fb

Docker include di default: CHOWN, DAC_OVERRIDE, FSETID, FOWNER, MKNOD, NET_RAW, SETGID, SETUID, SETFCAP, SETPCAP, NET_BIND_SERVICE, SYS_CHROOT, KILL, AUDIT_WRITE.

Best practice: drop ALL, aggiungi solo quello che serve:

spec:
  containers:
  - name: app
    securityContext:
      # Non permettere escalation a root
      allowPrivilegeEscalation: false
      # Richiede che l'immagine abbia USER non-root
      runAsNonRoot: true
      runAsUser: 65534       # UID nobody
      runAsGroup: 65534
      # Filesystem root in sola lettura
      readOnlyRootFilesystem: true
      # Capabilities
      capabilities:
        drop: ["ALL"]        # Rimuovi tutto
        add: ["NET_BIND_SERVICE"]  # Solo se il processo deve bindare porte < 1024
      # Profilo seccomp: filtra le syscall permesse
      seccompProfile:
        type: RuntimeDefault   # Profilo predefinito del container runtime
# SecurityContext a livello Pod (applicato a tutti i container)
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    runAsGroup: 3000
    fsGroup: 2000            # Proprietario del volume montato
    fsGroupChangePolicy: OnRootMismatch  # Cambia owner solo se necessario
    seccompProfile:
      type: RuntimeDefault
    sysctls:                 # Parametri kernel (richiedono privilegio)
    - name: net.core.somaxconn
      value: "1024"

Volume Writable con readOnlyRootFilesystem

# Con readOnlyRootFilesystem: true, devi montare emptyDir per file temporanei
spec:
  containers:
  - name: app
    securityContext:
      readOnlyRootFilesystem: true
    volumeMounts:
    - name: tmp
      mountPath: /tmp
    - name: cache
      mountPath: /app/cache
    - name: logs
      mountPath: /var/log/app
  volumes:
  - name: tmp
    emptyDir: {}
  - name: cache
    emptyDir: {}
  - name: logs
    emptyDir: {}

Pod Security Standards (PSS)

PSS sostituisce PodSecurityPolicy (rimossa in K8s 1.25) con tre livelli predefiniti:

LivelloCosa blocca
PrivilegedNessuna restrizione (per componenti di sistema)
BaselineVieta: privileged container, hostNetwork, hostPID, hostPath, porte < 1024...
RestrictedAggiunge: runAsNonRoot, readOnlyRootFilesystem, drop ALL capabilities, seccomp
# Abilita PSS su un namespace (audit = log solo, enforce = blocca)
kubectl label namespace production \
  pod-security.kubernetes.io/enforce=restricted \
  pod-security.kubernetes.io/warn=restricted \
  pod-security.kubernetes.io/audit=restricted

# Testa se un Pod passerebbe la validazione
kubectl apply --dry-run=server -f pod.yaml -n production

CRD e Operators: Estendere l'API Kubernetes

Le Custom Resource Definitions (CRD) permettono di aggiungere nuovi tipi di risorse all'API Kubernetes — senza modificare il codice del cluster.

Hai già incontrato diverse CRD in questo corso:

  • ServiceMonitor (Prometheus Operator) — per configurare il monitoring
  • Application (ArgoCD) — per il GitOps
  • PrometheusRule — per gli alert custom
# Lista tutte le CRD installate nel cluster
kubectl get crd

# Filtra CRD di un operator specifico
kubectl get crd | grep monitoring.coreos.com
# → servicemonitors.monitoring.coreos.com
# → prometheusrules.monitoring.coreos.com
# → podmonitors.monitoring.coreos.com

# Ispeziona una CRD
kubectl describe crd servicemonitors.monitoring.coreos.com

# Usa le risorse custom come qualsiasi risorsa nativa
kubectl get servicemonitors -A
kubectl get prometheusrules -n monitoring

Un Operator è un controller che gestisce il ciclo di vita di un'applicazione complessa usando CRD. L'operator osserva le risorse custom e reagisce (reconciliation loop):

OperatorCRD principaliCosa automatizza
Prometheus OperatorServiceMonitor, PrometheusRuleScraping, alerting, retention
cert-managerCertificate, ClusterIssuerEmissione e rinnovo certificati TLS
StrimziKafka, KafkaTopicCluster Kafka, topic, utenti
CloudNativePGCluster (PostgreSQL)HA, backup, failover PostgreSQL

Risorse: esplora gli operator disponibili su OperatorHub.io — catalogo curato dalla community CNCF.


Admission Webhooks: Validazione e Mutazione

Gli Admission Webhooks sono il meccanismo con cui Kubernetes intercetta e modifica le richieste API dopo l'autenticazione e l'autorizzazione RBAC, ma prima della persistenza su etcd.

Richiesta API → Autenticazione → Autorizzazione (RBAC) → Admission Webhooks → etcd
                                                          │
                                                          ├─ Mutating → modifica il manifest
                                                          └─ Validating → accetta o rifiuta
TipoCosa faEsempio
MutatingModifica il manifest in voloIniettare sidecar, aggiungere label di default
ValidatingAccetta o rifiuta il manifestBloccare immagini non firmate, imporre naming convention

Pod Security Standards (PSS) che hai appena studiato è implementato come admission controller built-in. I policy engine esterni usano webhook:

Policy EngineTipoLinguaggio Policy
OPA GatekeeperValidating webhookRego (linguaggio dichiarativo)
KyvernoValidating + MutatingYAML nativo (no nuovo linguaggio)
# Vedi i webhook configurati nel cluster
kubectl get validatingwebhookconfigurations
kubectl get mutatingwebhookconfigurations

# Esempio: Kyverno installa webhook per le sue policy
kubectl get validatingwebhookconfigurations | grep kyverno

Quando usare i webhook: PSS copre i casi base (non-root, no privileged). Per policy aziendali personalizzate (es. "tutte le immagini devono provenire dal registry interno", "ogni Deployment deve avere un PDB"), usa Kyverno o Gatekeeper.


NetworkPolicy e Sicurezza di Rete

Il controllo dell'accesso tramite RBAC e SecurityContext protegge l'accesso all'API Kubernetes e le syscall dei container, ma non protegge la comunicazione di rete tra i Pod. Per prevenire il movimento laterale (un Pod compromesso che attacca altri servizi), bisogna usare le NetworkPolicy.

Di default Kubernetes è default-allow: ogni Pod può comunicare con qualsiasi altro Pod nel cluster, indipendentemente dal namespace. In un'architettura multi-tenant o con dati sensibili, questo è un rischio significativo.

Cross-reference: Le NetworkPolicy sono trattate in dettaglio nell'Incontro 5 (Networking). Per la security posture completa di un namespace di produzione, combina:

  • RBAC (chi può interagire con l'API)
  • SecurityContext (cosa può fare il container a livello kernel)
  • NetworkPolicy (quale traffico di rete è permesso)

Una policy di base per un namespace di produzione:

# default-deny tutto il traffico in ingresso e uscita
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress
  # Nessuna regola → tutto bloccato (aggiungi allowlist selettive per ogni servizio)

Hands-on Guidato (90 min — su iximiuz Labs)

Esercizio 1 — RBAC per Team Separati

# Setup: tre namespace, tre team
kubectl create namespace team-a
kubectl create namespace team-b
kubectl create namespace monitoring

# ServiceAccount per ogni team
kubectl create serviceaccount team-a-sa -n team-a
kubectl create serviceaccount team-b-sa -n team-b
kubectl create serviceaccount monitoring-sa -n monitoring

# Team A: accesso completo nel proprio namespace
kubectl apply -f - <<'EOF'
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: team-a
  name: full-access
rules:
- apiGroups: ["", "apps", "batch"]
  resources: ["*"]
  verbs: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  namespace: team-a
  name: team-a-full-access
subjects:
- kind: ServiceAccount
  name: team-a-sa
  namespace: team-a
roleRef:
  kind: Role
  name: full-access
  apiGroup: rbac.authorization.k8s.io
EOF

# Team B: sola lettura
kubectl apply -f - <<'EOF'
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: team-b
  name: read-only
rules:
- apiGroups: ["", "apps"]
  resources: ["pods", "deployments", "services", "configmaps"]
  verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  namespace: team-b
  name: team-b-read-only
subjects:
- kind: ServiceAccount
  name: team-b-sa
  namespace: team-b
roleRef:
  kind: Role
  name: read-only
  apiGroup: rbac.authorization.k8s.io
EOF

# Monitoring: ClusterRole per leggere da tutti i namespace
kubectl create clusterrole monitoring-reader \
  --verb=get,list,watch \
  --resource=pods,nodes,endpoints,services
kubectl create clusterrolebinding monitoring-reader \
  --clusterrole=monitoring-reader \
  --serviceaccount=monitoring:monitoring-sa

# Verifica
kubectl auth can-i create pods \
  --as=system:serviceaccount:team-a:team-a-sa -n team-a
# → yes

kubectl auth can-i create pods \
  --as=system:serviceaccount:team-b:team-b-sa -n team-b
# → no

kubectl auth can-i list pods \
  --as=system:serviceaccount:monitoring:monitoring-sa -n team-a
# → yes

Esercizio 2 — TopologySpreadConstraints: 6 Repliche in 3 Zone

# Label dei nodi con zona
kubectl label node node1 topology.kubernetes.io/zone=zone-a
kubectl label node node2 topology.kubernetes.io/zone=zone-b
kubectl label node node3 topology.kubernetes.io/zone=zone-c

kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: multizone-app
spec:
  replicas: 6
  selector:
    matchLabels:
      app: multizone-app
  template:
    metadata:
      labels:
        app: multizone-app
    spec:
      topologySpreadConstraints:
      - maxSkew: 1
        topologyKey: topology.kubernetes.io/zone
        whenUnsatisfiable: DoNotSchedule
        labelSelector:
          matchLabels:
            app: multizone-app
      containers:
      - name: app
        image: nginx:alpine
        resources:
          requests:
            memory: "16Mi"
            cpu: "10m"
EOF

# Verifica la distribuzione (2 Pod per zona)
kubectl get pods -o wide
kubectl get pods -o custom-columns=NAME:.metadata.name,NODE:.spec.nodeName \
  | awk '{print $2}' | sort | uniq -c

Esercizio 3 — Hardening di un Pod Insicuro

# Pod insicuro (root, filesystem scrivibile, capabilities)
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: insecure-pod
spec:
  containers:
  - name: app
    image: nginx:alpine
EOF

kubectl exec insecure-pod -- id
# uid=0(root) gid=0(root) → pericoloso!

kubectl exec insecure-pod -- touch /root/evil-file
# → Funziona! Root può scrivere ovunque

# Pod hardened
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: hardened-pod
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    runAsGroup: 3000
    fsGroup: 2000
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: app
    image: nginx:unprivileged   # Versione di nginx che supporta non-root
    securityContext:
      allowPrivilegeEscalation: false
      readOnlyRootFilesystem: true
      capabilities:
        drop: ["ALL"]
    volumeMounts:
    - name: tmp
      mountPath: /tmp
    - name: nginx-cache
      mountPath: /var/cache/nginx
    - name: nginx-run
      mountPath: /var/run
    resources:
      requests:
        memory: "32Mi"
        cpu: "50m"
  volumes:
  - name: tmp
    emptyDir: {}
  - name: nginx-cache
    emptyDir: {}
  - name: nginx-run
    emptyDir: {}
EOF

kubectl exec hardened-pod -- id
# uid=1000 gid=3000 → non root

kubectl exec hardened-pod -- touch /tmp/test
# → FAIL! readOnlyRootFilesystem: solo /tmp (emptyDir) è scrivibile

kubectl exec hardened-pod -- touch /root/evil-file
# → Permission denied!

Capstone Challenge — su Proxmox (ultime 2h)

"L'Attacco RBAC"

Un token di ServiceAccount è stato compromesso. Il ServiceAccount compromised-sa ha permessi eccessivi: accesso a tutti i Secret del cluster.

Il tuo compito:

1. Analisi: identifica i permessi attuali del SA compromesso

kubectl auth can-i --list \
  --as=system:serviceaccount:production:compromised-sa -n production

2. Minimo privilegio: analizza cosa l'applicazione usa realmente

# Guarda i log dell'app per capire quali API chiama
kubectl logs deploy/myapp -n production | grep -E "GET|POST|PATCH" | sort | uniq -c

3. Riduzione permessi: crea un nuovo Role con solo le azioni necessarie

# Crea nuovo ServiceAccount con permessi minimi
kubectl create serviceaccount minimal-sa -n production
# Crea Role con solo get/list/watch su pods (niente secrets!)

4. Rotazione token: aggiorna il Deployment per usare il nuovo SA

kubectl patch deployment myapp -n production \
  -p '{"spec":{"template":{"spec":{"serviceAccountName":"minimal-sa"}}}}'

5. Verifica: l'app funziona ancora e non può più accedere ai Secret

kubectl auth can-i get secrets \
  --as=system:serviceaccount:production:minimal-sa -n production
# → no  ✓

Svolgere su cluster Proxmox reale. Portare le credenziali SSH.


Self-Study Assignment

Completa il seguente tutorial su iximiuz Labs:

Loading tutorial...

Challenge consigliate su iximiuz Labs (cerca nella sezione Challenges):

  • Taints & Tolerations
  • Pod Affinity/Anti-Affinity
  • "Kube Mysteries" series

Letture consigliate:


Risorse Aggiuntive

Documentazione Ufficiale Kubernetes

Guide e Benchmark di Sicurezza

Policy Engine e Compliance

Runtime Security

  • Falco — CNCF — runtime security: rileva syscall anomale e comportamenti sospetti nei container in produzione
  • Trivy — Aqua Security — scanner all-in-one: vulnerabilità immagini, misconfigurazioni K8s YAML, secrets, SBOM
  • Starboard — Aqua Security — integra risultati di sicurezza (Trivy, Polaris, Conftest) come CRD native in Kubernetes

Blog e Articoli Tecnici

Strumenti di Audit e Visualizzazione RBAC

  • rbac-tool — analizza e visualizza le policy RBAC del cluster: chi può fare cosa, policy graph
  • kubectl-who-can — mostra chi può eseguire una specifica azione su una risorsa K8s
  • rakkess — matrice di accesso: mostra tutti i verbi che un utente può eseguire su ogni risorsa