Les tres lliçons anteriors han assegurat la identitat, el canal i el codi. Tot això s'executa damunt d'una plataforma —imatges, pods, secrets, xarxa del clúster, permisos de l'API server, pipelines— i una fallada en aquesta capa deixa sense efecte les altres: una imatge amb una biblioteca vulnerable, un contenidor que corre com a root i s'escapa al node, un Secret llegible per qualsevol del namespace, un pod que pot parlar amb qui vulgui, o un token de CI amb permisos de cluster-admin. Aquesta lliçó tanca el mòdul endurint la plataforma de TechCorp: model d'amenaces i les 4C, imatges segures (escaneig amb Trivy, signatura amb cosign, digests), securityContext complet i Pod Security Admission, secrets (Kubernetes Secrets, External Secrets Operator, rotació, OIDC a CI), NetworkPolicy amb deny-all per defecte (tancant el que 07-02 va remetre aquí), RBAC i ServiceAccount per servei, auditoria i detecció, cadena de subministrament, i una llista de comprovació amb l'estat real de TechCorp. Autenticació d'usuaris (07-01), TLS/mTLS (07-02) i OWASP/codi (07-03) només s'enllacen.

Avís. Les polítiques, versions i valors d'aquesta lliçó són didàctics. Abans d'aplicar-los en un clúster real, revisa'ls amb un professional de seguretat de plataforma i amb qui respongui del compliment normatiu; i prova cada restricció a staging, perquè moltes trenquen càrregues de treball que fins ara funcionaven.

Contingut

  1. Model d'amenaces de la plataforma i les 4C
  2. Imatges segures: base mínima, escaneig, signatura i digests
  3. Seguretat del pod: securityContext complet i Pod Security Admission
  4. Secrets: Kubernetes Secrets, External Secrets Operator i rotació
  5. Secrets a CI: GitHub Environments i OIDC
  6. Xarxa: NetworkPolicy amb deny-all per defecte
  7. RBAC de Kubernetes: ServiceAccount per servei i rols mínims
  8. Auditoria i detecció
  9. Cadena de subministrament: lockfile, SBOM i SLSA
  10. Llista de comprovació de plataforma per a TechCorp

  1. Model d'amenaces de la plataforma i les 4C

Abans d'endurir res, què pot sortir malament i a quina capa:

Amenaça Com passa Capa Mesura en aquesta lliçó
Imatge compromesa Dependència amb CVE, imatge base vella, paquet maliciós a npm Container Base mínima, Trivy a CI, signatura cosign, digest
Contenidor que s'escapa Procés com a root + kernel vulnerable o capabilities de més Container / Cluster securityContext restrictiu, seccomp, Pod Security restricted
Secret filtrat kubectl get secret per algú de més, etcd sense xifrar, secret en un log o a git Cluster / Code RBAC de lectura, xifratge a etcd, External Secrets, rotació, gitleaks (07-03)
Pod que parla amb qui no ha de parlar Sense NetworkPolicy, qualsevol pod arriba a qualsevol port Cluster Deny-all + regles explícites
Credencials de CI robades Token de llarga durada en un secret de GitHub Cloud / Cluster OIDC, Environments, GitOps sense kubeconfig a fora
RBAC excessiu cluster-admin per al pipeline o per a Argo CD "perquè així funciona" Cluster Rols mínims per namespace
Núvol mal configurat Nodes amb IP pública, API server obert, buckets públics Cloud Responsabilitat de Plataforma/proveïdor; fora de l'abast detallat

Les 4C de la seguretat cloud native (Cloud, Cluster, Container, Code) recorden que cada capa hereta la seguretat de l'exterior: el millor securityContext no salva un clúster amb l'API server obert a Internet, i el millor clúster no salva un servei-comandes amb injecció SQL. Aquesta lliçó cobreix Container i Cluster; Code va ser 07-01/07-03; Cloud és del proveïdor i de Plataforma.

  1. Imatges segures: base mínima, escaneig, signatura i digests

El Dockerfile de 05-01 ja compleix l'essencial: node:20-alpine, multi-stage (res de compiladors ni devDependencies a la imatge final), USER node, COPY --chown, HEALTHCHECK i npm ci --omit=dev amb el .npmrc com a secret de build. El que s'hi afegeix:

  • Base mínima: alpine redueix la superfície a uns pocs MB; una alternativa encara més petita és distroless (gcr.io/distroless/nodejs20-debian12: sense shell, sense gestor de paquets; kubectl exec ... sh deixa de funcionar, cosa que és un avantatge de seguretat i un cost de depuració —es resol amb kubectl debug i contenidors efímers—). TechCorp manté alpine a la fase 1 i avalua distroless per a Pagaments.
  • Escaneig a CI amb Trivy, que 05-03 va deixar configurat amb severity: CRITICAL. La política definitiva: fallar amb CRITICAL i HIGH que tinguin pedaç disponible, ignorar les que no en tenen però registrar-les, i tornar a escanejar les imatges ja desplegades cada nit (les CVE apareixen després de construir):
# .github/workflows/servei-node-ci.yml (fragment, substitueix el de 05-03)
      - name: Escaneig de vulnerabilitats
        uses: aquasecurity/[email protected]
        with:
          image-ref: ${{ env.IMATGE }}:sha-${{ github.sha }}
          severity: CRITICAL,HIGH
          ignore-unfixed: true                 # sense pedaç disponible → avís, no bloqueig (es revisa setmanalment)
          exit-code: "1"
          format: sarif
          output: trivy.sarif
      - uses: github/codeql-action/upload-sarif@v3      # les troballes apareixen a la pestanya Security del repo
        with: { sarif_file: trivy.sarif }
  • Signatura d'imatges amb cosign (Sigstore): després del push, el pipeline signa la imatge amb la identitat OIDC del workflow (sense claus a guardar, keyless), i el clúster verifica la signatura abans d'arrencar un pod, amb un controlador d'admissió (Kyverno o el policy controller de Sigstore). Només esment amb les ordres:
# A CI (permissions: id-token: write), després de docker/build-push-action:
cosign sign --yes ghcr.io/techcorp/servei-comandes@sha256:9f1c…          # signatura per digest, keyless (Fulcio + Rekor)
# Al clúster o a mà, verificar que l'ha signat el workflow de TechCorp:
cosign verify ghcr.io/techcorp/servei-comandes@sha256:9f1c… \
  --certificate-identity-regexp 'https://github.com/techcorp/.*/.github/workflows/servei-node-ci.yml@.*' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com
  • imagePullPolicy i digest: una etiqueta (1.0.1) es pot reapuntar; un digest (@sha256:…) no. A producció, l'overlay de Kustomize fixa newTag i digest (kustomize edit set image ghcr.io/techcorp/servei-comandes@sha256:9f1c…), imagePullPolicy: IfNotPresent és segur perquè el digest és immutable, i el registre ghcr.io/techcorp és privat amb imagePullSecrets (o identitat de núvol) al ServiceAccount. latest continua prohibit (05-01).

  1. Seguretat del pod: securityContext complet i Pod Security Admission

El Deployment de 05-02 tenia runAsNonRoot, runAsUser: 1000, allowPrivilegeEscalation: false i readOnlyRootFilesystem: true. La versió completa, la que exigeix el perfil restricted:

# k8s/servei-comandes/base/deployment.yaml (fragment spec.template.spec)
    spec:
      serviceAccountName: servei-comandes             # apartat 7: identitat pròpia, sense token muntat
      automountServiceAccountToken: false
      securityContext:                                # a nivell de pod: l'hereten tots els contenidors
        runAsNonRoot: true
        runAsUser: 1000
        runAsGroup: 1000
        fsGroup: 1000                                 # els volums muntats pertanyen al grup 1000 (secrets com a fitxers, apartat 4)
        seccompProfile: { type: RuntimeDefault }      # filtre de syscalls del runtime (bloqueja les perilloses i rares)
      containers:
        - name: servei-comandes
          image: ghcr.io/techcorp/servei-comandes@sha256:9f1c…       # digest a prod (apartat 2)
          securityContext:
            allowPrivilegeEscalation: false           # ni setuid ni guanyar capabilities
            readOnlyRootFilesystem: true              # el sistema de fitxers de la imatge és de només lectura
            capabilities: { drop: [ALL] }             # cap capability de Linux (Node no en necessita cap al 3002 > 1024)
          volumeMounts:
            - { name: tmp, mountPath: /tmp }          # l'únic que es pot escriure: temporal, buit a cada arrencada
          resources:                                  # també és seguretat: un contenidor sense límit pot esgotar el node (05-02, 06-04)
            requests: { cpu: 100m, memory: 128Mi }
            limits:   { cpu: 500m, memory: 256Mi }
      volumes:
        - { name: tmp, emptyDir: { sizeLimit: 64Mi } }

Què aporta cada línia nova: seccompProfile: RuntimeDefault activa el perfil seccomp del runtime (containerd) que bloqueja desenes de syscalls que cap aplicació normal no fa servir i que són la via de moltes fugues del contenidor; capabilities: drop: [ALL] treu fins i tot les capabilities que Docker concedeix per defecte (NET_RAW, CHOWN...): si un atacant executa codi a dins, no pot obrir sockets raw ni canviar propietaris; emptyDir per a /tmp fa compatible readOnlyRootFilesystem amb llibreries que escriuen temporals (amb sizeLimit perquè no ompli el node); fsGroup permet llegir secrets muntats com a fitxers sense ser root.

Pod Security Admission (PSA) fa que el clúster rebutgi pods que no compleixin un perfil, en comptes de confiar que cada Deployment ho escrigui bé. Tres perfils: privileged (tot), baseline (sense el pitjor: privilegiat, hostPath, hostNetwork), restricted (tot l'anterior més runAsNonRoot, drop ALL, seccomp, sense escalada). S'activa amb etiquetes al namespace:

# k8s/namespace.yaml (amplia el de 05-02)
apiVersion: v1
kind: Namespace
metadata:
  name: techcorp
  labels:
    pod-security.kubernetes.io/enforce: restricted        # rebutja pods que no compleixin
    pod-security.kubernetes.io/enforce-version: v1.30
    pod-security.kubernetes.io/warn: restricted            # avisa en fer kubectl apply
    pod-security.kubernetes.io/audit: restricted           # ho anota a l'audit log (apartat 8)
kubectl label ns techcorp pod-security.kubernetes.io/warn=restricted --overwrite   # primer només warn: veure què es trencaria
kubectl apply -k k8s/servei-comandes/overlays/dev                                 # "Warning: would violate PodSecurity restricted: ..." si falta alguna cosa

El Job de migracions de 05-02, el gateway, RabbitMQ i Keycloak també l'han de complir (o viure en un altre namespace amb baseline justificat). Per a regles més fines que les de PSA (exigir digest, prohibir latest, exigir resources), es fa servir un motor de polítiques com Kyverno o OPA Gatekeeper; esment.

  1. Secrets: Kubernetes Secrets, External Secrets Operator i rotació

Un Secret de Kubernetes (05-02: comandes-db, comandes-rabbitmq, i des de 07-01 comandes-oidc; pagaments-passarella a 07-02) és base64, no xifrat: kubectl get secret comandes-db -o jsonpath='{.data.COMANDES_DB_URL}' | base64 -d el mostra a qualsevol amb permís de lectura. Tres capes perquè sigui segur:

  1. Xifratge en repòs a etcd (EncryptionConfiguration de l'API server amb aescbc/kms): responsabilitat de Plataforma o del proveïdor gestionat (actiu per defecte als principals). Sense això, una còpia d'etcd és una còpia de tots els secrets.
  2. RBAC de lectura (apartat 7): només els ServiceAccount dels pods que els munten i unes poques persones de Plataforma poden fer get de secrets; els desenvolupadors de Comandes, no. I el manifest del Secret mai no és a techcorp/plataforma (GitOps és públic dins de l'empresa).
  3. Font de veritat externa amb External Secrets Operator (ESO): el secret viu en un gestor (HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager...), i ESO crea i actualitza el Secret de Kubernetes a partir d'ell. Així git només conté la referència:
# k8s/servei-comandes/base/externalsecret-db.yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: comandes-db
  namespace: techcorp
spec:
  refreshInterval: 1h                                  # ESO rellegeix el gestor cada hora: si ha rotat, actualitza el Secret
  secretStoreRef: { name: techcorp-vault, kind: ClusterSecretStore }   # com parlar amb el gestor (Vault amb auth de Kubernetes)
  target:
    name: comandes-db                                  # el Secret que espera el Deployment de 05-02: no hi canvia res
    creationPolicy: Owner
  data:
    - secretKey: COMANDES_DB_URL                       # clau al Secret de Kubernetes
      remoteRef: { key: techcorp/prod/comandes/db, property: url }   # ruta al gestor
kubectl get externalsecret comandes-db -n techcorp     # STATUS SecretSynced, READY True
kubectl get secret comandes-db -n techcorp             # creat per ESO; el manifest a git no conté el valor

Rotació de credencials. Una contrasenya de base de dades viu anys perquè rotar-la fa por; amb aquesta arquitectura, el procediment és rutinari (i s'executa com a mínim semestralment i sempre després d'una fuita, 07-03):

Pas comandes-db (PostgreSQL) comandes-rabbitmq
1 Crear la nova contrasenya de svc_comandes al gestor (techcorp/prod/comandes/db); PostgreSQL permet tenir-ne dues de vàlides creant un usuari svc_comandes_b o canviant en finestra rabbitmqctl change_password comandes <nova> (les connexions obertes continuen; les noves fan servir la nova)
2 ESO actualitza el Secret en ≤ 1 h (o kubectl annotate externalsecret comandes-db force-sync=$(date +%s)) Igual
3 Els pods no rellegeixen variables d'entorn: kubectl rollout restart deploy/servei-comandes (rolling, sense pèrdua, 05-04); amb secrets com a fitxers (apartat 5) i un config.js que els rellegeixi, no caldria reinici Igual; el reconnectar d'amqplib (06-03) fa servir la nova URL després del reinici
4 Revocar la contrasenya antiga; comprovar als logs que no hi ha password authentication failed rabbitmqctl no té "doble contrasenya": la finestra és el rolling restart

Un Reloader (Stakater) o l'anotació d'Argo CD poden automatitzar el pas 3 quan canvia el Secret.

  1. Secrets a CI: GitHub Environments i OIDC

A 05-03 el pipeline tenia GITHUB_TOKEN, PACT_BROKER_TOKEN, PLATAFORMA_TOKEN i KUBECONFIG_STAGING, i va quedar pendent l'esment a OIDC. El que es tanca aquí:

  • Environments de GitHub (staging, production): secrets per entorn, required reviewers per a producció, i deployment branches perquè només main els pugui fer servir. Un workflow d'una branca de funcionalitat no veu KUBECONFIG_STAGING.
  • OIDC en comptes de tokens de llarga durada: amb permissions: id-token: write, el workflow obté un JWT signat per GitHub que diu "soc el workflow X del repo Y a la branca Z", i el núvol o el registre el bescanvien per credencials de minuts. És el mateix client credentials de 07-01, amb GitHub com a emissor. Per al registre (ghcr.io) ja ho fa GITHUB_TOKEN; per a un clúster gestionat al núvol:
# .github/workflows/servei-node-cd.yml (fragment: accés a AWS/EKS sense access keys)
permissions: { id-token: write, contents: read }
steps:
  - uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: arn:aws:iam::123456789012:role/gha-techcorp-staging-deploy   # rol amb permís NOMÉS sobre el clúster de staging
      aws-region: eu-west-1
  - run: aws eks update-kubeconfig --name techcorp-staging && kubectl rollout status deploy/servei-comandes -n techcorp

Amb això KUBECONFIG_STAGING desapareix com a secret (la confiança és "aquest repo, aquesta branca, aquest workflow", configurada a la política del rol del núvol), i a producció no hi ha cap credencial: Argo CD fa pull (05-03). PLATAFORMA_TOKEN (per obrir la PR de promoció) se substitueix per una GitHub App amb permisos acotats al repositori plataforma. PACT_BROKER_TOKEN es manté com a secret d'entorn, rotat per calendari.

Fitxers enfront de variables d'entorn. Les variables d'entorn es veuen a kubectl describe pod (no els valors de secretRef, però sí els d'env: value), a /proc/<pid>/environ i les hereten els processos fills; els fitxers muntats des d'un Secret (volumes: - secret: { secretName: comandes-db } + COMANDES_DB_URL_FILE=/etc/secrets/COMANDES_DB_URL) només els llegeix qui té l'UID/fsGroup adequat, i Kubernetes els actualitza en calent quan el Secret canvia. La convenció <VARIABLE>_FILE de 04-03 ja està implementada: TechCorp migra a fitxers els secrets de Pagaments primer (auditoria) i la resta a mesura que es toqui cada servei.

  1. Xarxa: NetworkPolicy amb deny-all per defecte

Sense NetworkPolicy, qualsevol pod del clúster pot obrir una connexió amb qualsevol altre: un pod compromès a techcorp arriba a servei-inventari:3006 amb X-Usuari-Rols: admin (07-01) o a postgres:5432 per provar contrasenyes. Les NetworkPolicy són un firewall per etiquetes; requereixen un CNI que les apliqui (Calico, Cilium; els gestionats solen portar-lo). Estratègia: denegar-ho tot al namespace i obrir explícitament.

# k8s/xarxa/00-deny-all.yaml — ningú no entra ni surt, llevat del que es permeti després
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: deny-all, namespace: techcorp }
spec:
  podSelector: {}                         # tots els pods del namespace
  policyTypes: [Ingress, Egress]
---
# k8s/xarxa/01-permetre-dns.yaml — tots necessiten resoldre noms (03-05); sense això, res no funciona
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: permetre-dns, namespace: techcorp }
spec:
  podSelector: {}
  policyTypes: [Egress]
  egress:
    - to: [{ namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: kube-system } }, podSelector: { matchLabels: { k8s-app: kube-dns } } }]
      ports: [{ protocol: UDP, port: 53 }, { protocol: TCP, port: 53 }]
---
# k8s/xarxa/gateway.yaml — el gateway és l'ÚNIC que rep de l'ingress controller, i només parla amb els serveis públics
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: gateway, namespace: techcorp }
spec:
  podSelector: { matchLabels: { app: gateway } }
  policyTypes: [Ingress, Egress]
  ingress:
    - from: [{ namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: ingress-nginx } } }]
      ports: [{ port: 8080 }]
  egress:
    - to: [{ podSelector: { matchLabels: { app: servei-cataleg } } }]
      ports: [{ port: 3001 }]
    - to: [{ podSelector: { matchLabels: { app: servei-comandes } } }]
      ports: [{ port: 3002 }]
    - to: [{ podSelector: { matchLabels: { app: servei-clients } } }]
      ports: [{ port: 3004 }]
    - to: [{ podSelector: { matchLabels: { app: bff-mobil } } }]
      ports: [{ port: 3010 }]
    - to: [{ podSelector: { matchLabels: { app: keycloak } } }]        # JWKS (07-01)
      ports: [{ port: 8080 }]
---
# k8s/xarxa/servei-comandes.yaml — rep del gateway (i de Prometheus); parla amb Catàleg, Clients, RabbitMQ, PostgreSQL i Keycloak
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: servei-comandes, namespace: techcorp }
spec:
  podSelector: { matchLabels: { app: servei-comandes } }
  policyTypes: [Ingress, Egress]
  ingress:
    - from: [{ podSelector: { matchLabels: { app: gateway } } }]
      ports: [{ port: 3002 }]
    - from: [{ namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: monitoring } } }]   # /metrics (06-01)
      ports: [{ port: 3002 }]
  egress:
    - to: [{ podSelector: { matchLabels: { app: servei-cataleg } } }]
      ports: [{ port: 3001 }]
    - to: [{ podSelector: { matchLabels: { app: servei-clients } } }]
      ports: [{ port: 3004 }]
    - to: [{ podSelector: { matchLabels: { app: rabbitmq } } }]
      ports: [{ port: 5671 }]                                          # amqps (07-02)
    - to: [{ podSelector: { matchLabels: { app: postgres } } }]
      ports: [{ port: 5432 }]
    - to: [{ podSelector: { matchLabels: { app: keycloak } } }]
      ports: [{ port: 8080 }]                                          # token de servei i JWKS
    - to: [{ namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: observabilitat } } }]   # OTLP a Jaeger (06-02)
      ports: [{ port: 4318 }]
---
# k8s/xarxa/servei-inventari.yaml — tanca el que 07-02 va deixar: a Inventari NOMÉS li parla Comandes
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: servei-inventari, namespace: techcorp }
spec:
  podSelector: { matchLabels: { app: servei-inventari } }
  policyTypes: [Ingress]
  ingress:
    - from: [{ podSelector: { matchLabels: { app: servei-comandes } } }]
      ports: [{ port: 3006 }]
    - from: [{ namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: monitoring } } }]
      ports: [{ port: 3006 }]

I servei-pagaments és l'únic amb egress a Internet (la passarel·la): ipBlock: { cidr: 0.0.0.0/0, except: [10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16] } al port 443, o millor, si el CNI ho suporta (Cilium), una política per FQDN (api.passarella-pagaments.example). La resta de serveis no surten del clúster: un servei-cataleg compromès no pot exfiltrar dades ni descarregar eines.

kubectl apply -f k8s/xarxa/
# Comprovar: des d'un pod qualsevol, Inventari no respon; des de Comandes, sí
kubectl run prova --rm -it --image=curlimages/curl -n techcorp -- curl -m 3 http://servei-inventari:3006/health/live   # timeout
kubectl exec deploy/servei-comandes -n techcorp -- wget -qO- http://servei-inventari:3006/health/live               # {"estat":"ok"}
flowchart LR
    IN[ingress-nginx] -->|8080| GW[gateway]
    GW -->|3001| CA[servei-cataleg]
    GW -->|3002| PE[servei-comandes]
    GW -->|3004| CL[servei-clients]
    PE -->|3001| CA
    PE -->|3004| CL
    PE -->|3006| INV[servei-inventari]
    PE -->|5671 amqps| MQ[(rabbitmq)]
    PE -->|5432| PG[(postgres)]
    PA[servei-pagaments] -->|443| EXT((passarel·la externa))
    PR[prometheus] -.->|/metrics| PE & INV & CA & CL
    X[qualsevol altre pod] -. bloquejat .-x INV

Amb les polítiques, l'advertència de 07-01 sobre X-Usuari-* passa de "qualsevol pod" a "només el gateway i Prometheus poden arribar a Comandes" —i mTLS amb mesh (07-02) hi afegirà, quan arribi, identitat criptogràfica a més de xarxa. Les polítiques viuen a techcorp/plataforma/k8s/xarxa/ i les revisa Plataforma; cada servei nou arriba amb la seva a la plantilla.

  1. RBAC de Kubernetes: ServiceAccount per servei i rols mínims

Tot pod corre amb un ServiceAccount (SA); per defecte, default, amb un token muntat a /var/run/secrets/kubernetes.io/serviceaccount que permet parlar amb l'API server. servei-comandes no necessita parlar amb l'API server, així que: SA propi (identitat clara per a NetworkPolicy en alguns CNI, per a mTLS amb mesh a 07-02, i per a imagePullSecrets) i sense token muntat:

# k8s/servei-comandes/base/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata: { name: servei-comandes, namespace: techcorp }
automountServiceAccountToken: false      # i al Deployment (apartat 3) també; si un dia necessita l'API, un Role mínim i token projectat amb caducitat
imagePullSecrets: [{ name: ghcr-techcorp }]

Qui que parla amb l'API server, amb el mínim:

Identitat Necessita Rol
Workflow de CD a staging (via OIDC, apartat 5) apply de Deployment/Service/ConfigMap/Job i rollout status a techcorp Role desplegador a techcorp (get/list/watch/create/update/patch sobre deployments, services, configmaps, jobs, pods només lectura); RoleBinding al grup/usuari federat. Mai cluster-admin
Argo CD (producció) Aplicar els manifests d'overlays/prod Argo CD porta la seva SA; se li dona un Role per namespace gestionat (techcorp, monitoring), no el ClusterRole admin per defecte de la instal·lació ràpida
Prometheus Descobrir pods i endpoints ClusterRole de només lectura (get/list/watch de pods, services, endpoints)
External Secrets Operator Crear/actualitzar Secret Role a cada namespace amb secrets (create/update/get), no cluster-admin
Persones Desenvolupador de Comandes: llegir pods/logs i fer port-forward a techcorp (view + pods/portforward); Plataforma: admin a techcorp; ningú no fa servir cluster-admin a diari (compte d'emergència auditat) RoleBinding a grups del proveïdor d'identitat (Keycloak/SSO també aquí)
# k8s/rbac/desplegador.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: { name: desplegador, namespace: techcorp }
rules:
  - { apiGroups: ["apps"],  resources: ["deployments"],           verbs: ["get", "list", "watch", "create", "update", "patch"] }
  - { apiGroups: [""],      resources: ["services", "configmaps"], verbs: ["get", "list", "create", "update", "patch"] }
  - { apiGroups: ["batch"], resources: ["jobs"],                  verbs: ["get", "list", "watch", "create", "delete"] }
  - { apiGroups: [""],      resources: ["pods", "pods/log"],      verbs: ["get", "list", "watch"] }
  # ni "secrets" ni "delete deployments": els secrets els posa ESO; esborrar és manual i auditat
kubectl auth can-i list secrets -n techcorp --as=system:serviceaccount:techcorp:servei-comandes    # no
kubectl auth can-i create deployments -n techcorp --as=desplegador-staging                          # yes
kubectl auth can-i '*' '*' --as=desplegador-staging                                                 # no

  1. Auditoria i detecció

  • Audit log de l'API server: registra qui ha fet què contra l'API (kubectl get secret comandes-db per part d'una persona, un create de Role, un pod que no compleix PSA). Es configura amb una Policy d'auditoria (nivell Metadata per a tot, RequestResponse per a secrets, roles i rolebindings) i s'envia a Loki (06-01) amb retenció llarga, separada dels logs d'aplicació (07-03). En clústers gestionats s'activa des del proveïdor.
  • Detecció en temps d'execució: Falco (esment) observa syscalls als nodes i alerta davant de comportaments anòmals —una shell oberta en un contenidor de producció, un procés que llegeix /etc/shadow, una connexió sortint des d'un pod que no hauria de sortir— i s'integra amb Alertmanager (06-05). Amb readOnlyRootFilesystem, drop ALL i NetworkPolicies, la majoria d'aquests comportaments ja fallen; Falco explica l'intent.
  • Higiene: kube-bench (CIS Benchmark) per a la configuració del clúster i kubectl get events -A --field-selector reason=FailedCreate per veure pods rebutjats per PSA.

  1. Cadena de subministrament: lockfile, SBOM i SLSA

La cadena de subministrament és tot el que hi ha entre el codi del Luis i el pod a producció, i cada baula es pot substituir per alguna cosa maliciosa. El que TechCorp fixa: package-lock.json i npm ci (05-01/07-03: exactament el que s'ha provat és el que es desplega); dependències i accions de CI fixades per versió (actions/checkout@v4, millor per SHA per a les crítiques) i la imatge base per digest (node:20-alpine@sha256:…, actualitzada per Renovate); SBOM per imatge amb Syft (syft ghcr.io/techcorp/servei-comandes:1.0.1 -o spdx-json > sbom.json) adjunt a la imatge (cosign attest) per respondre en minuts "quins serveis porten libxml2 2.9.x?"; signatura cosign i verificació en admissió (apartat 2); i SLSA (Supply-chain Levels for Software Artifacts) com a marc de referència: TechCorp és al nivell 2 (build a CI amb procedència signada) i aspira al 3 (runners efímers i aïllats). Només esment: l'essencial és que el pipeline de 05-03 sigui l'únic camí a producció, i que produeixi artefactes verificables.

  1. Llista de comprovació de plataforma per a TechCorp

Estat al tancament del mòdul 7, mantingut a techcorp/plataforma/SEGURETAT.md:

Àrea Comprovació Estat
Imatge Base node:20-alpine per digest, multi-stage, USER node, sense devDependencies Fet (05-01)
Trivy CRITICAL/HIGH bloquejant + escaneig nocturn d'imatges desplegades Fet / Pendent (nocturn)
Signatura cosign + verificació en admissió Pendent (fase 2)
Pod securityContext complet (runAsNonRoot, drop ALL, seccomp, RO rootfs, emptyDir /tmp), límits de recursos Fet a Comandes i Catàleg; pendent a gateway, Notificacions
PSA restricted a techcorp (enforce) warn actiu; enforce pendent fins a migrar RabbitMQ/Keycloak a namespace propi
Secrets Xifratge a etcd (proveïdor), RBAC de lectura, sense manifests de Secret a git Fet
ESO des del gestor extern per a tots els Secret d'aplicació; rotació semestral documentada Fet comandes-db, comandes-rabbitmq, comandes-oidc, pagaments-passarella; resta pendent
Secrets com a fitxers _FILE Pagaments en curs
CI Environments amb revisors; OIDC per a núvol i registre; sense kubeconfig a secrets; GitHub App en comptes de PLATAFORMA_TOKEN Fet / Pendent (GitHub App)
Xarxa Deny-all + DNS + polítiques per servei; només Pagaments amb egress extern Fet a techcorp
RBAC SA per servei sense token; Role desplegador; Argo CD acotat; sense cluster-admin diari Fet / Argo pendent
Auditoria Audit log a Loki amb retenció llarga; Falco Fet / Pendent
Cadena Lockfile, versions fixades, SBOM amb Syft, SLSA 2 Fet llevat de SBOM (en curs)

Errors Comuns i Consells

  • Aplicar deny-all sense la política de DNS: tot deixa de resoldre i sembla que "Kubernetes s'ha trencat". DNS primer, sempre.
  • Oblidar l'scraping de Prometheus, Jaeger o Keycloak a les polítiques: mètriques i traces desapareixen en silenci. Revisa cada to/from amb els diagrames de 06-01/06-02.
  • readOnlyRootFilesystem sense emptyDir a /tmp: l'aplicació falla en arrencar amb EROFS a la primera escriptura d'una dependència.
  • PSA enforce de cop en un namespace amb RabbitMQ, Keycloak o l'ingress controller: pods rebutjats. warn/audit primer, després enforce.
  • Secret al repositori GitOps "perquè està en base64": és text pla. Referència amb ExternalSecret, valor al gestor.
  • Rotar la contrasenya i no reiniciar els pods (variables d'entorn): fallen en reconnectar hores després, de matinada. Rotació = canvi + sincronització + rollout restart (o fitxers en calent).
  • cluster-admin per al pipeline o Argo CD "de moment": el moment dura anys. Role per namespace des del primer dia.
  • Consell: cada restricció nova es prova amb un game day (06-03): intenta fer el que està prohibit (kubectl run amb root, curl a Inventari des d'un altre pod, kubectl get secret com a desenvolupador) i comprova que falla i que queda a l'audit log.

Exercicis

Exercici 1. Escriu la NetworkPolicy de servei-notificacions (port 3005): consumeix de RabbitMQ (amqps), envia correu a un proveïdor extern per HTTPS, exposa /metrics a Prometheus, i ningú del clúster no ha de poder cridar la seva API llevat de Prometheus. Indica el risc que introdueix l'egress extern i com acotar-lo.

Exercici 2. Un desenvolupador de Catàleg demana poder executar kubectl exec als pods de servei-cataleg a staging "per depurar Mongo". Dissenya el Role/RoleBinding mínim, digues què no inclou i proposa una alternativa millor coherent amb aquesta lliçó.

Exercici 3. Recorre el model d'amenaces de l'apartat 1 per a l'escenari "un atacant aconsegueix executar codi dins del pod de servei-cataleg gràcies a una dependència compromesa" i enumera, capa per capa, quines mesures d'aquesta lliçó limiten el dany i què podria fer encara.

Solucions

Solució 1.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: servei-notificacions, namespace: techcorp }
spec:
  podSelector: { matchLabels: { app: servei-notificacions } }
  policyTypes: [Ingress, Egress]
  ingress:
    - from: [{ namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: monitoring } } }]
      ports: [{ port: 3005 }]
  egress:
    - to: [{ podSelector: { matchLabels: { app: rabbitmq } } }]
      ports: [{ port: 5671 }]
    - to: [{ podSelector: { matchLabels: { app: keycloak } } }]        # si valida tokens o demana el seu
      ports: [{ port: 8080 }]
    - to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16] } }]
      ports: [{ port: 443 }]

(El DNS el cobreix permetre-dns.) El risc: l'egress a "qualsevol IP pública al 443" permet exfiltrar dades (correus, noms) si el pod es compromet. Acotar-lo: política per FQDN del proveïdor de correu amb Cilium (toFQDNs: [{ matchName: api.correu.example }]), o un proxy de sortida (egress gateway) amb llista blanca de dominis que a més dona un únic punt d'auditoria; i la minimització de 07-03 (Notificacions no persisteix dades personals) redueix el que hi ha per exfiltrar.

Solució 2. Role depurador-cataleg a techcorp amb pods (get/list), pods/log (get), pods/exec (create) limitat als pods de Catàleg — RBAC no filtra per etiqueta, així que es fa amb resourceNames (fràgil, els noms canvien) o, millor, donant el Role en un namespace techcorp-cataleg si se separa per equip; RoleBinding al grup equip-experiencia-compra de l'SSO. No inclou secrets, exec en altres pods, ni res d'escriptura. Alternatives millors: (1) kubectl debug amb un contenidor efímer (kubectl debug -it pod/servei-cataleg-xxx --image=mongo:7 --target=servei-cataleg) en comptes d'exec al contenidor de l'aplicació, que a més funciona amb distroless; (2) un port-forward a MongoDB (pods/portforward) i el client de Mongo en local amb l'usuari svc_cataleg_ro de només lectura; (3) que el que calgui veure sigui als logs/mètriques (06-01) per no entrar mai a producció. I tot exec/debug queda a l'audit log (apartat 8).

Solució 3. Amb codi executant-se com l'usuari node (uid 1000) al pod de Catàleg: Contenidor: no és root, drop ALL i seccomp bloquegen la majoria de vies d'escapament; readOnlyRootFilesystem impedeix instal·lar eines (només /tmp de 64 MB); resources.limits eviten tombar el node. Secrets: veu les variables d'entorn del procés (MONGO_URL de Catàleg) —amb fitxers _FILE les continuaria veient, és el seu propi secret—; no pot llegir altres Secret (SA sense token, i sense RBAC). Xarxa: només arriba al MongoDB de Catàleg, a Keycloak i al que la seva política permeti; no arriba a Comandes, Inventari, PostgreSQL ni RabbitMQ, i no surt a Internet (sense egress extern): pot llegir/modificar el catàleg (el seu dany real) però no exfiltrar-lo fàcilment ni pivotar. Identitat: sense capçaleres vàlides ni token de servei d'un altre (els X-Usuari-* no li serveixen perquè no arriba a altres serveis; amb mesh, tampoc no tindria certificat). Detecció: Falco alertaria de la shell o del procés estrany; l'audit log no veuria res perquè no toca l'API. Què podria fer encara: canviar preus o descripcions a MongoDB (mitigat per l'auditoria de 07-03 i l'usuari svc_cataleg sense dropDatabase), servir respostes falses a Comandes (mitigat per la validació amb zod de l'ACL a Comandes i els preus congelats a la comanda) i consumir CPU fins al límit. I la lliçó de fons: la dependència compromesa l'hauria d'haver detectat Trivy/npm audit/SBOM abans d'arribar-hi (apartats 2 i 9).

Conclusió

Amb aquesta lliçó la plataforma deixa de ser la baula feble sota la identitat, el canal i el codi. TechCorp té un model d'amenaces per capes (4C); imatges mínimes escanejades amb Trivy (CRITICAL/HIGH bloquejant) i referenciades per digest, amb cosign i verificació en admissió com a pas següent; pods amb securityContext complet (runAsNonRoot, drop ALL, seccompProfile: RuntimeDefault, readOnlyRootFilesystem amb emptyDir a /tmp) i Pod Security Admission restricted a techcorp; secrets xifrats a etcd, restringits per RBAC i proveïts per External Secrets Operator (ExternalSecret comandes-db des del gestor extern) amb un procediment de rotació i rollout restart; CI amb Environments i OIDC en lloc de kubeconfig i tokens de llarga durada, i secrets com a fitxers _FILE on més importa; NetworkPolicy deny-all amb DNS, gateway com a única entrada des de l'Ingress, Comandes cap a Catàleg/Clients/RabbitMQ/PostgreSQL/Keycloak, Inventari només des de Comandes —tancant el que 07-02 va remetre aquí— i només Pagaments amb sortida a Internet; ServiceAccount per servei sense token, Role desplegador i Argo CD acotats, sense cluster-admin; audit log de l'API server i Falco com a detecció; lockfile, versions fixades, SBOM i SLSA com a cadena de subministrament; i una llista de comprovació amb el que s'ha fet i el que queda pendent. El mòdul 7 acaba així on va començar el 6: amb un sistema observable, resilient, escalable, operable i ara també segur per capes —autenticació i autorització amb Keycloak i JWT, canal xifrat i signat, codi validat i auditat, plataforma endurida—, i amb una llista honesta del que queda per a la fase 2 (mTLS amb mesh, cosign en admissió, PSA enforce, ESO per a tots els serveis). El mòdul 8 recull tot el recorregut: com es va executar la migració del monòlit pas a pas, la implementació completa dels serveis, el seu desplegament i operació d'extrem a extrem, i les lliçons apreses per la Marta, el Luis i els quatre equips de TechCorp.

Curs de Microserveis

Mòdul 1: Introducció als Microserveis

Mòdul 2: Disseny de Microserveis

Mòdul 3: Comunicació entre Microserveis

Mòdul 4: Implementació de Microserveis

Mòdul 5: Desplegament i Orquestració

Mòdul 6: Monitoratge i Manteniment

Mòdul 7: Seguretat en Microserveis

Mòdul 8: Casos d'Estudi i Exemples Pràctics

© Copyright 2026. Tots els drets reservats