Tancàvem la lliçó anterior amb una observació incòmoda: hem donat als components de Rutas Norte la seva configuració, les seves credencials i els seus recursos, però no els hem donat identitat. Ara mateix, els pods de botiga-web, api-reserves, postgres-reserves, redis-cache i worker-notificacions estan fent servir tots la mateixa ServiceAccount default del seu namespace, amb un token d'accés a l'API de Kubernetes muntat a dins que cap d'ells no necessita i que és la primera cosa que buscaria un atacant que aconseguís executar codi en un d'aquells contenidors. Aquesta lliçó ho arregla: entendràs què és una ServiceAccount i en què es diferencia d'un usuari, com funcionen els tokens projectats moderns, per què automountServiceAccountToken: false ha de ser la teva opció per defecte, i parlaràs amb l'API des de dins d'un pod amb curl per veure amb els teus ulls tant una resposta autoritzada com un 403 Forbidden ben merescut.

Contingut

  1. La identitat d'una càrrega de treball
  2. Usuaris davant de ServiceAccounts
  3. La ServiceAccount default i per què no s'ha de fer servir
  4. Crear ServiceAccounts dedicades per a Rutas Norte
  5. El token projectat: com ha canviat
  6. Què hi ha a /var/run/secrets/kubernetes.io/serviceaccount/
  7. automountServiceAccountToken: false
  8. Parlar amb l'API des de dins d'un pod
  9. El 403 Forbidden i què significa
  10. imagePullSecrets a la ServiceAccount
  11. La ServiceAccount diu qui ets, RBAC diu què pots fer
  12. Federació d'identitat amb els núvols

  1. La identitat d'una càrrega de treball

Tota petició que arriba al kube-apiserver passa per tres fases que ja coneixes del mòdul 1:

flowchart LR
    A["Peticio"] --> B["AUTENTICACIO<br/>Qui ets?"]
    B --> C["AUTORITZACIO (RBAC)<br/>Pots fer aixo?"]
    C --> D["ADMISSIO<br/>LimitRanger, ResourceQuota..."]
    D --> E["etcd"]
    B -.->|"no identificat"| F["401 Unauthorized"]
    C -.->|"identificat pero sense permis"| G["403 Forbidden"]

Quan tu executes kubectl get pods, la primera fase es resol amb el certificat o el token del teu kubeconfig. Però, i quan qui pregunta és un pod?

Els casos en què un pod necessita parlar amb l'API són més comuns del que sembla:

Qui Per a què
Un controlador d'Ingress Llegir objectes Ingress i Service per configurar el seu encaminament
Prometheus Descobrir quins pods cal consultar (07-03)
cert-manager Crear i actualitzar Secrets de tipus TLS (04-05)
Un operador Reconciliar els seus recursos personalitzats (06-07)
Argo CD Aplicar manifests des de Git (10-05)
Una aplicació teva Llegir un ConfigMap en calent, consultar les seves pròpies rèpliques, coordinar una elecció de líder

I a Rutas Norte hi ha un cas concret que arribarà al mòdul 6: informes-ocupacio, la tasca nocturna, haurà de consultar l'API per saber quantes rèpliques d'api-reserves hi va haver actives cada hora i correlacionar-ho amb l'ocupació. Necessita identitat i necessita permisos.

Però la raó principal per estudiar això no són els pods que que parlen amb l'API: són els cinc que no ho fan i que, tanmateix, porten a sobre una credencial vàlida sense necessitar-la. Això és superfície d'atac gratuïta.

  1. Usuaris davant de ServiceAccounts

Kubernetes distingeix dos tipus d'identitat, i la diferència és de fons:

Usuaris (users) ServiceAccounts
Per a qui Persones: administradors, desenvolupadors Processos: pods, controladors
És un objecte de Kubernetes? No. No existeix kind: User : kind: ServiceAccount
Qui els gestiona Un sistema extern: certificats, OIDC, LDAP, IAM del núvol Kubernetes, amb kubectl create sa
kubectl get Impossible kubectl get serviceaccounts
Àmbit Tot el clúster Amb namespace
Nom a RBAC [email protected] system:serviceaccount:<ns>:<nom>
Com s'autentiquen Certificat de client, token OIDC Token JWT signat pel clúster
Cicle de vida Extern Lligat al namespace

El primer punt sorprèn sempre: Kubernetes no té usuaris. No hi ha cap base de dades d'usuaris, ni cap ordre per crear-ne un. Quan el teu kubeconfig fa servir un certificat de client, Kubernetes llegeix el camp CN (Common Name) del certificat i el pren com a nom d'usuari, confiant que l'autoritat certificadora del clúster només signa certificats legítims. Amb OIDC, delega en el proveïdor d'identitat. La gestió de persones és, deliberadament, un problema de fora.

Les ServiceAccounts, en canvi, són objectes de primera classe:

kubectl get serviceaccounts -n rutas-norte-pro
NAME      SECRETS   AGE
default   0         14d

Només n'hi ha una, la default, i tots els nostres pods la fan servir. Fixa't en SECRETS 0: és el senyal que som a Kubernetes modern, on les ServiceAccounts ja no porten un Secret amb un token permanent associat. Hi tornarem a l'apartat 5.

El nom complet d'una ServiceAccount al sistema d'autorització té aquesta forma:

system:serviceaccount:rutas-norte-pro:api-reserves
     |         |              |             |
   prefix    tipus        namespace        nom

Aquest és l'identificador exacte que faràs servir als RoleBindings de RBAC, i també el que apareix als registres d'auditoria. A més, tota ServiceAccount pertany automàticament al grup system:serviceaccounts i al grup del seu namespace, system:serviceaccounts:rutas-norte-pro, cosa que permet concedir permisos a totes les d'un entorn de cop (una cosa que gairebé mai no és bona idea, però convé saber que existeix).

  1. La ServiceAccount default i per què no s'ha de fer servir

Cada namespace obté una ServiceAccount anomenada default en el moment de crear-se. Si un pod no n'especifica cap, se li assigna aquesta.

kubectl get pod api-reserves-7f4b8c9d6-2xkqp -n rutas-norte-pro \
  -o jsonpath='{.spec.serviceAccountName}'; echo
default

Cinc raons per no fer-la servir:

Raó 1: és compartida. Tots els pods del namespace tenen la mateixa identitat. Si demà informes-ocupacio necessita permís per llegir Deployments i l'hi concedeixes a default, l'estàs concedint també a botiga-web, a redis-cache i a postgres-reserves. És el contrari del mínim privilegi.

Raó 2: impedeix la traçabilitat. Al registre d'auditoria totes les peticions apareixen com a system:serviceaccount:rutas-norte-pro:default. Davant d'un accés sospitós, no hi ha manera de saber quin component el va fer.

Raó 3: impedeix revocar de manera selectiva. Si worker-notificacions es veu compromès, vols retirar-li els permisos immediatament. Amb la default, retirar-los-hi significa retirar-los a tot el namespace.

Raó 4: munta un token que gairebé ningú no necessita. Per defecte, automountServiceAccountToken és true, així que tots els nostres pods porten a dins un token vàlid de l'API. Comprova-ho:

kubectl exec -n rutas-norte-pro deploy/botiga-web -- ls /var/run/secrets/kubernetes.io/serviceaccount/
ca.crt
namespace
token

Un servidor nginx que només serveix fitxers estàtics hi té una credencial d'accés a l'API del clúster. Si algú troba una vulnerabilitat de lectura arbitrària de fitxers en aquella aplicació (una cosa gens exòtica), la primera cosa que farà és llegir aquell token.

Raó 5: la temptació de donar-li permisos. És tan còmoda que, en un destret, algú acaba enllaçant la default a un ClusterRole potent perquè "funcioni ja". I això no es reverteix mai.

La regla és simple:

Una ServiceAccount per component, i automountServiceAccountToken: false a totes llevat de les que de debò parlin amb l'API.

  1. Crear ServiceAccounts dedicades per a Rutas Norte

Comencem per l'objecte. És dels més simples de Kubernetes:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: api-reserves
  namespace: rutas-norte-pro
  labels:
    app: api-reserves
    app.kubernetes.io/name: api-reserves
    app.kubernetes.io/part-of: rutas-norte
    entorn: pro
automountServiceAccountToken: false     # api-reserves NO parla amb l'API

O de manera imperativa, per generar el YAML:

kubectl create serviceaccount api-reserves -n rutas-norte-pro --dry-run=client -o yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  creationTimestamp: null
  name: api-reserves
  namespace: rutas-norte-pro

El pla complet per a Rutas Norte, amb la decisió raonada per a cada component:

Component ServiceAccount Parla amb l'API? automountServiceAccountToken
botiga-web botiga-web No false
api-reserves api-reserves No false
postgres-reserves postgres-reserves No false
redis-cache redis-cache No false
worker-notificacions worker-notificacions No false
informes-ocupacio informes-ocupacio (mòdul 6) true

Cinc de sis no necessiten token. Aquest és el resultat normal: la immensa majoria de les aplicacions de negoci no parlen amb l'API de Kubernetes. I tot i així, la configuració per defecte el munta a totes.

I per què crear una ServiceAccount per als que no la fan servir, si no parlaran amb l'API? Per tres motius:

  1. Traçabilitat: si algun dia un d'ells fa una petició, sabràs quin.
  2. imagePullSecrets: la ServiceAccount és el lloc correcte per declarar-los (apartat 10).
  3. Preparació: quan demà api-reserves necessiti un permís concret, la identitat ja existeix i no cal tocar el Deployment.

Els sis objectes, a k8s/base/serviceaccounts.yaml:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: botiga-web
  namespace: rutas-norte-pro
  labels: {app: botiga-web, app.kubernetes.io/part-of: rutas-norte, entorn: pro}
automountServiceAccountToken: false
imagePullSecrets:
  - name: registry-rutasnorte
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: api-reserves
  namespace: rutas-norte-pro
  labels: {app: api-reserves, app.kubernetes.io/part-of: rutas-norte, entorn: pro}
automountServiceAccountToken: false
imagePullSecrets:
  - name: registry-rutasnorte
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: postgres-reserves
  namespace: rutas-norte-pro
  labels: {app: postgres-reserves, app.kubernetes.io/part-of: rutas-norte, entorn: pro}
automountServiceAccountToken: false
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: redis-cache
  namespace: rutas-norte-pro
  labels: {app: redis-cache, app.kubernetes.io/part-of: rutas-norte, entorn: pro}
automountServiceAccountToken: false
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: worker-notificacions
  namespace: rutas-norte-pro
  labels: {app: worker-notificacions, app.kubernetes.io/part-of: rutas-norte, entorn: pro}
automountServiceAccountToken: false
imagePullSecrets:
  - name: registry-rutasnorte
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: informes-ocupacio
  namespace: rutas-norte-pro
  labels: {app: informes-ocupacio, app.kubernetes.io/part-of: rutas-norte, entorn: pro}
# Aquesta SI que necessita el token: consultara l'API al modul 6.
automountServiceAccountToken: true
imagePullSecrets:
  - name: registry-rutasnorte

I l'assignació al pod, amb el camp serviceAccountName:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-reserves
  namespace: rutas-norte-pro
spec:
  replicas: 4
  selector:
    matchLabels:
      app: api-reserves
      entorn: pro
  template:
    metadata:
      labels:
        app: api-reserves
        app.kubernetes.io/name: api-reserves
        app.kubernetes.io/part-of: rutas-norte
        entorn: pro
    spec:
      serviceAccountName: api-reserves       # <-- la identitat del pod
      automountServiceAccountToken: false    # cinturo i tirants
      containers:
        - name: api
          image: registry.rutasnorte.example/api-reserves:2.5.0

Un detall històric que veuràs en manifests antics: existeix també un camp serviceAccount (sense Name). Està obsolet des de fa moltes versions i només es conserva per compatibilitat. Fes servir sempre serviceAccountName.

Apliquem i verifiquem:

kubectl apply -f k8s/base/serviceaccounts.yaml
kubectl rollout restart deploy -n rutas-norte-pro
kubectl get pods -n rutas-norte-pro \
  -o custom-columns='POD:.metadata.name,SA:.spec.serviceAccountName'
serviceaccount/botiga-web created
serviceaccount/api-reserves created
serviceaccount/postgres-reserves created
serviceaccount/redis-cache created
serviceaccount/worker-notificacions created
serviceaccount/informes-ocupacio created

POD                                     SA
api-reserves-8b5c7d9e2-4mkqp            api-reserves
api-reserves-8b5c7d9e2-9wnzr            api-reserves
postgres-reserves-6e9g7c0d5-l3nqx       postgres-reserves
redis-cache-7d0e9g8c6-wo5ry             redis-cache
botiga-web-6c8d0g5e9-i8qol              botiga-web
worker-notificacions-7g8e0d9c5-mn4qu    worker-notificacions

Cada component amb la seva identitat. I el token ha desaparegut d'on no calia:

kubectl exec -n rutas-norte-pro deploy/botiga-web -- ls /var/run/secrets/kubernetes.io/serviceaccount/
ls: /var/run/secrets/kubernetes.io/serviceaccount/: No such file or directory
command terminated with exit code 1

Aquest error és exactament el resultat desitjat. No hi ha credencial per robar.

  1. El token projectat: com ha canviat

Aquesta part té història i és important conèixer-la, perquè trobaràs manifests i tutorials de l'època anterior.

Abans de Kubernetes 1.24: tokens permanents en Secrets

En crear una ServiceAccount, un controlador generava automàticament un Secret de tipus kubernetes.io/service-account-token amb un JWT a dins. Aquell token:

  • No caducava mai.
  • No estava lligat a cap pod: qui el copiés el podia fer servir des de qualsevol lloc, fins i tot des de fora del clúster.
  • Es desava a etcd, així que era a les còpies de seguretat.
  • No es podia revocar sense esborrar el Secret i la ServiceAccount.

Era, a la pràctica, una contrasenya eterna a cada namespace. Si un token es filtrava en un log, en un bolcat o en un repositori, continuava sent vàlid mesos després.

Des d'1.24, i obligatori des d'1.25: l'API TokenRequest

El mecanisme actual és completament diferent i molt millor:

sequenceDiagram
    participant K as kubelet
    participant A as apiserver (TokenRequest)
    participant P as Pod

    K->>A: TokenRequest per a la SA api-reserves,<br/>audiencia i vida limitades,<br/>lligat a AQUEST pod
    A-->>K: JWT signat (caduca en 1 hora)
    K->>P: l'escriu en tmpfs<br/>/var/run/secrets/.../token
    Note over K,P: Als 48 minuts (80% de la vida)
    K->>A: renovacio
    A-->>K: JWT nou
    K->>P: substitueix el fitxer
    Note over P: L'aplicacio ha de RELLEGIR el fitxer

Les propietats del token modern:

Propietat Token antic (Secret) Token projectat (TokenRequest)
Caducitat Mai 1 hora per defecte
Rotació Manual Automàtica, pel kubelet al 80 % de la vida
Lligat al pod No : conté l'UID del pod
En esborrar el pod Continua sent vàlid S'invalida
Audiència (aud) Genèrica Restringible a un destinatari
Desat a etcd No
On viu Secret + volum Només al tmpfs del pod

Vegem el contingut real d'un token. Dins d'un pod que sí que el tingui muntat:

kubectl exec -n rutas-norte-pro deploy/informes-ocupacio -- sh -c \
  'cut -d. -f2 /var/run/secrets/kubernetes.io/serviceaccount/token | base64 -d 2>/dev/null'
{
  "aud": ["https://kubernetes.default.svc.cluster.local"],
  "exp": 1785703921,
  "iat": 1785700321,
  "iss": "https://kubernetes.default.svc.cluster.local",
  "jti": "b41e9c02-7f3a-4d18-9a6e-2c85f0d31447",
  "kubernetes.io": {
    "namespace": "rutas-norte-pro",
    "node": {"name": "rutas-norte-m02", "uid": "d3a1..."},
    "pod": {"name": "informes-ocupacio-28912440-x7mzq", "uid": "7f2c..."},
    "serviceaccount": {"name": "informes-ocupacio", "uid": "1e8b..."}
  },
  "nbf": 1785700321,
  "sub": "system:serviceaccount:rutas-norte-pro:informes-ocupacio"
}

És un JWT estàndard. El rellevant:

  • sub és l'identificador que farà servir RBAC.
  • exp - iat = 3600 segons: una hora de vida.
  • kubernetes.io.pod lliga el token a un pod concret. Si aquell pod s'esborra, l'apiserver rebutja el token encara que no hagi caducat.
  • aud limita per a qui és vàlid.

Un avís pràctic important: si la teva aplicació llegeix el token una vegada en arrencar i el desa a memòria, deixarà de funcionar en una hora. Les biblioteques client oficials (client-go, kubernetes de Python, etc.) rellegeixen el fitxer automàticament. Si parles amb l'API amb curl o amb una biblioteca HTTP genèrica, has de rellegir el fitxer a cada petició. És una font d'errors 401 desconcertants que apareixen exactament una hora després del desplegament.

Es pot personalitzar la projecció amb un volum explícit:

      volumes:
        - name: token-api
          projected:
            sources:
              - serviceAccountToken:
                  path: token
                  expirationSeconds: 3600          # minim 600
                  audience: api.rutasnorte.example  # per a un destinatari concret

El camp audience és especialment útil per a un patró avançat: emetre un token que només serveixi davant d'un servei extern concret, de manera que si es filtra no valgui per parlar amb l'API de Kubernetes.

I si alguna vegada necessites un token permanent (integrar una eina externa que no pot rotar), cal crear-lo explícitament:

apiVersion: v1
kind: Secret
metadata:
  name: informes-ocupacio-token-permanent
  namespace: rutas-norte-pro
  annotations:
    kubernetes.io/service-account.name: informes-ocupacio
type: kubernetes.io/service-account-token

Evita-ho sempre que puguis. Torna a introduir tots els problemes del model antic. Per a casos puntuals, l'alternativa correcta és demanar un token de vida limitada:

kubectl create token informes-ocupacio -n rutas-norte-pro --duration=30m
eyJhbGciOiJSUzI1NiIsImtpZCI6Ilp3...

  1. Què hi ha a /var/run/secrets/kubernetes.io/serviceaccount/

Quan el token es munta, el directori conté tres fitxers:

kubectl exec -n rutas-norte-pro deploy/informes-ocupacio -- sh -c \
  'ls -la /var/run/secrets/kubernetes.io/serviceaccount/ && mount | grep serviceaccount'
total 0
drwxrwxrwt 3 root root 140 Aug  5 22:03 .
drwxr-xr-x 3 root root  60 Aug  5 22:03 ..
lrwxrwxrwx 1 root root  13 Aug  5 22:03 ca.crt -> ..data/ca.crt
lrwxrwxrwx 1 root root  16 Aug  5 22:03 namespace -> ..data/namespace
lrwxrwxrwx 1 root root  12 Aug  5 22:03 token -> ..data/token

tmpfs on /var/run/secrets/kubernetes.io/serviceaccount type tmpfs (ro,relatime)

Reconeixes dues coses de lliçons anteriors: la cadena d'enllaços a ..data que permet l'actualització atòmica (aquí és el que fa possible la rotació del token) i el tmpfs, és a dir, memòria RAM.

Fitxer Contingut Per a què serveix
token El JWT signat Capçalera Authorization: Bearer <token>
ca.crt Certificat de la CA del clúster Verificar el certificat TLS de l'apiserver
namespace Nom del namespace, en text pla Que l'aplicació sàpiga on viu sense la Downward API
kubectl exec -n rutas-norte-pro deploy/informes-ocupacio -- sh -c \
  'cat /var/run/secrets/kubernetes.io/serviceaccount/namespace; echo; \
   head -1 /var/run/secrets/kubernetes.io/serviceaccount/ca.crt'
rutas-norte-pro
-----BEGIN CERTIFICATE-----

El ca.crt és la peça que se sol oblidar. Sense ell, un client que parli amb https://kubernetes.default.svc no pot validar el certificat de l'apiserver i cal recórrer a --insecure, que és exactament el que no s'ha de fer: sense validació, un atacant amb accés a la xarxa del pod podria suplantar l'apiserver i capturar el token.

Juntament amb les variables KUBERNETES_SERVICE_HOST i KUBERNETES_SERVICE_PORT que Kubernetes injecta a tots els pods (les vas veure a la lliçó de variables d'entorn), aquests tres fitxers són tot el que cal per parlar amb l'API. És precisament el que les biblioteques client anomenen configuració dins del clúster (InClusterConfig).

  1. automountServiceAccountToken: false

Aquest camp es pot posar en dos llocs, i la precedència importa:

# A la ServiceAccount: afecta TOTS els pods que la facin servir
apiVersion: v1
kind: ServiceAccount
metadata:
  name: api-reserves
  namespace: rutas-norte-pro
automountServiceAccountToken: false
# Al pod: afecta nomes AQUELL pod i GUANYA sobre la ServiceAccount
spec:
  serviceAccountName: api-reserves
  automountServiceAccountToken: false
ServiceAccount Pod Resultat
(sense especificar) (sense especificar) Es munta (el valor per defecte)
false (sense especificar) No es munta
true (sense especificar) Es munta
false true Es munta: el pod guanya
true false No es munta: el pod guanya

El pod sempre té l'última paraula. Això permet el patró recomanat: posar false a la ServiceAccount com a xarxa de seguretat, i true explícit només al pod concret que de debò ho necessita.

Per què això importa tant, en termes de seguretat: si un atacant aconsegueix executar codi en un contenidor —a través d'una vulnerabilitat de l'aplicació, una dependència compromesa o una injecció—, la primera cosa que fa un guió d'explotació automatitzat és llegir aquell token i provar quins permisos té. És un pas estàndard en qualsevol eina d'escalada de privilegis a Kubernetes.

Comparem la superfície d'atac:

Amb token muntat Sense token muntat
Pot llegir el token No existeix
Pot consultar l'API Sí, amb els permisos de la SA No, ni tan sols s'identifica
Pot enumerar el clúster Depèn de RBAC No
Si RBAC està mal configurat Escalada de privilegis Res a escalar

Sense token, fins i tot una configuració de RBAC descurada queda neutralitzada per a aquell pod: no hi ha credencial amb què exercir-la.

Comprovació de l'estat a tot el clúster, que és una auditoria que val la pena fer:

kubectl get pods -A -o json | jq -r '
  .items[] |
  select(.spec.automountServiceAccountToken != false) |
  "\(.metadata.namespace)/\(.metadata.name) -> \(.spec.serviceAccountName)"' | head -10
kube-system/coredns-7db6d8ff4d-2vqxn -> coredns
kube-system/kube-proxy-8xvmk -> kube-proxy
rutas-norte-pro/informes-ocupacio-28912440-x7mzq -> informes-ocupacio

Els del sistema ho necessiten legítimament. De Rutas Norte, només informes-ocupacio. Aquest és l'objectiu.

Un apunt d'operació: si afegeixes automountServiceAccountToken: false a una ServiceAccount, els pods que ja corren conserven el seu token. El muntatge es decideix en crear el pod. Cal recrear-los:

kubectl rollout restart deploy -n rutas-norte-pro

  1. Parlar amb l'API des de dins d'un pod

Ho farem a mà per entendre què fan per dins les biblioteques client. Farem servir informes-ocupacio, l'únic component de Rutas Norte que sí que necessita parlar amb l'API.

Pas 1: un pod amb la ServiceAccount i el token muntat.

apiVersion: v1
kind: Pod
metadata:
  name: consultor-api
  namespace: rutas-norte-pro
  labels:
    app: informes-ocupacio
    app.kubernetes.io/part-of: rutas-norte
    entorn: pro
spec:
  serviceAccountName: informes-ocupacio
  automountServiceAccountToken: true
  restartPolicy: Never
  containers:
    - name: consultor
      image: curlimages/curl:8.10.1
      command: ["sleep", "3600"]
      resources:
        requests:
          cpu: 50m
          memory: 64Mi
        limits:
          cpu: 100m
          memory: 128Mi
kubectl apply -f consultor-api.yaml
kubectl exec -it consultor-api -n rutas-norte-pro -- sh

Pas 2: preparar les variables dins del pod.

# Ja dins del contenidor
SA=/var/run/secrets/kubernetes.io/serviceaccount
TOKEN=$(cat $SA/token)
NS=$(cat $SA/namespace)
APISERVER=https://kubernetes.default.svc

echo "Namespace: $NS"
echo "API:       $APISERVER"
echo "Token:     ${TOKEN:0:40}..."
Namespace: rutas-norte-pro
API:       https://kubernetes.default.svc
Token:     eyJhbGciOiJSUzI1NiIsImtpZCI6Ilp3RTFvSm...

kubernetes.default.svc és el Service de l'apiserver, que existeix al namespace default de tot clúster. Qualsevol pod el pot resoldre per DNS, com vas veure al mòdul 2.

Pas 3: la primera petició, a l'endpoint de versió (que no requereix permisos).

curl -s --cacert $SA/ca.crt \
  -H "Authorization: Bearer $TOKEN" \
  $APISERVER/version
{
  "major": "1",
  "minor": "30",
  "gitVersion": "v1.30.4",
  "goVersion": "go1.22.5",
  "platform": "linux/amd64"
}

Ha funcionat. Els tres elements de la petició:

  1. --cacert $SA/ca.crt: verifica que el servidor és realment l'apiserver del clúster. No facis servir mai -k ni --insecure.
  2. Authorization: Bearer $TOKEN: l'autenticació. És el mecanisme estàndard de JWT.
  3. La URL del Service intern, resolta per DNS.

Pas 4: preguntar qui sóc. Aquest endpoint és utilíssim per depurar:

curl -s --cacert $SA/ca.crt \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -X POST $APISERVER/apis/authentication.k8s.io/v1/selfsubjectreviews \
  -d '{"apiVersion":"authentication.k8s.io/v1","kind":"SelfSubjectReview"}'
{
  "kind": "SelfSubjectReview",
  "apiVersion": "authentication.k8s.io/v1",
  "status": {
    "userInfo": {
      "username": "system:serviceaccount:rutas-norte-pro:informes-ocupacio",
      "uid": "1e8b4c73-9d02-4a5f-8e31-7b6c0f294a15",
      "groups": [
        "system:serviceaccounts",
        "system:serviceaccounts:rutas-norte-pro",
        "system:authenticated"
      ]
    }
  }
}

Aquí hi ha la identitat completa: el username amb el format que anticipàvem i els tres grups automàtics. Des de fora, l'equivalent és:

kubectl auth whoami --as=system:serviceaccount:rutas-norte-pro:informes-ocupacio

Pas 5: intentar alguna cosa que requereix permisos.

curl -s -o /dev/null -w "%{http_code}\n" --cacert $SA/ca.crt \
  -H "Authorization: Bearer $TOKEN" \
  $APISERVER/api/v1/namespaces/$NS/pods
403

I aquí arriba la part instructiva.

  1. El 403 Forbidden i què significa

Vegem el cos complet d'aquella resposta:

curl -s --cacert $SA/ca.crt \
  -H "Authorization: Bearer $TOKEN" \
  $APISERVER/api/v1/namespaces/$NS/pods | head -20
{
  "kind": "Status",
  "apiVersion": "v1",
  "metadata": {},
  "status": "Failure",
  "message": "pods is forbidden: User \"system:serviceaccount:rutas-norte-pro:informes-ocupacio\"
              cannot list resource \"pods\" in API group \"\" in the namespace \"rutas-norte-pro\"",
  "reason": "Forbidden",
  "details": {"kind": "pods"},
  "code": 403
}

Aquest missatge és una petita lliçó en si mateix. Descomponguem-lo:

Part Significat
User "system:serviceaccount:..." L'autenticació va funcionar: l'apiserver sap qui ets
cannot list El verb denegat
resource "pods" El recurs
in API group "" El grup d'API: "" és el grup core
in the namespace "..." L'àmbit
"code": 403 Prohibit, no "no autenticat"

La distinció entre 401 i 403 és fonamental i cal tenir-la clara:

Codi Significat Causa típica
401 Unauthorized "No sé qui ets" Token absent, caducat, malformat o signat per un altre clúster
403 Forbidden "Sé qui ets, però no pots" Falta un Role o un RoleBinding

Si obtens un 401, el problema és al token: revisa que el llegeixis bé, que no hagi caducat (recorda l'hora de vida) i que el pod tingui el volum muntat. Si obtens un 403, el token és correcte i el que falten són permisos.

I aquell 403 és exactament el que ha de passar ara mateix. La nostra ServiceAccount informes-ocupacio no té cap permís concedit, perquè concedir permisos és feina de RBAC, i RBAC és el tema de la lliçó 08-01. Una ServiceAccount acabada de crear, sense RoleBindings, no pot fer absolutament res més enllà dels pocs endpoints públics com /version.

Això mereix subratllar-se perquè és el disseny correcte:

Kubernetes denega per defecte. Crear una identitat no concedeix cap permís. Tot el que una ServiceAccount pugui fer ha d'estar concedit explícitament.

Podem comprovar els permisos sense fer la petició, amb kubectl auth can-i:

kubectl auth can-i list pods -n rutas-norte-pro \
  --as=system:serviceaccount:rutas-norte-pro:informes-ocupacio
kubectl auth can-i get deployments -n rutas-norte-pro \
  --as=system:serviceaccount:rutas-norte-pro:informes-ocupacio
no
no

I la llista completa del que sí que pot:

kubectl auth can-i --list -n rutas-norte-pro \
  --as=system:serviceaccount:rutas-norte-pro:informes-ocupacio
Resources                                       Non-Resource URLs   Verbs
selfsubjectreviews.authentication.k8s.io        []                  [create]
selfsubjectaccessreviews.authorization.k8s.io   []                  [create]
selfsubjectrulesreviews.authorization.k8s.io    []                  [create]
                                                [/healthz]          [get]
                                                [/version]          [get]

Només el mínim: preguntar qui és, preguntar què pot fer, i consultar la salut i la versió. Res de llegir pods, ni deployments, ni moltíssim menys els Secrets amb la contrasenya de postgres-reserves.

Com a avançament del mòdul 8, així es veuria la concessió mínima que informes-ocupacio necessitarà, perquè sàpigues cap on anem:

# AVANCAMENT de 08-01: no ho apliquis encara, s'explica alla
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: lector-deployments
  namespace: rutas-norte-pro
rules:
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "list"]          # nomes lectura, nomes deployments
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: informes-ocupacio-lector
  namespace: rutas-norte-pro
subjects:
  - kind: ServiceAccount
    name: informes-ocupacio
    namespace: rutas-norte-pro
roleRef:
  kind: Role
  name: lector-deployments
  apiGroup: rbac.authorization.k8s.io

Fixa't en el subjects: aquí és on la ServiceAccount que hem creat avui es connecta amb els permisos. Les dues peces encaixen, però són objectes diferents i responsabilitats diferents.

Netegem el pod de proves:

kubectl delete pod consultor-api -n rutas-norte-pro

  1. imagePullSecrets a la ServiceAccount

Recuperem un cap solt de la lliçó de Secrets. Allà vam declarar imagePullSecrets a cada pod per poder descarregar imatges de registry.rutasnorte.example:

    spec:
      imagePullSecrets:
        - name: registry-rutasnorte      # cal repetir-ho a CADA pod

Repetir-ho a cada Deployment, cada Job i cada CronJob és tediós i, sobretot, fàcil d'oblidar: la fallada apareix setmanes després, quan algú afegeix un component nou i es troba amb un ImagePullBackOff incomprensible.

La ServiceAccount és el lloc correcte:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: api-reserves
  namespace: rutas-norte-pro
automountServiceAccountToken: false
imagePullSecrets:
  - name: registry-rutasnorte

I el Deployment queda net:

    spec:
      serviceAccountName: api-reserves    # hereta l'imagePullSecrets
      containers:
        - name: api
          image: registry.rutasnorte.example/api-reserves:2.5.0

Com funciona: quan es crea un pod, un controlador d'admissió copia els imagePullSecrets de la ServiceAccount al spec del pod. Ho pots veure:

kubectl get pod api-reserves-8b5c7d9e2-4mkqp -n rutas-norte-pro \
  -o jsonpath='{.spec.imagePullSecrets}'; echo
[{"name":"registry-rutasnorte"}]

El manifest no ho tenia i l'objecte sí. És el mateix fenomen que amb el LimitRanger de la lliçó anterior: un controlador d'admissió ha mutat l'objecte.

Tres detalls:

  1. Se sumen, no se substitueixen. Si el pod declara els seus, es combinen amb els de la ServiceAccount.
  2. El Secret ha d'existir al mateix namespace. Cal crear-lo a cadascun dels tres entorns.
  3. No és retroactiu. Els pods ja creats no l'hereten; cal recrear-los.

I un truc molt comú per no repetir-ho per component: afegir-lo a la ServiceAccount default de cada namespace, de manera que qualsevol pod que no n'especifiqui una altra l'hereti.

kubectl patch serviceaccount default -n rutas-norte-pro \
  -p '{"imagePullSecrets":[{"name":"registry-rutasnorte"}]}'
serviceaccount/default patched

És útil, però recorda que no eximeix de crear ServiceAccounts dedicades: imagePullSecrets és l'única cosa raonable que ha de portar la default.

  1. La ServiceAccount diu qui ets, RBAC diu què pots fer

És la frase que resumeix la lliçó i convé gravar-la:

La ServiceAccount és la identitat; RBAC són els permisos. Són dues coses independents i totes dues fan falta.

flowchart LR
    SA["ServiceAccount<br/>informes-ocupacio<br/>QUI ets"] --> T["Token JWT<br/>muntat al pod"]
    T --> AU["Autenticacio<br/>l'apiserver t'identifica"]
    AU --> RB["RoleBinding<br/>connecta identitat i permisos"]
    RB --> R["Role<br/>QUE pots fer"]
    R --> OK["Peticio permesa"]
    AU -.->|"sense RoleBinding"| NO["403 Forbidden"]

Les combinacions possibles i el que signifiquen:

ServiceAccount RBAC Resultat
Dedicada Sense permisos S'identifica, no pot fer res. L'estat actual de Rutas Norte
Dedicada Permisos mínims L'objectiu: cada component fa exactament el seu
default compartida Permisos amplis L'antipatró: tot el namespace hereta tot
Dedicada cluster-admin Identitat perfecta, permisos catastròfics
Sense token muntat Qualsevol No pot parlar amb l'API. El correcte per a 5 de 6 components

Fixa't en l'última fila, perquè és la conclusió més important en termes pràctics: la millor manera de gestionar els permisos d'un pod que no necessita l'API és que no tingui token. Cap RBAC mal configurat no li pot fer mal si no hi ha credencial per fer servir.

El que queda per a 08-01: els objectes Role i ClusterRole (quins verbs sobre quins recursos), els RoleBinding i ClusterRoleBinding (a qui), l'agregació de rols, els rols predefinits del clúster (view, edit, admin, cluster-admin), i l'escalada de privilegis que cal evitar. Tot això es recolza en la identitat que has creat avui.

  1. Federació d'identitat amb els núvols

Un últim apunt perquè sàpigues que existeix, perquè és el que faràs servir si Rutas Norte passa a un clúster gestionat.

El problema: informes-ocupacio ha d'escriure els seus informes en un bucket d'emmagatzematge al núvol (s3://informes-rutasnorte/). La forma antiga seria crear una clau d'accés permanent al proveïdor i desar-la en un Secret. Això reintrodueix tots els problemes que vam veure a la lliçó de Secrets: una credencial estàtica, que cal rotar a mà i que no caduca.

La solució moderna es diu federació d'identitat de càrregues de treball: el proveïdor de núvol confia en l'emissor de tokens del clúster de Kubernetes i accepta el token de la ServiceAccount com a prova d'identitat, lliurant a canvi credencials temporals.

flowchart LR
    P["Pod amb SA<br/>informes-ocupacio"] --> T["Token projectat<br/>audience: sts.amazonaws.com"]
    T --> S["STS del nuvol<br/>verifica la signatura de<br/>l'emissor OIDC del cluster"]
    S --> C["Credencials TEMPORALS<br/>caduquen en 1 hora"]
    C --> B["Bucket d'informes"]

Com es diu a cada proveïdor:

Proveïdor Nom Com s'associa
AWS (EKS) IRSA (IAM Roles for Service Accounts) o Pod Identity Anotació eks.amazonaws.com/role-arn a la ServiceAccount
Google (GKE) Workload Identity Anotació iam.gke.io/gcp-service-account
Azure (AKS) Workload Identity Anotació azure.workload.identity/client-id + etiqueta al pod

Un exemple, només perquè reconeguis el patró:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: informes-ocupacio
  namespace: rutas-norte-pro
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/rutasnorte-informes

Amb aquella anotació, l'SDK d'AWS dins del pod obté credencials temporals automàticament, sense cap clau estàtica en cap Secret. La ServiceAccount de Kubernetes es converteix en la identitat reconeguda també fora del clúster.

Els avantatges són els mateixos que ja coneixes del token projectat: res permanent, rotació automàtica, àmbit limitat, i revocació immediata en esborrar l'associació. El detall de configurar-ho a cada proveïdor és tema de Kubernetes Gestionat.

Errors Comuns i Consells

Error Símptoma Solució
Fer servir la ServiceAccount default Sense traçabilitat, permisos compartits Una ServiceAccount per component
Deixar el token muntat a tots els pods Superfície d'atac innecessària automountServiceAccountToken: false
Canviar la ServiceAccount sense recrear pods Els pods vells conserven l'anterior kubectl rollout restart
Fer servir serviceAccount en lloc de serviceAccountName Funciona però està obsolet serviceAccountName
Llegir el token una sola vegada en arrencar 401 exactament una hora després Rellegir-lo a cada petició, o fer servir una biblioteca client
Fer servir curl -k contra l'apiserver Vulnerable a suplantació --cacert /var/run/secrets/.../ca.crt
Confondre 401 i 403 Es busca el problema on no és 401 = token; 403 = permisos
Crear un Secret de token permanent Credencial eterna i irrevocable kubectl create token --duration
Esperar que crear una SA concedeixi permisos Tot retorna 403 Kubernetes denega per defecte: cal RBAC
ServiceAccount d'un altre namespace El pod no arrenca Han de ser al mateix namespace
imagePullSecrets repetit a cada pod S'oblida al component nou Declarar-lo a la ServiceAccount
Desar claus estàtiques del núvol en Secrets Credencials sense rotar Federació d'identitat

Consells:

  1. Crea la ServiceAccount al costat del Deployment, al mateix fitxer. Que mai no hi hagi un component sense identitat pròpia.
  2. automountServiceAccountToken: false per defecte a la plantilla base. Que muntar el token sigui una decisió conscient, no el comportament heretat.
  3. Audita periòdicament quins pods porten token. L'ordre jq de l'apartat 7 hauria de retornar només components del sistema i els que de debò ho necessiten.
  4. Fes servir kubectl auth can-i --list --as=... a les revisions. És la manera ràpida de veure els permisos efectius d'una identitat abans d'aprovar un canvi.
  5. No concedeixis mai permisos a la default. És la via més ràpida a una escalada de privilegis a tot el namespace.

Exercicis

Exercici 1: Donar identitat a la plataforma

  1. Crea les sis ServiceAccounts de Rutas Norte a rutas-norte-pro amb l'esquema d'etiquetes del mòdul 2, posant automountServiceAccountToken: false a totes menys a informes-ocupacio.
  2. Modifica els cinc Deployments existents perquè facin servir la seva ServiceAccount i recrea els pods.
  3. Demostra amb una sola ordre que cada pod fa servir la seva.
  4. Demostra que botiga-web ja no té el directori del token i que informes-ocupacio sí.
  5. Escriu una ordre que llisti tots els pods del clúster que que tenen el token muntat i explica quins d'ells són legítims.

Exercici 2: Parlar amb l'API i entendre el 403

  1. Crea un pod consultor-api amb la ServiceAccount informes-ocupacio i el token muntat.
  2. Des de dins, consulta /version i /apis/authentication.k8s.io/v1/selfsubjectreviews i mostra la identitat completa.
  3. Intenta llistar els pods del namespace, captura el codi HTTP i el missatge complet, i identifica-hi el verb, el recurs, el grup d'API i l'àmbit.
  4. Descodifica la càrrega útil del token i localitza sub, exp, el pod al qual està lligat i la seva durada en minuts.
  5. Intenta fer la mateixa petició sense la capçalera Authorization i amb un token manipulat (canvia un caràcter). Explica els codis que obtens en cada cas i per què són diferents.

Exercici 3: Auditoria d'identitat i superfície d'atac

T'encarreguen una auditoria d'identitat de la plataforma abans d'una revisió de seguretat.

  1. Elabora una taula amb els sis components indicant: ServiceAccount, si munta token, si té imagePullSecrets i quins permisos efectius té.
  2. Identifica quins pods del clúster (inclòs kube-system) munten token i classifica'ls en "legítim" o "revisar".
  3. Simula un incident: un atacant aconsegueix executar codi dins del contenidor de botiga-web. Enumera què pot fer amb la configuració actual i què hauria pogut fer abans d'aquesta lliçó.
  4. Explica per què imagePullSecrets a la ServiceAccount default és acceptable però concedir permisos RBAC a la default no ho és.
  5. Proposa tres mesures addicionals d'enduriment, indicant en quina lliçó del curs s'estudia cadascuna.

Solucions

Solució 1

El fitxer k8s/base/serviceaccounts.yaml és el de l'apartat 4. Aplicació i modificació dels Deployments:

kubectl apply -f k8s/base/serviceaccounts.yaml

for C in botiga-web api-reserves postgres-reserves redis-cache worker-notificacions; do
  kubectl patch deployment $C -n rutas-norte-pro -p \
    "{\"spec\":{\"template\":{\"spec\":{\"serviceAccountName\":\"$C\",
       \"automountServiceAccountToken\":false}}}}"
done

kubectl rollout status deploy/api-reserves -n rutas-norte-pro
serviceaccount/botiga-web created
serviceaccount/api-reserves created
serviceaccount/postgres-reserves created
serviceaccount/redis-cache created
serviceaccount/worker-notificacions created
serviceaccount/informes-ocupacio created
deployment.apps/botiga-web patched
deployment.apps/api-reserves patched
deployment.apps/postgres-reserves patched
deployment.apps/redis-cache patched
deployment.apps/worker-notificacions patched
deployment "api-reserves" successfully rolled out
kubectl get pods -n rutas-norte-pro -o custom-columns=\
'POD:.metadata.name,SA:.spec.serviceAccountName,TOKEN:.spec.automountServiceAccountToken'
POD                                     SA                     TOKEN
api-reserves-8b5c7d9e2-4mkqp            api-reserves           false
api-reserves-8b5c7d9e2-9wnzr            api-reserves           false
postgres-reserves-6e9g7c0d5-l3nqx       postgres-reserves      false
redis-cache-7d0e9g8c6-wo5ry             redis-cache            false
botiga-web-6c8d0g5e9-i8qol              botiga-web             false
worker-notificacions-7g8e0d9c5-mn4qu    worker-notificacions   false
kubectl exec -n rutas-norte-pro deploy/botiga-web -- ls /var/run/secrets/kubernetes.io/ 2>&1
kubectl run prova-informes --image=curlimages/curl:8.10.1 -n rutas-norte-pro \
  --overrides='{"spec":{"serviceAccountName":"informes-ocupacio"}}' \
  --restart=Never -- sleep 60
sleep 5
kubectl exec -n rutas-norte-pro prova-informes -- ls /var/run/secrets/kubernetes.io/serviceaccount/
ls: /var/run/secrets/kubernetes.io/: No such file or directory
command terminated with exit code 1

pod/prova-informes created
ca.crt
namespace
token

L'auditoria de tokens muntats:

kubectl get pods -A -o json | jq -r '
  .items[] |
  select(.spec.automountServiceAccountToken != false) |
  "\(.metadata.namespace)\t\(.metadata.name)\t\(.spec.serviceAccountName)"' | column -t
kube-system       coredns-7db6d8ff4d-2vqxn          coredns
kube-system       kube-proxy-8xvmk                  kube-proxy
kube-system       storage-provisioner               storage-provisioner
kube-system       metrics-server-7d9f8c6b4-x2mkl    metrics-server
ingress-nginx     ingress-nginx-controller-9wnzr    ingress-nginx
rutas-norte-pro   prova-informes                    informes-ocupacio

Tots legítims: CoreDNS vigila Services i Endpoints, kube-proxy també, metrics-server publica una API agregada, el controlador d'Ingress llegeix objectes Ingress, i informes-ocupacio és el nostre. De Rutas Norte, només un de sis.

Solució 2

kubectl apply -f consultor-api.yaml
kubectl exec -it consultor-api -n rutas-norte-pro -- sh

Dins del pod:

SA=/var/run/secrets/kubernetes.io/serviceaccount
TOKEN=$(cat $SA/token); NS=$(cat $SA/namespace); API=https://kubernetes.default.svc

curl -s --cacert $SA/ca.crt -H "Authorization: Bearer $TOKEN" $API/version | head -4

curl -s --cacert $SA/ca.crt -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -X POST $API/apis/authentication.k8s.io/v1/selfsubjectreviews \
  -d '{"apiVersion":"authentication.k8s.io/v1","kind":"SelfSubjectReview"}' \
  | grep username
{
  "major": "1",
  "minor": "30",
  "gitVersion": "v1.30.4",

    "username": "system:serviceaccount:rutas-norte-pro:informes-ocupacio",
curl -s -w "\nHTTP: %{http_code}\n" --cacert $SA/ca.crt \
  -H "Authorization: Bearer $TOKEN" $API/api/v1/namespaces/$NS/pods | grep -E "message|HTTP"
  "message": "pods is forbidden: User \"system:serviceaccount:rutas-norte-pro:informes-ocupacio\"
   cannot list resource \"pods\" in API group \"\" in the namespace \"rutas-norte-pro\"",
HTTP: 403

Descomposició del missatge:

Element Valor
Verb list
Recurs pods
Grup d'API "" (core)
Àmbit namespace rutas-norte-pro
Identitat system:serviceaccount:rutas-norte-pro:informes-ocupacio

La càrrega útil del token:

cut -d. -f2 $SA/token | base64 -d 2>/dev/null | tr ',' '\n' | grep -E '"sub"|"exp"|"iat"|"pod"'
"sub":"system:serviceaccount:rutas-norte-pro:informes-ocupacio"
"exp":1785703921
"iat":1785700321
"pod":{"name":"consultor-api"
(1785703921 - 1785700321) / 60 = 60 minuts

Sense capçalera i amb token manipulat:

curl -s -o /dev/null -w "sense token: %{http_code}\n" --cacert $SA/ca.crt \
  $API/api/v1/namespaces/$NS/pods
curl -s -o /dev/null -w "token dolent: %{http_code}\n" --cacert $SA/ca.crt \
  -H "Authorization: Bearer ${TOKEN}X" $API/api/v1/namespaces/$NS/pods
sense token: 403
token dolent: 401

La diferència és subtil i instructiva:

  • Sense capçalera → 403. No és que l'apiserver no sàpiga qui ets: et classifica com l'usuari anònim system:anonymous, que és una identitat vàlida sense permisos. És un cas d'autenticació reeixida com a anònim i autorització denegada. (Si el clúster tingués desactivat l'accés anònim, sí que donaria 401.)
  • Token manipulat → 401. La signatura del JWT no valida, així que l'apiserver no pot establir cap identitat. És una fallada d'autenticació pura.

Aquesta distinció és la que et dirà, en un incident real, si el teu problema és la credencial o els permisos.

Solució 3

  1. La taula d'identitat:
Component ServiceAccount Token? imagePullSecrets Permisos efectius
botiga-web botiga-web No registry-rutasnorte Cap (sense credencial)
api-reserves api-reserves No registry-rutasnorte Cap (sense credencial)
postgres-reserves postgres-reserves No — (imatge pública) Cap (sense credencial)
redis-cache redis-cache No — (imatge pública) Cap (sense credencial)
worker-notificacions worker-notificacions No registry-rutasnorte Cap (sense credencial)
informes-ocupacio informes-ocupacio registry-rutasnorte Només selfsubject*, /healthz, /version
  1. Classificació dels pods amb token, amb l'ordre de la solució 1: tots els de kube-system i el controlador d'Ingress són legítims (necessiten vigilar objectes de l'API per funcionar). informes-ocupacio és legítim i previst. Si aparegués qualsevol altre pod de rutas-norte-*, seria un "revisar" immediat.

  2. L'incident simulat. Un atacant amb execució de codi a botiga-web:

Abans d'aquesta lliçó Ara
Llegir el token de la SA : /var/run/secrets/.../token No existeix el fitxer
Identificar-se davant l'API Sí, com a ...:default No: seria system:anonymous
Enumerar pods, services, deployments Depèn de RBAC sobre default No
Llegir els Secrets del namespace Si default tingués get secrets, sí: inclosa la contrasenya de postgres-reserves amb les dades personals dels clients No
Crear pods per escalar privilegis Depèn de RBAC No
Arribar a postgres-reserves per xarxa (el namespace no aïlla la xarxa) Sí, encara

La quarta fila és la que dona la mesura del risc: en un clúster on algú hagués concedit permisos a la default "per provar", una vulnerabilitat en un servidor de fitxers estàtics hauria permès llegir les credencials de la base de dades amb les dades personals de tots els clients de Rutas Norte. Treure el token elimina aquesta ruta per complet.

L'última fila assenyala el que no resol aquesta lliçó: la xarxa continua plana. Qualsevol pod del namespace pot obrir una connexió TCP a postgres-reserves:5432. Això ho tanquen les polítiques de xarxa.

  1. imagePullSecrets a la default és acceptable perquè no concedeix cap capacitat als pods: només permet al kubelet descarregar imatges del registre privat, que és una operació prèvia a l'arrencada i no una acció que el procés del contenidor pugui exercir. El contingut d'aquell Secret ni tan sols és visible des de dins del pod.

Concedir permisos RBAC a la default no ho és perquè la default la fa servir, per definició, tot pod que no especifiqui una altra cosa: qualsevol pod nou, qualsevol pod de depuració, qualsevol manifest enganxat d'internet. Un permís concedit allà s'atorga a un conjunt obert i impredictible de càrregues de treball, presents i futures. És el contrari d'una decisió explícita.

  1. Tres mesures d'enduriment addicionals:
Mesura Què aporta Lliçó
RBAC amb permisos mínims Que informes-ocupacio pugui llegir només el just, i ningú més res 08-01
NetworkPolicies Que només api-reserves i worker-notificacions puguin obrir connexions a postgres-reserves:5432 04-06
SecurityContext i Pod Security Standards Contenidor sense root, sistema de fitxers de només lectura, sense capacitats i sense escalada de privilegis: dificulta que l'execució de codi arribi a ser útil 08-02 i 08-03

Les tres, juntament amb el d'avui, formen les capes d'una defensa en profunditat: identitat mínima, permisos mínims, xarxa mínima i contenidor mínim.

Conclusió

Has tancat el mòdul 3 donant identitat a la plataforma. Saps que Kubernetes distingeix entre usuaris, que no són objectes del clúster i els gestiona un sistema extern, i ServiceAccounts, que sí que ho són, tenen namespace i s'identifiquen com a system:serviceaccount:<ns>:<nom>. Coneixes les cinc raons per no fer servir la default —és compartida, impedeix la traçabilitat, impedeix revocar de manera selectiva, munta un token que gairebé ningú no necessita i tempta a concedir-li permisos— i has creat una ServiceAccount dedicada per a cadascun dels sis components de Rutas Norte, assignant-la amb serviceAccountName.

Entens com ha canviat el token: d'aquells Secrets amb JWT eterns, no lligats a cap pod i desats a etcd, als tokens projectats de l'API TokenRequest, que caduquen en una hora, els rota el kubelet al 80 % de la seva vida, estan lligats a l'UID d'un pod concret, viuen només en tmpfs i s'invaliden en esborrar el pod. Saps què hi ha a /var/run/secrets/kubernetes.io/serviceaccount/token, ca.crt i namespace— i per a què serveix cada fitxer, inclòs el ca.crt que evita haver de fer servir --insecure. I tens clara la precedència d'automountServiceAccountToken, amb el patró recomanat: false a la ServiceAccount com a xarxa de seguretat i true explícit només on de debò es necessita. A Rutas Norte, cinc de sis components ja no porten cap credencial a dins, cosa que elimina d'arrel el primer moviment de qualsevol guió d'escalada de privilegis.

Has parlat amb l'API des de dins d'un pod amb curl, muntant la petició a mà amb la CA, el token i el Service kubernetes.default.svc; has preguntat qui ets amb SelfSubjectReview; i has rebut un 403 Forbidden que saps descompondre en verb, recurs, grup d'API i àmbit, distingint-lo del 401 que assenyala un problema de credencial i no de permisos. Aquell 403 no era una fallada: era la demostració que Kubernetes denega per defecte i que crear una identitat no concedeix absolutament res. Has mogut els imagePullSecrets a la ServiceAccount per no repetir-los ni oblidar-los, i saps que existeix la federació d'identitat amb els núvols perquè ni tan sols les credencials externes hagin de ser estàtiques.

Amb això tanques el mòdul 3 i la plataforma Rutas Norte està molt més sana que fa sis lliçons. La configuració viu fora de la imatge, en ConfigMaps per entorn; la contrasenya de postgres-reserves ja no és a Git, sinó en un Secret muntat a memòria amb només les claus que cada component necessita; les variables d'entorn estan catalogades component a component i entorn a entorn, amb la Downward API donant traçabilitat a cada petició; els tres entorns tenen quota i LimitRange, de manera que un error a desenvolupament no pot tocar producció; cada pod té una classe de QoS assignada amb criteri de negoci, amb postgres-reserves com a Guaranteed i últim supervivent; i cada component té la seva pròpia identitat davant l'API, gairebé sempre sense credencial muntada.

Però la plataforma continua tenint un forat molt visible: la xarxa és completament plana. Qualsevol pod de rutas-norte-pro pot obrir una connexió a postgres-reserves:5432, i cap client d'internet no pot arribar encara a www.rutasnorte.example ni a api.rutasnorte.example, perquè els nostres Services són tots ClusterIP i només existeixen dins del clúster. El mòdul 4, Xarxes a Kubernetes, ataca això de principi a fi: com funciona realment la xarxa del clúster i el model de "cada pod amb la seva IP", els tipus de Service més enllà de ClusterIP, el DNS intern que anem fent servir sense explicar del tot, els controladors d'Ingress que per fi publicaran la botiga i l'API als seus dominis, els certificats TLS gestionats automàticament amb cert-manager, i les polítiques de xarxa que impediran que ningú que no sigui api-reserves o worker-notificacions pugui ni tan sols intentar connectar-se a la base de dades amb les dades personals dels clients.

Curs de Kubernetes

Mòdul 1: Introducció a Kubernetes

Mòdul 2: Components Principals de Kubernetes

Mòdul 3: Gestió de Configuració i Secrets

Mòdul 4: Xarxes a Kubernetes

Mòdul 5: Emmagatzematge a Kubernetes

Mòdul 6: Conceptes Avançats de Kubernetes

Mòdul 7: Monitoratge i Registre

Mòdul 8: Seguretat a Kubernetes

Mòdul 9: Escalat i Rendiment

Mòdul 10: Ecosistema i Eines de Kubernetes

Mòdul 11: Estudis de Cas i Aplicacions del Món Real

Mòdul 12: Preparació per a la Certificació de Kubernetes

© Copyright 2026. Tots els drets reservats