Cerrábamos la lección anterior con una observación incómoda: hemos dado a los componentes de Rutas Norte su configuración, sus credenciales y sus recursos, pero no les hemos dado identidad. Ahora mismo, los pods de tienda-web, api-reservas, postgres-reservas, redis-cache y worker-notificaciones están usando todos la misma ServiceAccount default de su namespace, con un token de acceso a la API de Kubernetes montado dentro que ninguno de ellos necesita y que es lo primero que buscaría un atacante que consiguiera ejecutar código en uno de esos contenedores. Esta lección lo arregla: entenderás qué es una ServiceAccount y en qué se diferencia de un usuario, cómo funcionan los tokens proyectados modernos, por qué automountServiceAccountToken: false debe ser tu opción por defecto, y hablarás con la API desde dentro de un pod con curl para ver con tus ojos tanto una respuesta autorizada como un 403 Forbidden bien merecido.
Contenido
- La identidad de una carga de trabajo
- Usuarios frente a ServiceAccounts
- La ServiceAccount
defaulty por qué no debe usarse - Crear ServiceAccounts dedicadas para Rutas Norte
- El token proyectado: cómo ha cambiado
- Qué hay en
/var/run/secrets/kubernetes.io/serviceaccount/ automountServiceAccountToken: false- Hablar con la API desde dentro de un pod
- El
403 Forbiddeny qué significa imagePullSecretsen la ServiceAccount- La ServiceAccount dice quién eres, RBAC dice qué puedes hacer
- Federación de identidad con las nubes
- La identidad de una carga de trabajo
Toda petición que llega al kube-apiserver pasa por tres fases que ya conoces del módulo 1:
flowchart LR
A["Peticion"] --> B["AUTENTICACION<br/>Quien eres?"]
B --> C["AUTORIZACION (RBAC)<br/>Puedes hacer esto?"]
C --> D["ADMISION<br/>LimitRanger, ResourceQuota..."]
D --> E["etcd"]
B -.->|"no identificado"| F["401 Unauthorized"]
C -.->|"identificado pero sin permiso"| G["403 Forbidden"]
Cuando tú ejecutas kubectl get pods, la primera fase se resuelve con el certificado o el token de tu kubeconfig. Pero, ¿y cuando quien pregunta es un pod?
Los casos en que un pod necesita hablar con la API son más comunes de lo que parece:
| Quién | Para qué |
|---|---|
| Un controlador de Ingress | Leer objetos Ingress y Service para configurar su enrutamiento |
| Prometheus | Descubrir qué pods hay que consultar (07-03) |
| cert-manager | Crear y actualizar Secrets de tipo TLS (04-05) |
| Un operador | Reconciliar sus recursos personalizados (06-07) |
| Argo CD | Aplicar manifiestos desde Git (10-05) |
| Una aplicación tuya | Leer un ConfigMap en caliente, consultar sus propias réplicas, coordinar una elección de líder |
Y en Rutas Norte hay un caso concreto que llegará en el módulo 6: informes-ocupacion, la tarea nocturna, tendrá que consultar la API para saber cuántas réplicas de api-reservas hubo activas cada hora y correlacionarlo con la ocupación. Necesita identidad y necesita permisos.
Pero la razón principal para estudiar esto no son los pods que sí hablan con la API: son los cinco que no lo hacen y que, sin embargo, llevan encima una credencial válida sin necesitarla. Eso es superficie de ataque gratuita.
- Usuarios frente a ServiceAccounts
Kubernetes distingue dos tipos de identidad, y la diferencia es de fondo:
| Usuarios (users) | ServiceAccounts | |
|---|---|---|
| Para quién | Personas: administradores, desarrolladores | Procesos: pods, controladores |
| ¿Es un objeto de Kubernetes? | No. No existe kind: User |
Sí: kind: ServiceAccount |
| Quién los gestiona | Un sistema externo: certificados, OIDC, LDAP, IAM de la nube | Kubernetes, con kubectl create sa |
kubectl get |
Imposible | kubectl get serviceaccounts |
| Ámbito | Todo el clúster | Con namespace |
| Nombre en RBAC | [email protected] |
system:serviceaccount:<ns>:<nombre> |
| Cómo se autentican | Certificado de cliente, token OIDC | Token JWT firmado por el clúster |
| Ciclo de vida | Externo | Ligado al namespace |
El primer punto sorprende siempre: Kubernetes no tiene usuarios. No hay una base de datos de usuarios, ni un comando para crear uno. Cuando tu kubeconfig usa un certificado de cliente, Kubernetes lee el campo CN (Common Name) del certificado y lo toma como nombre de usuario, confiando en que la autoridad certificadora del clúster solo firma certificados legítimos. Con OIDC, delega en el proveedor de identidad. La gestión de personas es, deliberadamente, un problema de fuera.
Las ServiceAccounts, en cambio, son objetos de primera clase:
Solo hay una, la default, y todos nuestros pods la están usando. Fíjate en SECRETS 0: es la señal de que estamos en Kubernetes moderno, donde las ServiceAccounts ya no llevan un Secret con un token permanente asociado. Volveremos a ello en el apartado 5.
El nombre completo de una ServiceAccount en el sistema de autorización tiene esta forma:
Ese es el identificador exacto que usarás en los RoleBindings de RBAC, y también el que aparece en los registros de auditoría. Además, toda ServiceAccount pertenece automáticamente al grupo system:serviceaccounts y al grupo de su namespace, system:serviceaccounts:rutas-norte-pro, lo que permite conceder permisos a todas las de un entorno de una vez (algo que casi nunca es buena idea, pero conviene saber que existe).
- La ServiceAccount
default y por qué no debe usarse
default y por qué no debe usarseCada namespace obtiene una ServiceAccount llamada default en el momento de crearse. Si un pod no especifica ninguna, se le asigna esa.
kubectl get pod api-reservas-7f4b8c9d6-2xkqp -n rutas-norte-pro \
-o jsonpath='{.spec.serviceAccountName}'; echoCinco razones para no usarla:
Razón 1: es compartida. Todos los pods del namespace tienen la misma identidad. Si mañana informes-ocupacion necesita permiso para leer Deployments y se lo concedes a default, se lo estás concediendo también a tienda-web, a redis-cache y a postgres-reservas. Es lo contrario del mínimo privilegio.
Razón 2: impide la trazabilidad. En el registro de auditoría todas las peticiones aparecen como system:serviceaccount:rutas-norte-pro:default. Ante un acceso sospechoso, no hay forma de saber qué componente lo hizo.
Razón 3: impide revocar de forma selectiva. Si worker-notificaciones se ve comprometido, quieres retirarle los permisos de inmediato. Con la default, retirárselos significa retirárselos a todo el namespace.
Razón 4: monta un token que casi nadie necesita. Por defecto, automountServiceAccountToken es true, así que todos nuestros pods llevan dentro un token válido de la API. Compruébalo:
kubectl exec -n rutas-norte-pro deploy/tienda-web -- ls /var/run/secrets/kubernetes.io/serviceaccount/Un servidor nginx que solo sirve ficheros estáticos tiene ahí una credencial de acceso a la API del clúster. Si alguien encuentra una vulnerabilidad de lectura arbitraria de ficheros en esa aplicación (algo nada exótico), lo primero que hará es leer ese token.
Razón 5: la tentación de darle permisos. Es tan cómodo que, en un apuro, alguien acaba enlazando la default a un ClusterRole potente para "que funcione ya". Y eso no se revierte nunca.
La regla es simple:
Una ServiceAccount por componente, y
automountServiceAccountToken: falseen todas salvo en las que de verdad hablen con la API.
- Crear ServiceAccounts dedicadas para Rutas Norte
Empecemos por el objeto. Es de los más simples de Kubernetes:
apiVersion: v1
kind: ServiceAccount
metadata:
name: api-reservas
namespace: rutas-norte-pro
labels:
app: api-reservas
app.kubernetes.io/name: api-reservas
app.kubernetes.io/part-of: rutas-norte
entorno: pro
automountServiceAccountToken: false # api-reservas NO habla con la APIO de forma imperativa, para generar el YAML:
apiVersion: v1
kind: ServiceAccount
metadata:
creationTimestamp: null
name: api-reservas
namespace: rutas-norte-proEl plan completo para Rutas Norte, con la decisión razonada para cada componente:
| Componente | ServiceAccount | ¿Habla con la API? | automountServiceAccountToken |
|---|---|---|---|
tienda-web |
tienda-web |
No | false |
api-reservas |
api-reservas |
No | false |
postgres-reservas |
postgres-reservas |
No | false |
redis-cache |
redis-cache |
No | false |
worker-notificaciones |
worker-notificaciones |
No | false |
informes-ocupacion |
informes-ocupacion |
Sí (módulo 6) | true |
Cinco de seis no necesitan token. Ese es el resultado normal: la inmensa mayoría de las aplicaciones de negocio no hablan con la API de Kubernetes. Y aun así, la configuración por defecto se lo monta a todas.
¿Y por qué crear una ServiceAccount para los que no la usan, si no van a hablar con la API? Por tres motivos:
- Trazabilidad: si algún día uno de ellos hace una petición, sabrás cuál.
imagePullSecrets: la ServiceAccount es el sitio correcto para declararlos (apartado 10).- Preparación: cuando mañana
api-reservasnecesite un permiso concreto, la identidad ya existe y no hay que tocar el Deployment.
Los seis objetos, en k8s/base/serviceaccounts.yaml:
apiVersion: v1
kind: ServiceAccount
metadata:
name: tienda-web
namespace: rutas-norte-pro
labels: {app: tienda-web, app.kubernetes.io/part-of: rutas-norte, entorno: pro}
automountServiceAccountToken: false
imagePullSecrets:
- name: registry-rutasnorte
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: api-reservas
namespace: rutas-norte-pro
labels: {app: api-reservas, app.kubernetes.io/part-of: rutas-norte, entorno: pro}
automountServiceAccountToken: false
imagePullSecrets:
- name: registry-rutasnorte
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: postgres-reservas
namespace: rutas-norte-pro
labels: {app: postgres-reservas, app.kubernetes.io/part-of: rutas-norte, entorno: 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, entorno: pro}
automountServiceAccountToken: false
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: worker-notificaciones
namespace: rutas-norte-pro
labels: {app: worker-notificaciones, app.kubernetes.io/part-of: rutas-norte, entorno: pro}
automountServiceAccountToken: false
imagePullSecrets:
- name: registry-rutasnorte
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: informes-ocupacion
namespace: rutas-norte-pro
labels: {app: informes-ocupacion, app.kubernetes.io/part-of: rutas-norte, entorno: pro}
# Esta SI necesita el token: consultara la API en el modulo 6.
automountServiceAccountToken: true
imagePullSecrets:
- name: registry-rutasnorteY la asignación en el pod, con el campo serviceAccountName:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-reservas
namespace: rutas-norte-pro
spec:
replicas: 4
selector:
matchLabels:
app: api-reservas
entorno: pro
template:
metadata:
labels:
app: api-reservas
app.kubernetes.io/name: api-reservas
app.kubernetes.io/part-of: rutas-norte
entorno: pro
spec:
serviceAccountName: api-reservas # <-- la identidad del pod
automountServiceAccountToken: false # cinturon y tirantes
containers:
- name: api
image: registry.rutasnorte.example/api-reservas:2.5.0Un detalle histórico que verás en manifiestos antiguos: existe también un campo serviceAccount (sin Name). Está obsoleto desde hace muchas versiones y solo se conserva por compatibilidad. Usa siempre serviceAccountName.
Aplicamos y verificamos:
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/tienda-web created
serviceaccount/api-reservas created
serviceaccount/postgres-reservas created
serviceaccount/redis-cache created
serviceaccount/worker-notificaciones created
serviceaccount/informes-ocupacion created
POD SA
api-reservas-8b5c7d9e2-4mkqp api-reservas
api-reservas-8b5c7d9e2-9wnzr api-reservas
postgres-reservas-6e9g7c0d5-l3nqx postgres-reservas
redis-cache-7d0e9g8c6-wo5ry redis-cache
tienda-web-6c8d0g5e9-i8qol tienda-web
worker-notificaciones-7g8e0d9c5-mn4qu worker-notificacionesCada componente con su identidad. Y el token ha desaparecido de donde no hacía falta:
kubectl exec -n rutas-norte-pro deploy/tienda-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 1Ese error es exactamente el resultado deseado. No hay credencial que robar.
- El token proyectado: cómo ha cambiado
Esta parte tiene historia y es importante conocerla, porque encontrarás manifiestos y tutoriales de la época anterior.
Antes de Kubernetes 1.24: tokens permanentes en Secrets
Al crear una ServiceAccount, un controlador generaba automáticamente un Secret de tipo kubernetes.io/service-account-token con un JWT dentro. Ese token:
- No caducaba nunca.
- No estaba ligado a ningún pod: quien lo copiara podía usarlo desde cualquier sitio, incluso desde fuera del clúster.
- Se guardaba en etcd, así que estaba en las copias de seguridad.
- No se podía revocar sin borrar el Secret y la ServiceAccount.
Era, en la práctica, una contraseña eterna en cada namespace. Si un token se filtraba en un log, en un volcado o en un repositorio, seguía siendo válido meses después.
Desde 1.24, y obligatorio desde 1.25: la API TokenRequest
El mecanismo actual es completamente distinto y mucho mejor:
sequenceDiagram
participant K as kubelet
participant A as apiserver (TokenRequest)
participant P as Pod
K->>A: TokenRequest para la SA api-reservas,<br/>audiencia y vida limitadas,<br/>ligado a ESTE pod
A-->>K: JWT firmado (caduca en 1 hora)
K->>P: lo escribe en tmpfs<br/>/var/run/secrets/.../token
Note over K,P: A las 48 minutos (80% de la vida)
K->>A: renovacion
A-->>K: JWT nuevo
K->>P: sustituye el fichero
Note over P: La aplicacion debe RELEER el fichero
Las propiedades del token moderno:
| Propiedad | Token antiguo (Secret) | Token proyectado (TokenRequest) |
|---|---|---|
| Caducidad | Nunca | 1 hora por defecto |
| Rotación | Manual | Automática, por el kubelet al 80 % de la vida |
| Ligado al pod | No | Sí: contiene el UID del pod |
| Al borrar el pod | Sigue siendo válido | Se invalida |
Audiencia (aud) |
Genérica | Restringible a un destinatario |
| Guardado en etcd | Sí | No |
| Dónde vive | Secret + volumen | Solo en tmpfs del pod |
Veamos el contenido real de un token. Dentro de un pod que sí lo tenga montado:
kubectl exec -n rutas-norte-pro deploy/informes-ocupacion -- 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-ocupacion-28912440-x7mzq", "uid": "7f2c..."},
"serviceaccount": {"name": "informes-ocupacion", "uid": "1e8b..."}
},
"nbf": 1785700321,
"sub": "system:serviceaccount:rutas-norte-pro:informes-ocupacion"
}Es un JWT estándar. Lo relevante:
subes el identificador que usará RBAC.exp-iat= 3600 segundos: una hora de vida.kubernetes.io.podata el token a un pod concreto. Si ese pod se borra, el apiserver rechaza el token aunque no haya caducado.audlimita para quién es válido.
Un aviso práctico importante: si tu aplicación lee el token una vez al arrancar y lo guarda en memoria, dejará de funcionar en una hora. Las bibliotecas cliente oficiales (client-go, kubernetes de Python, etc.) releen el fichero automáticamente. Si hablas con la API con curl o con una biblioteca HTTP genérica, tienes que releer el fichero en cada petición. Es una fuente de errores 401 desconcertantes que aparecen exactamente una hora después del despliegue.
Se puede personalizar la proyección con un volumen explícito:
volumes:
- name: token-api
projected:
sources:
- serviceAccountToken:
path: token
expirationSeconds: 3600 # minimo 600
audience: api.rutasnorte.example # para un destinatario concretoEl campo audience es especialmente útil para un patrón avanzado: emitir un token que solo sirva ante un servicio externo concreto, de modo que si se filtra no valga para hablar con la API de Kubernetes.
Y si alguna vez necesitas un token permanente (integrar una herramienta externa que no puede rotar), hay que crearlo explícitamente:
apiVersion: v1
kind: Secret
metadata:
name: informes-ocupacion-token-permanente
namespace: rutas-norte-pro
annotations:
kubernetes.io/service-account.name: informes-ocupacion
type: kubernetes.io/service-account-tokenEvítalo siempre que puedas. Vuelve a introducir todos los problemas del modelo antiguo. Para casos puntuales, la alternativa correcta es pedir un token de vida limitada:
- Qué hay en
/var/run/secrets/kubernetes.io/serviceaccount/
/var/run/secrets/kubernetes.io/serviceaccount/Cuando el token se monta, el directorio contiene tres ficheros:
kubectl exec -n rutas-norte-pro deploy/informes-ocupacion -- 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)Reconoces dos cosas de lecciones anteriores: la cadena de enlaces a ..data que permite la actualización atómica (aquí es lo que hace posible la rotación del token) y el tmpfs, es decir, memoria RAM.
| Fichero | Contenido | Para qué sirve |
|---|---|---|
token |
El JWT firmado | Cabecera Authorization: Bearer <token> |
ca.crt |
Certificado de la CA del clúster | Verificar el certificado TLS del apiserver |
namespace |
Nombre del namespace, en texto plano | Que la aplicación sepa dónde vive sin la Downward API |
kubectl exec -n rutas-norte-pro deploy/informes-ocupacion -- sh -c \
'cat /var/run/secrets/kubernetes.io/serviceaccount/namespace; echo; \
head -1 /var/run/secrets/kubernetes.io/serviceaccount/ca.crt'El ca.crt es la pieza que suele olvidarse. Sin él, un cliente que hable con https://kubernetes.default.svc no puede validar el certificado del apiserver y hay que recurrir a --insecure, que es exactamente lo que no hay que hacer: sin validación, un atacante con acceso a la red del pod podría suplantar al apiserver y capturar el token.
Junto con las variables KUBERNETES_SERVICE_HOST y KUBERNETES_SERVICE_PORT que Kubernetes inyecta en todos los pods (las viste en la lección de variables de entorno), estos tres ficheros son todo lo necesario para hablar con la API. Es precisamente lo que las bibliotecas cliente llaman configuración dentro del clúster (InClusterConfig).
automountServiceAccountToken: false
automountServiceAccountToken: falseEste campo se puede poner en dos sitios, y la precedencia importa:
# En la ServiceAccount: afecta a TODOS los pods que la usen
apiVersion: v1
kind: ServiceAccount
metadata:
name: api-reservas
namespace: rutas-norte-pro
automountServiceAccountToken: false# En el pod: afecta solo a ESE pod y GANA sobre la ServiceAccount
spec:
serviceAccountName: api-reservas
automountServiceAccountToken: false| ServiceAccount | Pod | Resultado |
|---|---|---|
| (sin especificar) | (sin especificar) | Se monta (el valor por defecto) |
false |
(sin especificar) | No se monta |
true |
(sin especificar) | Se monta |
false |
true |
Se monta: el pod gana |
true |
false |
No se monta: el pod gana |
El pod siempre tiene la última palabra. Eso permite el patrón recomendado: poner false en la ServiceAccount como red de seguridad, y true explícito solo en el pod concreto que de verdad lo necesita.
Por qué esto importa tanto, en términos de seguridad: si un atacante consigue ejecutar código en un contenedor —a través de una vulnerabilidad de la aplicación, una dependencia comprometida o una inyección—, lo primero que hace un guion de explotación automatizado es leer ese token y probar qué permisos tiene. Es un paso estándar en cualquier herramienta de escalada de privilegios en Kubernetes.
Comparemos la superficie de ataque:
| Con token montado | Sin token montado | |
|---|---|---|
| Puede leer el token | Sí | No existe |
| Puede consultar la API | Sí, con los permisos de la SA | No, ni siquiera se identifica |
| Puede enumerar el clúster | Depende de RBAC | No |
| Si RBAC está mal configurado | Escalada de privilegios | Nada que escalar |
Sin token, incluso una configuración de RBAC descuidada queda neutralizada para ese pod: no hay credencial con la que ejercerla.
Comprobación del estado en todo el clúster, que es una auditoría que merece la pena hacer:
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-ocupacion-28912440-x7mzq -> informes-ocupacionLos del sistema lo necesitan legítimamente. De Rutas Norte, solo informes-ocupacion. Ese es el objetivo.
Un apunte de operación: si añades automountServiceAccountToken: false a una ServiceAccount, los pods que ya corren conservan su token. El montaje se decide al crear el pod. Hay que recrearlos:
- Hablar con la API desde dentro de un pod
Vamos a hacerlo a mano para entender qué hacen por dentro las bibliotecas cliente. Usaremos informes-ocupacion, el único componente de Rutas Norte que sí necesita hablar con la API.
Paso 1: un pod con la ServiceAccount y el token montado.
apiVersion: v1
kind: Pod
metadata:
name: consultor-api
namespace: rutas-norte-pro
labels:
app: informes-ocupacion
app.kubernetes.io/part-of: rutas-norte
entorno: pro
spec:
serviceAccountName: informes-ocupacion
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: 128MiPaso 2: preparar las variables dentro del pod.
# Ya dentro del contenedor
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 es el Service del apiserver, que existe en el namespace default de todo clúster. Cualquier pod puede resolverlo por DNS, como viste en el módulo 2.
Paso 3: la primera petición, al endpoint de versión (que no requiere permisos).
{
"major": "1",
"minor": "30",
"gitVersion": "v1.30.4",
"goVersion": "go1.22.5",
"platform": "linux/amd64"
}Ha funcionado. Los tres elementos de la petición:
--cacert $SA/ca.crt: verifica que el servidor es realmente el apiserver del clúster. Nunca uses-kni--insecure.Authorization: Bearer $TOKEN: la autenticación. Es el mecanismo estándar de JWT.- La URL del Service interno, resuelta por DNS.
Paso 4: preguntar quién soy. Este endpoint es utilísimo para 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-ocupacion",
"uid": "1e8b4c73-9d02-4a5f-8e31-7b6c0f294a15",
"groups": [
"system:serviceaccounts",
"system:serviceaccounts:rutas-norte-pro",
"system:authenticated"
]
}
}
}Ahí está la identidad completa: el username con el formato que anticipábamos y los tres grupos automáticos. Desde fuera, el equivalente es:
Paso 5: intentar algo que requiere permisos.
curl -s -o /dev/null -w "%{http_code}\n" --cacert $SA/ca.crt \
-H "Authorization: Bearer $TOKEN" \
$APISERVER/api/v1/namespaces/$NS/podsY aquí llega la parte instructiva.
- El
403 Forbidden y qué significa
403 Forbidden y qué significaVeamos el cuerpo completo de esa respuesta:
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-ocupacion\"
cannot list resource \"pods\" in API group \"\" in the namespace \"rutas-norte-pro\"",
"reason": "Forbidden",
"details": {"kind": "pods"},
"code": 403
}Este mensaje es una pequeña lección en sí mismo. Descompongámoslo:
| Parte | Significado |
|---|---|
User "system:serviceaccount:..." |
La autenticación funcionó: el apiserver sabe quién eres |
cannot list |
El verbo denegado |
resource "pods" |
El recurso |
in API group "" |
El grupo de API: "" es el grupo core |
in the namespace "..." |
El ámbito |
"code": 403 |
Prohibido, no "no autenticado" |
La distinción entre 401 y 403 es fundamental y hay que tenerla clara:
| Código | Significado | Causa típica |
|---|---|---|
| 401 Unauthorized | "No sé quién eres" | Token ausente, caducado, malformado o firmado por otro clúster |
| 403 Forbidden | "Sé quién eres, pero no puedes" | Falta un Role o un RoleBinding |
Si obtienes un 401, el problema está en el token: revisa que lo estés leyendo bien, que no haya caducado (recuerda la hora de vida) y que el pod tenga el volumen montado. Si obtienes un 403, el token es correcto y lo que falta son permisos.
Y ese 403 es exactamente lo que debe pasar ahora mismo. Nuestra ServiceAccount informes-ocupacion no tiene ningún permiso concedido, porque conceder permisos es trabajo de RBAC, y RBAC es el tema de la lección 08-01. Una ServiceAccount recién creada, sin RoleBindings, no puede hacer absolutamente nada más allá de los pocos endpoints públicos como /version.
Esto merece subrayarse porque es el diseño correcto:
Kubernetes deniega por defecto. Crear una identidad no concede ningún permiso. Todo lo que una ServiceAccount pueda hacer tiene que estar concedido explícitamente.
Podemos comprobar los permisos sin hacer la petición, con kubectl auth can-i:
kubectl auth can-i list pods -n rutas-norte-pro \
--as=system:serviceaccount:rutas-norte-pro:informes-ocupacion
kubectl auth can-i get deployments -n rutas-norte-pro \
--as=system:serviceaccount:rutas-norte-pro:informes-ocupacionY la lista completa de lo que sí puede:
kubectl auth can-i --list -n rutas-norte-pro \
--as=system:serviceaccount:rutas-norte-pro:informes-ocupacionResources Non-Resource URLs Verbs
selfsubjectreviews.authentication.k8s.io [] [create]
selfsubjectaccessreviews.authorization.k8s.io [] [create]
selfsubjectrulesreviews.authorization.k8s.io [] [create]
[/healthz] [get]
[/version] [get]Solo lo mínimo: preguntar quién es, preguntar qué puede hacer, y consultar la salud y la versión. Nada de leer pods, ni deployments, ni muchísimo menos los Secrets con la contraseña de postgres-reservas.
Como anticipo del módulo 8, así se vería la concesión mínima que informes-ocupacion necesitará, para que sepas hacia dónde vamos:
# ADELANTO de 08-01: no lo apliques todavia, se explica alli
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: lector-deployments
namespace: rutas-norte-pro
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list"] # solo lectura, solo deployments
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: informes-ocupacion-lector
namespace: rutas-norte-pro
subjects:
- kind: ServiceAccount
name: informes-ocupacion
namespace: rutas-norte-pro
roleRef:
kind: Role
name: lector-deployments
apiGroup: rbac.authorization.k8s.ioFíjate en el subjects: ahí es donde la ServiceAccount que hemos creado hoy se conecta con los permisos. Las dos piezas encajan, pero son objetos distintos y responsabilidades distintas.
Limpiamos el pod de pruebas:
imagePullSecrets en la ServiceAccount
imagePullSecrets en la ServiceAccountRecuperamos un cabo suelto de la lección de Secrets. Allí declaramos imagePullSecrets en cada pod para poder descargar imágenes de registry.rutasnorte.example:
Repetirlo en cada Deployment, cada Job y cada CronJob es tedioso y, sobre todo, fácil de olvidar: el fallo aparece semanas después, cuando alguien añade un componente nuevo y se encuentra con un ImagePullBackOff incomprensible.
La ServiceAccount es el sitio correcto:
apiVersion: v1
kind: ServiceAccount
metadata:
name: api-reservas
namespace: rutas-norte-pro
automountServiceAccountToken: false
imagePullSecrets:
- name: registry-rutasnorteY el Deployment queda limpio:
spec:
serviceAccountName: api-reservas # hereda el imagePullSecrets
containers:
- name: api
image: registry.rutasnorte.example/api-reservas:2.5.0Cómo funciona: cuando se crea un pod, un controlador de admisión copia los imagePullSecrets de la ServiceAccount al spec del pod. Puedes verlo:
kubectl get pod api-reservas-8b5c7d9e2-4mkqp -n rutas-norte-pro \
-o jsonpath='{.spec.imagePullSecrets}'; echoEl manifiesto no lo tenía y el objeto sí. Es el mismo fenómeno que con el LimitRanger de la lección anterior: un controlador de admisión ha mutado el objeto.
Tres detalles:
- Se suman, no se sustituyen. Si el pod declara los suyos, se combinan con los de la ServiceAccount.
- El Secret debe existir en el mismo namespace. Hay que crearlo en cada uno de los tres entornos.
- No es retroactivo. Los pods ya creados no lo heredan; hay que recrearlos.
Y un truco muy común para no repetirlo por componente: añadirlo a la ServiceAccount default de cada namespace, de modo que cualquier pod que no especifique otra lo herede.
kubectl patch serviceaccount default -n rutas-norte-pro \
-p '{"imagePullSecrets":[{"name":"registry-rutasnorte"}]}'Es útil, pero recuerda que no exime de crear ServiceAccounts dedicadas: imagePullSecrets es lo único razonable que debe llevar la default.
- La ServiceAccount dice quién eres, RBAC dice qué puedes hacer
Es la frase que resume la lección y conviene grabarla:
La ServiceAccount es la identidad; RBAC son los permisos. Son dos cosas independientes y ambas hacen falta.
flowchart LR
SA["ServiceAccount<br/>informes-ocupacion<br/>QUIEN eres"] --> T["Token JWT<br/>montado en el pod"]
T --> AU["Autenticacion<br/>el apiserver te identifica"]
AU --> RB["RoleBinding<br/>conecta identidad y permisos"]
RB --> R["Role<br/>QUE puedes hacer"]
R --> OK["Peticion permitida"]
AU -.->|"sin RoleBinding"| NO["403 Forbidden"]
Las combinaciones posibles y lo que significan:
| ServiceAccount | RBAC | Resultado |
|---|---|---|
| Dedicada | Sin permisos | Se identifica, no puede hacer nada. El estado actual de Rutas Norte |
| Dedicada | Permisos mínimos | El objetivo: cada componente hace exactamente lo suyo |
default compartida |
Permisos amplios | El antipatrón: todo el namespace hereda todo |
| Dedicada | cluster-admin |
Identidad perfecta, permisos catastróficos |
| Sin token montado | Cualquiera | No puede hablar con la API. Lo correcto para 5 de 6 componentes |
Fíjate en la última fila, porque es la conclusión más importante en términos prácticos: la mejor forma de gestionar los permisos de un pod que no necesita la API es que no tenga token. Ningún RBAC mal configurado puede hacerle daño si no hay credencial que usar.
Lo que queda para 08-01: los objetos Role y ClusterRole (qué verbos sobre qué recursos), los RoleBinding y ClusterRoleBinding (a quién), la agregación de roles, los roles predefinidos del clúster (view, edit, admin, cluster-admin), y la escalada de privilegios que hay que evitar. Todo eso se apoya en la identidad que has creado hoy.
- Federación de identidad con las nubes
Un último apunte para que sepas que existe, porque es lo que usarás si Rutas Norte pasa a un clúster gestionado.
El problema: informes-ocupacion debe escribir sus informes en un bucket de almacenamiento en la nube (s3://informes-rutasnorte/). La forma antigua sería crear una clave de acceso permanente en el proveedor y guardarla en un Secret. Eso reintroduce todos los problemas que vimos en la lección de Secrets: una credencial estática, que hay que rotar a mano y que no caduca.
La solución moderna se llama federación de identidad de cargas de trabajo: el proveedor de nube confía en el emisor de tokens del clúster de Kubernetes y acepta el token de la ServiceAccount como prueba de identidad, entregando a cambio credenciales temporales.
flowchart LR
P["Pod con SA<br/>informes-ocupacion"] --> T["Token proyectado<br/>audience: sts.amazonaws.com"]
T --> S["STS de la nube<br/>verifica la firma del<br/>emisor OIDC del cluster"]
S --> C["Credenciales TEMPORALES<br/>caducan en 1 hora"]
C --> B["Bucket de informes"]
Cómo se llama en cada proveedor:
| Proveedor | Nombre | Cómo se asocia |
|---|---|---|
| AWS (EKS) | IRSA (IAM Roles for Service Accounts) o Pod Identity | Anotación eks.amazonaws.com/role-arn en la ServiceAccount |
| Google (GKE) | Workload Identity | Anotación iam.gke.io/gcp-service-account |
| Azure (AKS) | Workload Identity | Anotación azure.workload.identity/client-id + etiqueta en el pod |
Un ejemplo, solo para que reconozcas el patrón:
apiVersion: v1
kind: ServiceAccount
metadata:
name: informes-ocupacion
namespace: rutas-norte-pro
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/rutasnorte-informesCon esa anotación, el SDK de AWS dentro del pod obtiene credenciales temporales automáticamente, sin ninguna clave estática en ningún Secret. La ServiceAccount de Kubernetes se convierte en la identidad reconocida también fuera del clúster.
Las ventajas son las mismas que ya conoces del token proyectado: nada permanente, rotación automática, ámbito limitado, y revocación inmediata al borrar la asociación. El detalle de configurarlo en cada proveedor es tema de Kubernetes Gestionado.
Errores Comunes y Consejos
| Error | Síntoma | Solución |
|---|---|---|
Usar la ServiceAccount default |
Sin trazabilidad, permisos compartidos | Una ServiceAccount por componente |
| Dejar el token montado en todos los pods | Superficie de ataque innecesaria | automountServiceAccountToken: false |
| Cambiar la ServiceAccount sin recrear pods | Los pods viejos conservan la anterior | kubectl rollout restart |
Usar serviceAccount en vez de serviceAccountName |
Funciona pero está obsoleto | serviceAccountName |
| Leer el token una sola vez al arrancar | 401 exactamente una hora después |
Releerlo en cada petición, o usar una biblioteca cliente |
Usar curl -k contra el apiserver |
Vulnerable a suplantación | --cacert /var/run/secrets/.../ca.crt |
| Confundir 401 y 403 | Se busca el problema donde no está | 401 = token; 403 = permisos |
| Crear un Secret de token permanente | Credencial eterna e irrevocable | kubectl create token --duration |
| Esperar que crear una SA conceda permisos | Todo devuelve 403 |
Kubernetes deniega por defecto: hace falta RBAC |
| ServiceAccount de otro namespace | El pod no arranca | Deben estar en el mismo namespace |
imagePullSecrets repetido en cada pod |
Se olvida en el componente nuevo | Declararlo en la ServiceAccount |
| Guardar claves estáticas de la nube en Secrets | Credenciales sin rotar | Federación de identidad |
Consejos:
- Crea la ServiceAccount junto al Deployment, en el mismo fichero. Que nunca haya un componente sin identidad propia.
automountServiceAccountToken: falsepor defecto en la plantilla base. Que montar el token sea una decisión consciente, no el comportamiento heredado.- Audita periódicamente qué pods llevan token. El comando
jqdel apartado 7 debería devolver solo componentes del sistema y los que de verdad lo necesitan. - Usa
kubectl auth can-i --list --as=...en las revisiones. Es la forma rápida de ver los permisos efectivos de una identidad antes de aprobar un cambio. - Nunca concedas permisos a la
default. Es la vía más rápida a una escalada de privilegios en todo el namespace.
Ejercicios
Ejercicio 1: Dar identidad a la plataforma
- Crea las seis ServiceAccounts de Rutas Norte en
rutas-norte-procon el esquema de etiquetas del módulo 2, poniendoautomountServiceAccountToken: falseen todas menos eninformes-ocupacion. - Modifica los cinco Deployments existentes para que usen su ServiceAccount y recrea los pods.
- Demuestra con un solo comando que cada pod usa la suya.
- Demuestra que
tienda-webya no tiene el directorio del token y queinformes-ocupacionsí. - Escribe un comando que liste todos los pods del clúster que sí tienen el token montado y explica cuáles de ellos son legítimos.
Ejercicio 2: Hablar con la API y entender el 403
- Crea un pod
consultor-apicon la ServiceAccountinformes-ocupaciony el token montado. - Desde dentro, consulta
/versiony/apis/authentication.k8s.io/v1/selfsubjectreviewsy muestra la identidad completa. - Intenta listar los pods del namespace, captura el código HTTP y el mensaje completo, e identifica en él el verbo, el recurso, el grupo de API y el ámbito.
- Descodifica la carga útil del token y localiza
sub,exp, el pod al que está ligado y su duración en minutos. - Intenta hacer la misma petición sin la cabecera
Authorizationy con un token manipulado (cambia un carácter). Explica los códigos que obtienes en cada caso y por qué son distintos.
Ejercicio 3: Auditoría de identidad y superficie de ataque
Te encargan una auditoría de identidad de la plataforma antes de una revisión de seguridad.
- Elabora una tabla con los seis componentes indicando: ServiceAccount, si monta token, si tiene
imagePullSecretsy qué permisos efectivos tiene. - Identifica qué pods del clúster (incluido
kube-system) montan token y clasifícalos en "legítimo" o "revisar". - Simula un incidente: un atacante logra ejecutar código dentro del contenedor de
tienda-web. Enumera qué puede hacer con la configuración actual y qué habría podido hacer antes de esta lección. - Explica por qué
imagePullSecretsen la ServiceAccountdefaultes aceptable pero conceder permisos RBAC a ladefaultno lo es. - Propón tres medidas adicionales de endurecimiento, indicando en qué lección del curso se estudia cada una.
Soluciones
Solución 1
El fichero k8s/base/serviceaccounts.yaml es el del apartado 4. Aplicación y modificación de los Deployments:
kubectl apply -f k8s/base/serviceaccounts.yaml
for C in tienda-web api-reservas postgres-reservas redis-cache worker-notificaciones; do
kubectl patch deployment $C -n rutas-norte-pro -p \
"{\"spec\":{\"template\":{\"spec\":{\"serviceAccountName\":\"$C\",
\"automountServiceAccountToken\":false}}}}"
done
kubectl rollout status deploy/api-reservas -n rutas-norte-proserviceaccount/tienda-web created
serviceaccount/api-reservas created
serviceaccount/postgres-reservas created
serviceaccount/redis-cache created
serviceaccount/worker-notificaciones created
serviceaccount/informes-ocupacion created
deployment.apps/tienda-web patched
deployment.apps/api-reservas patched
deployment.apps/postgres-reservas patched
deployment.apps/redis-cache patched
deployment.apps/worker-notificaciones patched
deployment "api-reservas" 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-reservas-8b5c7d9e2-4mkqp api-reservas false
api-reservas-8b5c7d9e2-9wnzr api-reservas false
postgres-reservas-6e9g7c0d5-l3nqx postgres-reservas false
redis-cache-7d0e9g8c6-wo5ry redis-cache false
tienda-web-6c8d0g5e9-i8qol tienda-web false
worker-notificaciones-7g8e0d9c5-mn4qu worker-notificaciones falsekubectl exec -n rutas-norte-pro deploy/tienda-web -- ls /var/run/secrets/kubernetes.io/ 2>&1
kubectl run prueba-informes --image=curlimages/curl:8.10.1 -n rutas-norte-pro \
--overrides='{"spec":{"serviceAccountName":"informes-ocupacion"}}' \
--restart=Never -- sleep 60
sleep 5
kubectl exec -n rutas-norte-pro prueba-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/prueba-informes created
ca.crt
namespace
tokenLa auditoría de tokens montados:
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 prueba-informes informes-ocupacionTodos legítimos: CoreDNS vigila Services y Endpoints, kube-proxy también, metrics-server publica una API agregada, el controlador de Ingress lee objetos Ingress, y informes-ocupacion es el nuestro. De Rutas Norte, solo uno de seis.
Solución 2
Dentro 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-ocupacion",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-ocupacion\"
cannot list resource \"pods\" in API group \"\" in the namespace \"rutas-norte-pro\"",
HTTP: 403Descomposición del mensaje:
| Elemento | Valor |
|---|---|
| Verbo | list |
| Recurso | pods |
| Grupo de API | "" (core) |
| Ámbito | namespace rutas-norte-pro |
| Identidad | system:serviceaccount:rutas-norte-pro:informes-ocupacion |
La carga útil del token:
"sub":"system:serviceaccount:rutas-norte-pro:informes-ocupacion"
"exp":1785703921
"iat":1785700321
"pod":{"name":"consultor-api"Sin cabecera y con token manipulado:
curl -s -o /dev/null -w "sin token: %{http_code}\n" --cacert $SA/ca.crt \
$API/api/v1/namespaces/$NS/pods
curl -s -o /dev/null -w "token malo: %{http_code}\n" --cacert $SA/ca.crt \
-H "Authorization: Bearer ${TOKEN}X" $API/api/v1/namespaces/$NS/podsLa diferencia es sutil e instructiva:
- Sin cabecera → 403. No es que el apiserver no sepa quién eres: te clasifica como el usuario anónimo
system:anonymous, que es una identidad válida sin permisos. Es un caso de autenticación exitosa como anónimo y autorización denegada. (Si el clúster tuviera desactivado el acceso anónimo, sí darías 401.) - Token manipulado → 401. La firma del JWT no valida, así que el apiserver no puede establecer ninguna identidad. Es un fallo de autenticación puro.
Esa distinción es la que te dirá, en un incidente real, si tu problema es la credencial o los permisos.
Solución 3
- La tabla de identidad:
| Componente | ServiceAccount | ¿Token? | imagePullSecrets |
Permisos efectivos |
|---|---|---|---|---|
tienda-web |
tienda-web |
No | registry-rutasnorte |
Ninguno (sin credencial) |
api-reservas |
api-reservas |
No | registry-rutasnorte |
Ninguno (sin credencial) |
postgres-reservas |
postgres-reservas |
No | — (imagen pública) | Ninguno (sin credencial) |
redis-cache |
redis-cache |
No | — (imagen pública) | Ninguno (sin credencial) |
worker-notificaciones |
worker-notificaciones |
No | registry-rutasnorte |
Ninguno (sin credencial) |
informes-ocupacion |
informes-ocupacion |
Sí | registry-rutasnorte |
Solo selfsubject*, /healthz, /version |
-
Clasificación de los pods con token, con el comando de la solución 1: todos los de
kube-systemy el controlador de Ingress son legítimos (necesitan vigilar objetos de la API para funcionar).informes-ocupaciones legítimo y previsto. Si apareciera cualquier otro pod derutas-norte-*, sería un "revisar" inmediato. -
El incidente simulado. Un atacante con ejecución de código en
tienda-web:
| Antes de esta lección | Ahora | |
|---|---|---|
| Leer el token de la SA | Sí: /var/run/secrets/.../token |
No existe el fichero |
| Identificarse ante la API | Sí, como ...:default |
No: sería system:anonymous |
| Enumerar pods, services, deployments | Depende de RBAC sobre default |
No |
| Leer los Secrets del namespace | Si default tuviera get secrets, sí: incluida la contraseña de postgres-reservas con los datos personales de los clientes |
No |
| Crear pods para escalar privilegios | Depende de RBAC | No |
Alcanzar postgres-reservas por red |
Sí (el namespace no aísla la red) | Sí, todavía |
La cuarta fila es la que da la medida del riesgo: en un clúster donde alguien hubiera concedido permisos a la default "para probar", una vulnerabilidad en un servidor de ficheros estáticos habría permitido leer las credenciales de la base de datos con los datos personales de todos los clientes de Rutas Norte. Quitar el token elimina esa ruta por completo.
La última fila señala lo que no resuelve esta lección: la red sigue plana. Cualquier pod del namespace puede abrir una conexión TCP a postgres-reservas:5432. Eso lo cierran las políticas de red.
imagePullSecretsen ladefaultes aceptable porque no concede ninguna capacidad a los pods: solo permite al kubelet descargar imágenes del registro privado, que es una operación previa al arranque y no una acción que el proceso del contenedor pueda ejercer. El contenido de ese Secret ni siquiera es visible desde dentro del pod.
Conceder permisos RBAC a la default no lo es porque la default la usa, por definición, todo pod que no especifique otra cosa: cualquier pod nuevo, cualquier pod de depuración, cualquier manifiesto pegado de internet. Un permiso concedido ahí se otorga a un conjunto abierto e impredecible de cargas de trabajo, presentes y futuras. Es lo contrario de una decisión explícita.
- Tres medidas de endurecimiento adicionales:
| Medida | Qué aporta | Lección |
|---|---|---|
| RBAC con permisos mínimos | Que informes-ocupacion pueda leer solo lo justo, y nadie más nada |
08-01 |
| NetworkPolicies | Que solo api-reservas y worker-notificaciones puedan abrir conexiones a postgres-reservas:5432 |
04-06 |
| SecurityContext y Pod Security Standards | Contenedor sin root, sistema de ficheros de solo lectura, sin capacidades y sin escalada de privilegios: dificulta que la ejecución de código llegue a ser útil | 08-02 y 08-03 |
Las tres, junto con lo de hoy, forman las capas de una defensa en profundidad: identidad mínima, permisos mínimos, red mínima y contenedor mínimo.
Conclusión
Has cerrado el módulo 3 dando identidad a la plataforma. Sabes que Kubernetes distingue entre usuarios, que no son objetos del clúster y los gestiona un sistema externo, y ServiceAccounts, que sí lo son, tienen namespace y se identifican como system:serviceaccount:<ns>:<nombre>. Conoces las cinco razones para no usar la default —es compartida, impide la trazabilidad, impide revocar de forma selectiva, monta un token que casi nadie necesita y tienta a concederle permisos— y has creado una ServiceAccount dedicada para cada uno de los seis componentes de Rutas Norte, asignándola con serviceAccountName.
Entiendes cómo ha cambiado el token: de aquellos Secrets con JWT eternos, no ligados a ningún pod y guardados en etcd, a los tokens proyectados de la API TokenRequest, que caducan en una hora, los rota el kubelet al 80 % de su vida, están atados al UID de un pod concreto, viven solo en tmpfs y se invalidan al borrar el pod. Sabes qué hay en /var/run/secrets/kubernetes.io/serviceaccount/ —token, ca.crt y namespace— y para qué sirve cada fichero, incluido el ca.crt que evita tener que usar --insecure. Y tienes clara la precedencia de automountServiceAccountToken, con el patrón recomendado: false en la ServiceAccount como red de seguridad y true explícito solo donde de verdad se necesita. En Rutas Norte, cinco de seis componentes ya no llevan credencial alguna dentro, lo que elimina de raíz el primer movimiento de cualquier guion de escalada de privilegios.
Has hablado con la API desde dentro de un pod con curl, montando la petición a mano con la CA, el token y el Service kubernetes.default.svc; has preguntado quién eres con SelfSubjectReview; y has recibido un 403 Forbidden que sabes descomponer en verbo, recurso, grupo de API y ámbito, distinguiéndolo del 401 que señala un problema de credencial y no de permisos. Ese 403 no era un fallo: era la demostración de que Kubernetes deniega por defecto y de que crear una identidad no concede absolutamente nada. Has movido los imagePullSecrets a la ServiceAccount para no repetirlos ni olvidarlos, y sabes que existe la federación de identidad con las nubes para que ni siquiera las credenciales externas tengan que ser estáticas.
Con esto cierras el módulo 3 y la plataforma Rutas Norte está mucho más sana que hace seis lecciones. La configuración vive fuera de la imagen, en ConfigMaps por entorno; la contraseña de postgres-reservas ya no está en Git, sino en un Secret montado en memoria con solo las claves que cada componente necesita; las variables de entorno están catalogadas componente a componente y entorno a entorno, con la Downward API dando trazabilidad a cada petición; los tres entornos tienen cuota y LimitRange, de modo que un error en desarrollo no puede tocar producción; cada pod tiene una clase de QoS asignada con criterio de negocio, con postgres-reservas como Guaranteed y último superviviente; y cada componente tiene su propia identidad ante la API, casi siempre sin credencial montada.
Pero la plataforma sigue teniendo un agujero muy visible: la red es completamente plana. Cualquier pod de rutas-norte-pro puede abrir una conexión a postgres-reservas:5432, y ningún cliente de internet puede llegar todavía a www.rutasnorte.example ni a api.rutasnorte.example, porque nuestros Services son todos ClusterIP y solo existen dentro del clúster. El módulo 4, Redes en Kubernetes, ataca eso de principio a fin: cómo funciona realmente la red del clúster y el modelo de "cada pod con su IP", los tipos de Service más allá de ClusterIP, el DNS interno que llevamos usando sin explicar del todo, los controladores de Ingress que por fin publicarán la tienda y la API en sus dominios, los certificados TLS gestionados automáticamente con cert-manager, y las políticas de red que impedirán que nadie que no sea api-reservas o worker-notificaciones pueda siquiera intentar conectarse a la base de datos con los datos personales de los clientes.
Curso de Kubernetes
Módulo 1: Introducción a Kubernetes
- ¿Qué es Kubernetes?
- Arquitectura de Kubernetes
- Conceptos y Terminología Clave
- Configuración de un Clúster de Kubernetes
- La CLI de Kubernetes: kubectl
- Objetos, Manifiestos YAML y el Modelo Declarativo
- El Proyecto del Curso: la Plataforma Rutas Norte
Módulo 2: Componentes Principales de Kubernetes
- Pods
- ReplicaSets
- Deployments
- Actualizaciones, Rollbacks y Estrategias de Despliegue
- Servicios
- Namespaces
- Etiquetas, Selectores y Anotaciones
Módulo 3: Gestión de Configuración y Secretos
- ConfigMaps
- Secrets
- Variables de Entorno
- Cuotas y Límites de Recursos
- LimitRanges y Clases de Calidad de Servicio (QoS)
- ServiceAccounts y Acceso a la API desde los Pods
Módulo 4: Redes en Kubernetes
- Redes de Clúster
- Tipos de Servicios
- DNS Interno y Descubrimiento de Servicios
- Controladores de Ingress
- TLS y Gestión de Certificados con cert-manager
- Políticas de Red
Módulo 5: Almacenamiento en Kubernetes
- Volúmenes
- Volúmenes Persistentes
- Reclamaciones de Volúmenes Persistentes
- Clases de Almacenamiento
- Aprovisionamiento Dinámico, Expansión y Snapshots
- Copias de Seguridad y Restauración de Datos
Módulo 6: Conceptos Avanzados de Kubernetes
- StatefulSets
- DaemonSets
- Trabajos y CronJobs
- Init Containers, Sidecars y Patrones Multi-Contenedor
- Planificación: Afinidad, Taints y Tolerations
- Definiciones de Recursos Personalizados (CRDs)
- Operadores y el Patrón Controlador
Módulo 7: Monitoreo y Registro
- Verificaciones de Salud y Sondas
- Servidor de Métricas y kubectl top
- Monitoreo con Prometheus
- Visualización y Alertas con Grafana y Alertmanager
- Registro Centralizado con Elasticsearch, Fluentd y Kibana (EFK)
- Depuración de Aplicaciones y Eventos del Clúster
Módulo 8: Seguridad en Kubernetes
- Control de Acceso Basado en Roles (RBAC)
- Contextos de Seguridad y Endurecimiento del Contenedor
- Políticas de Seguridad de Pods y Pod Security Standards
- Seguridad de Red
- Seguridad de Imágenes
- Auditoría, Escaneo y Gestión de Vulnerabilidades
Módulo 9: Escalado y Rendimiento
- Autoescalado Horizontal de Pods
- Autoescalado Vertical de Pods
- Autoescalado de Clúster
- Escalado por Eventos y Métricas Personalizadas con KEDA
- Alta Disponibilidad: PodDisruptionBudgets y Topología
- Ajuste de Rendimiento
Módulo 10: Ecosistema y Herramientas de Kubernetes
- Minikube y Entornos Locales con kind
- Kubeadm
- Helm
- Kustomize
- GitOps con Argo CD y Flux
- Kubernetes Gestionado: EKS, AKS y GKE
Módulo 11: Estudios de Caso y Aplicaciones del Mundo Real
- Despliegue de una Aplicación Web
- Ejecución de Aplicaciones con Estado
- CI/CD con Kubernetes
- Estrategias de Despliegue: Blue-Green y Canary
- Gestión Multi-Clúster
- Operación en Producción: Incidencias, Runbooks y Costes
