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
- La identitat d'una càrrega de treball
- Usuaris davant de ServiceAccounts
- La ServiceAccount
defaulti per què no s'ha de fer servir - Crear ServiceAccounts dedicades per a Rutas Norte
- El token projectat: com ha canviat
- Què hi ha a
/var/run/secrets/kubernetes.io/serviceaccount/ automountServiceAccountToken: false- Parlar amb l'API des de dins d'un pod
- El
403 Forbiddeni què significa imagePullSecretsa la ServiceAccount- La ServiceAccount diu qui ets, RBAC diu què pots fer
- Federació d'identitat amb els núvols
- 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 sí 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.
- 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 |
Sí: 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:
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:
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).
- La ServiceAccount
default i per què no s'ha de fer servir
default i per què no s'ha de fer servirCada 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}'; echoCinc 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/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: falsea totes llevat de les que de debò parlin amb l'API.
- 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'APIO de manera imperativa, per generar el YAML:
apiVersion: v1
kind: ServiceAccount
metadata:
creationTimestamp: null
name: api-reserves
namespace: rutas-norte-proEl 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 |
Sí (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:
- Traçabilitat: si algun dia un d'ells fa una petició, sabràs quin.
imagePullSecrets: la ServiceAccount és el lloc correcte per declarar-los (apartat 10).- Preparació: quan demà
api-reservesnecessiti 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-rutasnorteI 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.0Un 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-notificacionsCada 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 1Aquest error és exactament el resultat desitjat. No hi ha credencial per robar.
- 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 | Sí: 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 | Sí | 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.podlliga el token a un pod concret. Si aquell pod s'esborra, l'apiserver rebutja el token encara que no hagi caducat.audlimita 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 concretEl 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-tokenEvita-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:
- Què hi ha a
/var/run/secrets/kubernetes.io/serviceaccount/
/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'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).
automountServiceAccountToken: false
automountServiceAccountToken: falseAquest 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 | Sí | 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 -10kube-system/coredns-7db6d8ff4d-2vqxn -> coredns
kube-system/kube-proxy-8xvmk -> kube-proxy
rutas-norte-pro/informes-ocupacio-28912440-x7mzq -> informes-ocupacioEls 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:
- 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: 128MiPas 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).
{
"major": "1",
"minor": "30",
"gitVersion": "v1.30.4",
"goVersion": "go1.22.5",
"platform": "linux/amd64"
}Ha funcionat. Els tres elements de la petició:
--cacert $SA/ca.crt: verifica que el servidor és realment l'apiserver del clúster. No facis servir mai-kni--insecure.Authorization: Bearer $TOKEN: l'autenticació. És el mecanisme estàndard de JWT.- 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:
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/podsI aquí arriba la part instructiva.
- El
403 Forbidden i què significa
403 Forbidden i què significaVegem 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-ocupacioI la llista completa del que sí que pot:
kubectl auth can-i --list -n rutas-norte-pro \
--as=system:serviceaccount:rutas-norte-pro:informes-ocupacioResources 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.ioFixa'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:
imagePullSecrets a la ServiceAccount
imagePullSecrets a la ServiceAccountRecuperem un cap solt de la lliçó de Secrets. Allà vam declarar imagePullSecrets a cada pod per poder descarregar imatges de registry.rutasnorte.example:
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-rutasnorteI el Deployment queda net:
spec:
serviceAccountName: api-reserves # hereta l'imagePullSecrets
containers:
- name: api
image: registry.rutasnorte.example/api-reserves:2.5.0Com 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}'; echoEl 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:
- Se sumen, no se substitueixen. Si el pod declara els seus, es combinen amb els de la ServiceAccount.
- El Secret ha d'existir al mateix namespace. Cal crear-lo a cadascun dels tres entorns.
- 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"}]}'És útil, però recorda que no eximeix de crear ServiceAccounts dedicades: imagePullSecrets és l'única cosa raonable que ha de portar la default.
- 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.
- 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-informesAmb 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:
- Crea la ServiceAccount al costat del Deployment, al mateix fitxer. Que mai no hi hagi un component sense identitat pròpia.
automountServiceAccountToken: falseper defecte a la plantilla base. Que muntar el token sigui una decisió conscient, no el comportament heretat.- Audita periòdicament quins pods porten token. L'ordre
jqde l'apartat 7 hauria de retornar només components del sistema i els que de debò ho necessiten. - 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. - 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
- Crea les sis ServiceAccounts de Rutas Norte a
rutas-norte-proamb l'esquema d'etiquetes del mòdul 2, posantautomountServiceAccountToken: falsea totes menys ainformes-ocupacio. - Modifica els cinc Deployments existents perquè facin servir la seva ServiceAccount i recrea els pods.
- Demostra amb una sola ordre que cada pod fa servir la seva.
- Demostra que
botiga-webja no té el directori del token i queinformes-ocupaciosí. - Escriu una ordre que llisti tots els pods del clúster que sí que tenen el token muntat i explica quins d'ells són legítims.
Exercici 2: Parlar amb l'API i entendre el 403
- Crea un pod
consultor-apiamb la ServiceAccountinformes-ocupacioi el token muntat. - Des de dins, consulta
/versioni/apis/authentication.k8s.io/v1/selfsubjectreviewsi mostra la identitat completa. - 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.
- Descodifica la càrrega útil del token i localitza
sub,exp, el pod al qual està lligat i la seva durada en minuts. - Intenta fer la mateixa petició sense la capçalera
Authorizationi 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.
- Elabora una taula amb els sis components indicant: ServiceAccount, si munta token, si té
imagePullSecretsi quins permisos efectius té. - Identifica quins pods del clúster (inclòs
kube-system) munten token i classifica'ls en "legítim" o "revisar". - 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çó. - Explica per què
imagePullSecretsa la ServiceAccountdefaultés acceptable però concedir permisos RBAC a ladefaultno ho és. - 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-proserviceaccount/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 outkubectl 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 falsekubectl 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
tokenL'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 -tkube-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-ocupacioTots 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
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: 403Descomposició 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:
"sub":"system:serviceaccount:rutas-norte-pro:informes-ocupacio"
"exp":1785703921
"iat":1785700321
"pod":{"name":"consultor-api"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/podsLa 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
- 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 |
Sí | registry-rutasnorte |
Només selfsubject*, /healthz, /version |
-
Classificació dels pods amb token, amb l'ordre de la solució 1: tots els de
kube-systemi 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 derutas-norte-*, seria un "revisar" immediat. -
L'incident simulat. Un atacant amb execució de codi a
botiga-web:
| Abans d'aquesta lliçó | Ara | |
|---|---|---|
| Llegir el token de la SA | Sí: /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 |
Sí (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.
imagePullSecretsa ladefaulté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.
- 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
- Què és Kubernetes?
- Arquitectura de Kubernetes
- Conceptes i Terminologia Clau
- Configuració d'un Clúster de Kubernetes
- La CLI de Kubernetes: kubectl
- Objectes, Manifests YAML i el Model Declaratiu
- El Projecte del Curs: la Plataforma Rutas Norte
Mòdul 2: Components Principals de Kubernetes
- Pods
- ReplicaSets
- Deployments
- Actualitzacions, Rollbacks i Estratègies de Desplegament
- Serveis
- Namespaces
- Etiquetes, Selectors i Anotacions
Mòdul 3: Gestió de Configuració i Secrets
- ConfigMaps
- Secrets
- Variables d'Entorn
- Quotes i Límits de Recursos
- LimitRanges i Classes de Qualitat de Servei (QoS)
- ServiceAccounts i Accés a l'API des dels Pods
Mòdul 4: Xarxes a Kubernetes
- Xarxes de Clúster
- Tipus de Serveis
- DNS Intern i Descobriment de Serveis
- Controladors d'Ingress
- TLS i Gestió de Certificats amb cert-manager
- Polítiques de Xarxa
Mòdul 5: Emmagatzematge a Kubernetes
- Volums
- Volums Persistents
- Reclamacions de Volums Persistents
- Classes d'Emmagatzematge
- Aprovisionament Dinàmic, Expansió i Snapshots
- Còpies de Seguretat i Restauració de Dades
Mòdul 6: Conceptes Avançats de Kubernetes
- StatefulSets
- DaemonSets
- Treballs i CronJobs
- Init Containers, Sidecars i Patrons Multicontenidor
- Planificació: Afinitat, Taints i Toleracions
- Definicions de Recursos Personalitzats (CRDs)
- Operadors i el Patró Controlador
Mòdul 7: Monitoratge i Registre
- Verificacions de Salut i Sondes
- Servidor de Mètriques i kubectl top
- Monitoratge amb Prometheus
- Visualització i Alertes amb Grafana i Alertmanager
- Registre Centralitzat amb Elasticsearch, Fluentd i Kibana (EFK)
- Depuració d'Aplicacions i Esdeveniments del Clúster
Mòdul 8: Seguretat a Kubernetes
- Control d'Accés Basat en Rols (RBAC)
- Contextos de Seguretat i Enduriment del Contenidor
- Polítiques de Seguretat de Pods i Pod Security Standards
- Seguretat de Xarxa
- Seguretat d'Imatges
- Auditoria, Escaneig i Gestió de Vulnerabilitats
Mòdul 9: Escalat i Rendiment
- Autoescalat Horitzontal de Pods
- Autoescalat Vertical de Pods
- Autoescalat de Clúster
- Escalat per Esdeveniments i Mètriques Personalitzades amb KEDA
- Alta Disponibilitat: PodDisruptionBudgets i Topologia
- Ajust de Rendiment
Mòdul 10: Ecosistema i Eines de Kubernetes
- Minikube i Entorns Locals amb kind
- Kubeadm
- Helm
- Kustomize
- GitOps amb Argo CD i Flux
- Kubernetes Gestionat: EKS, AKS i GKE
Mòdul 11: Estudis de Cas i Aplicacions del Món Real
- Desplegament d'una Aplicació Web
- Execució d'Aplicacions amb Estat
- CI/CD amb Kubernetes
- Estratègies de Desplegament: Blue-Green i Canary
- Gestió Multi-Clúster
- Operació en Producció: Incidències, Runbooks i Costos
