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
- Model d'amenaces de la plataforma i les 4C
- Imatges segures: base mínima, escaneig, signatura i digests
- Seguretat del pod:
securityContextcomplet i Pod Security Admission - Secrets: Kubernetes Secrets, External Secrets Operator i rotació
- Secrets a CI: GitHub Environments i OIDC
- Xarxa:
NetworkPolicyamb deny-all per defecte - RBAC de Kubernetes:
ServiceAccountper servei i rols mínims - Auditoria i detecció
- Cadena de subministrament: lockfile, SBOM i SLSA
- Llista de comprovació de plataforma per a TechCorp
- 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.
- 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:
alpineredueix 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 ... shdeixa de funcionar, cosa que és un avantatge de seguretat i un cost de depuració —es resol ambkubectl debugi contenidors efímers—). TechCorp mantéalpinea 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.comimagePullPolicyi digest: una etiqueta (1.0.1) es pot reapuntar; un digest (@sha256:…) no. A producció, l'overlay de Kustomize fixanewTagidigest(kustomize edit set image ghcr.io/techcorp/servei-comandes@sha256:9f1c…),imagePullPolicy: IfNotPresentés segur perquè el digest és immutable, i el registreghcr.io/techcorpés privat ambimagePullSecrets(o identitat de núvol) alServiceAccount.latestcontinua prohibit (05-01).
- Seguretat del pod:
securityContext complet i Pod Security Admission
securityContext complet i Pod Security AdmissionEl 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 cosaEl 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.
- 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:
- Xifratge en repòs a etcd (
EncryptionConfigurationde l'API server ambaescbc/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. - RBAC de lectura (apartat 7): només els
ServiceAccountdels pods que els munten i unes poques persones de Plataforma poden fergetde secrets; els desenvolupadors de Comandes, no. I el manifest del Secret mai no és atechcorp/plataforma(GitOps és públic dins de l'empresa). - 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
Secretde 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 gestorkubectl 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 valorRotació 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.
- 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ó, ideployment branchesperquè nomésmainels pugui fer servir. Un workflow d'una branca de funcionalitat no veuKUBECONFIG_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 faGITHUB_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 techcorpAmb 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.
- Xarxa:
NetworkPolicy amb deny-all per defecte
NetworkPolicy amb deny-all per defecteSense 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.
- RBAC de Kubernetes:
ServiceAccount per servei i rols mínims
ServiceAccount per servei i rols mínimsTot 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 sí 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 auditatkubectl 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
- Auditoria i detecció
- Audit log de l'API server: registra qui ha fet què contra l'API (
kubectl get secret comandes-dbper part d'una persona, uncreatedeRole, un pod que no compleix PSA). Es configura amb unaPolicyd'auditoria (nivellMetadataper a tot,RequestResponseper asecrets,rolesirolebindings) 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). AmbreadOnlyRootFilesystem,drop ALLi NetworkPolicies, la majoria d'aquests comportaments ja fallen; Falco explica l'intent. - Higiene:
kube-bench(CIS Benchmark) per a la configuració del clúster ikubectl get events -A --field-selector reason=FailedCreateper veure pods rebutjats per PSA.
- 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.
- 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-allsense 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/fromamb els diagrames de 06-01/06-02. readOnlyRootFilesystemsenseemptyDira/tmp: l'aplicació falla en arrencar ambEROFSa la primera escriptura d'una dependència.- PSA
enforcede cop en un namespace amb RabbitMQ, Keycloak o l'ingress controller: pods rebutjats.warn/auditprimer, desprésenforce. Secretal repositori GitOps "perquè està en base64": és text pla. Referència ambExternalSecret, 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-adminper al pipeline o Argo CD "de moment": el moment dura anys.Roleper namespace des del primer dia.- Consell: cada restricció nova es prova amb un game day (06-03): intenta fer el que està prohibit (
kubectl runambroot,curla Inventari des d'un altre pod,kubectl get secretcom 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
- Conceptes Bàsics de Microserveis
- Avantatges i Desavantatges dels Microserveis
- Comparació amb l'Arquitectura Monolítica
- Quan Adoptar Microserveis: Criteris de Decisió
- El Cas Pràctic del Curs: la Botiga Online de TechCorp
Mòdul 2: Disseny de Microserveis
- Principis de Disseny de Microserveis
- Descomposició d'Aplicacions Monolítiques
- Definició de Bounded Contexts
- Gestió de Dades: una Base de Dades per Servei
- Consistència Distribuïda: Sagues, CQRS i Event Sourcing
Mòdul 3: Comunicació entre Microserveis
- APIs RESTful
- Missatgeria Asíncrona
- Protocols de Comunicació: gRPC, GraphQL
- API Gateway i Backend for Frontend
- Descobriment de Serveis i Balanceig de Càrrega
- Contractes i Versionat d'APIs
Mòdul 4: Implementació de Microserveis
- Elecció de Tecnologies i Eines
- Desenvolupament d'un Microservei Simple
- Gestió de Configuració
- Integració Pràctica: Consumir APIs i Publicar Esdeveniments
- Proves en Microserveis: Unitàries, d'Integració i de Contracte
Mòdul 5: Desplegament i Orquestració
- Contenidors i Docker
- Orquestració amb Kubernetes
- CI/CD per a Microserveis
- Estratègies de Desplegament: Rolling, Blue-Green i Canary
- Service Mesh: Istio i Linkerd
Mòdul 6: Monitoratge i Manteniment
- Monitoratge i Logging
- Traçabilitat Distribuïda amb OpenTelemetry
- Gestió d'Errors i Recuperació
- Escalabilitat i Rendiment
- SLOs, Alertes i Gestió d'Incidents
Mòdul 7: Seguretat en Microserveis
- Autenticació i Autorització
- Seguretat en la Comunicació
- Pràctiques de Seguretat
- Seguretat en Contenidors i Kubernetes
