Lesson  in  Kubernetes 101

Namespaces, RBAC y ServiceAccounts

Aísla un equipo en su propio namespace, dale una identidad que puede listar Pods pero no borrarlos, y entrégasela a un Pod para que hable con la API del clúster con el privilegio mínimo.

Namespaces y RBAC

Hasta ahora has trabajado como administrador absoluto de un clúster de un solo inquilino. Los clústeres reales no son así: los comparten equipos, aplicaciones y entornos que no deben pisarse. Las dos herramientas para ordenarlo son los Namespaces, que dividen el clúster en espacios lógicos, y RBAC, que decide quién puede hacer qué en cada uno.

El capítulo de seguridad del libro separa con cuidado la autenticación (quién eres) de la autorización (qué puedes hacer). Este módulo se ocupa de la segunda, que es la que se toca con las manos.

El objetivo de esta unidad: dar al espacio del equipo A una identidad que puede mirar sus Pods pero no tocarlos. Trabaja desde la pestaña dev-machine.

Paso 1: El Namespace en el que llevas trabajando

Antes de crear nada, una comprobación que va a explicar el curso entero hacia atrás:

kubectl config view --minify -o jsonpath='{..namespace}'; echo
kubectl get namespaces

Llevas todo el curso trabajando dentro de un Namespace y no lo habías notado. Nunca has escrito -n porque el contexto ya apuntaba a tienda desde la primera lección. Eso es exactamente lo que hace un Namespace bien puesto: desaparecer.

Ahora que lo miras de frente, encajan cosas que has visto de pasada. El DNS interno de la lección de Services resolvía web.tienda.svc.cluster.local, y ese segundo trozo no era un adorno: era esto. Y en la lección del DaemonSet el agent tuvo que irse a plataforma porque tienda no admitía su hostPath, que es la primera vez que un Namespace te dijo que no.

Fíjate también en los que hay y no son tuyos: kube-system (los componentes del clúster, incluido el Traefik del módulo de red), default (el que Kubernetes crea siempre, y donde vive el Service del propio API server) y plataforma.

Crea el del equipo vecino, que dentro de un rato te va a servir para comprobar hasta dónde llegan los permisos que concedas:

kubectl create namespace equipo-b
kubectl get namespaces

Paso 2: La identidad (ServiceAccount)

RBAC autoriza identidades, así que primero hace falta una. Las ServiceAccounts son las identidades de las cargas de trabajo (los procesos que corren en Pods) y también sirven perfectamente para practicar RBAC:

kubectl create serviceaccount agent-sa -n tienda

Observa el -n tienda: la ServiceAccount, como casi todo a partir de ahora, vive dentro del namespace.

Y no es una exageración: compruébalo con el comando estrella de esta lección, que responde preguntas de permisos sin necesidad de probarlos:

kubectl auth can-i list pods --as=system:serviceaccount:tienda:agent-sa -n tienda

Responde no. En RBAC todo está denegado por defecto; los permisos solo se suman.

Paso 3: Los permisos (Role y RoleBinding)

RBAC funciona con dos piezas que la gente confunde constantemente: el Role define un conjunto de permisos, y el RoleBinding se los entrega a una identidad. Un Role sin binding es un papel sin firmar. Crea rbac.yaml con ambas:

cat << 'EOF' > rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: lector-pods
  namespace: tienda
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: agent-lector-pods
  namespace: tienda
subjects:
- kind: ServiceAccount
  name: agent-sa
  namespace: tienda
roleRef:
  kind: Role
  name: lector-pods
  apiGroup: rbac.authorization.k8s.io
EOF

El YAML, explicado con preguntas y respuestas

¿Qué hace el --- entre los dos objetos?

Es el separador de documentos de YAML: un mismo archivo puede contener varios objetos y kubectl apply -f los crea todos. Muy habitual para piezas que solo tienen sentido juntas, como estas dos.

¿Por qué apiGroups: [""]?

La cadena vacía designa el grupo core de la API, el de los Pods. Si el Role concediera permisos sobre Deployments, aquí pondría ["apps"]. Es la misma división de grupos que llevas viendo en los apiVersion de todo el curso.

¿Qué son exactamente los verbs?

Las acciones de la API de Kubernetes: la tríada de lectura es get (uno), list (todos) y watch (suscribirse a cambios). Las de escritura (create, update, patch, delete) se quedan deliberadamente fuera de este Role.

¿Qué une el RoleBinding con qué?

subjects (a quién: la ServiceAccount agent-sa) con roleRef (qué: el Role lector-pods). Los subjects también pueden ser usuarios o grupos, y un mismo Role puede repartirse a muchos subjects con distintos bindings.

¿Y si quisiera dar estos permisos en todo el clúster?

Existen las versiones sin namespace: ClusterRole y ClusterRoleBinding. La mecánica es idéntica; el alcance, global. Empieza siempre por la versión con namespace: en seguridad, el ámbito pequeño es el ámbito correcto.

Aplícalo y repite el interrogatorio, ahora con las dos preguntas que definen el objetivo de la lección:

kubectl apply -f rbac.yaml
kubectl auth can-i list pods --as=system:serviceaccount:tienda:agent-sa -n tienda
kubectl auth can-i delete pods --as=system:serviceaccount:tienda:agent-sa -n tienda

yes y no. Exactamente el privilegio mínimo que se pedía. Y ahora la pregunta que cierra la lección, la misma de antes pero apuntando al namespace del equipo vecino:

kubectl auth can-i list pods --as=system:serviceaccount:tienda:agent-sa -n equipo-b

También no, y esa es la diferencia entre un Role y un ClusterRole: el permiso que acabas de conceder no existe fuera de tienda. Por eso equipo-b estaba ahí desde el principio.

Resumen

  • Los Namespaces dividen el clúster en espacios lógicos con su propio contenido y sus propias reglas.
  • RBAC deniega todo por defecto; los permisos se conceden con Role (qué) más RoleBinding (a quién).
  • kubectl auth can-i --as=... responde preguntas de permisos sin ejecutar nada: úsalo antes, durante y después de cada cambio de RBAC.
  • ClusterRole y ClusterRoleBinding son las variantes de ámbito global, para cuando de verdad hagan falta.

El namespace ya tiene identidades y permisos. En la siguiente unidad verás para qué sirve de verdad una ServiceAccount: dársela a un Pod.

ServiceAccounts: la identidad de los Pods

Hasta ahora has usado la ServiceAccount agent-sa como maniquí: una identidad con la que practicar RBAC desde tu terminal, con --as=. Pero su trabajo real es otro. Una ServiceAccount es la identidad con la que un proceso que corre dentro de un Pod habla con la API de Kubernetes.

Y aquí está el dato que despierta a mucha gente: todos los Pods que has creado en este curso ya tenían una. Compruébalo:

kubectl get pods -n tienda -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.serviceAccountName}{"\n"}{end}'

Todos usan la ServiceAccount default, que existe automáticamente en cada namespace. Si tu Pod nunca habla con la API, esa identidad no le hace falta para nada, y aun así la lleva encima.

Paso 1: Darle a un Pod la identidad correcta

Crea agent.yaml, un Pod que sí necesita mirar el clúster:

cat << 'EOF' > agent.yaml
apiVersion: v1
kind: Pod
metadata:
  name: agent
  namespace: tienda
spec:
  serviceAccountName: agent-sa
  containers:
  - name: app
    image: ghcr.io/iximiuz/labs/nginx:alpine
    command: ["sh", "-c", "sleep infinity"]
EOF

Una sola línea nueva, serviceAccountName, y ese Pod deja de ser anónimo.

kubectl apply -f agent.yaml
kubectl wait --for=condition=Ready pod/agent -n tienda --timeout=60s
kubectl exec agent -n tienda -- ls -l /var/run/secrets/kubernetes.io/serviceaccount/

Tres ficheros aparecen dentro del contenedor sin que nadie los pidiera:

  • token: un JWT firmado por el clúster. Es la credencial.
  • ca.crt: el certificado de la autoridad del clúster, para que el Pod pueda verificar que habla con el API server de verdad.
  • namespace: el namespace del Pod, por comodidad.

Las preguntas que toca hacerse

¿De dónde sale ese token, si nadie ha creado ningún Secret?

De un volumen proyectado. El kubelet le pide al API server un token con caducidad corta, lo escribe en el sistema de archivos del contenedor y lo renueva solo antes de que expire. Antes se hacía de otra forma: cada ServiceAccount tenía un Secret con un token eterno guardado en etcd. Un token que no caduca nunca y que está en un Secret que cualquiera con permiso de lectura puede leer es exactamente lo que suena, así que se cambió. Es el mismo mecanismo detrás del kubectl create token que usarás en el módulo de kubectl.

¿Qué puede hacer este Pod con su token?

Exactamente lo que su RBAC le permita, ni más ni menos: listar Pods en tienda, porque eso es lo que le concediste con el RoleBinding. Ni borrar, ni salir de su namespace. La identidad no vale nada por sí sola; lo que vale son los permisos que le hayas atado.

Compruébalo desde dentro del propio Pod, hablando con la API a pelo:

kubectl exec agent -n tienda -- sh -c '
  TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
  curl -s --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
    -H "Authorization: Bearer ${TOKEN}" \
    https://kubernetes.default.svc/api/v1/namespaces/tienda/pods' | jq .

Responde con la lista de Pods en JSON. Los tres ficheros del volumen entran en juego a la vez: el token va en la cabecera, el ca.crt verifica que al otro lado está el API server de verdad, y el namespace es el que has escrito en la URL.

Y ahora pídele algo que no le corresponde, los Pods del namespace default:

kubectl exec agent -n tienda -- sh -c '
  TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
  curl -s --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
    -H "Authorization: Bearer ${TOKEN}" \
    https://kubernetes.default.svc/api/v1/namespaces/default/pods' | jq .

La API contesta un Failure con el motivo escrito entero: pods is forbidden: User "system:serviceaccount:tienda:agent-sa" cannot list resource "pods". RBAC funciona igual venga la petición de tu terminal o de dentro de un contenedor, y el sujeto que aparece en el error es exactamente el mismo que llevas escribiendo en los --as=. Fíjate además en el nombre kubernetes.default.svc: es un Service normal del namespace default, con su ClusterIP, exactamente igual que los que creaste en el módulo de red. El API server se descubre por DNS interno, como todo lo demás.

Paso 2: El Pod que no quiere identidad

Y ahora la pregunta que casi nadie se hace: ¿por qué lleva un token un Pod que nunca va a hablar con la API? Tu nginx no lo necesita. Es una credencial montada dentro de un contenedor que sirve páginas web a internet, esperando a que alguien encuentre una vulnerabilidad y la lea.

Crea api.yaml:

cat << 'EOF' > api.yaml
apiVersion: v1
kind: Pod
metadata:
  name: api
  namespace: tienda
spec:
  automountServiceAccountToken: false
  containers:
  - name: app
    image: ghcr.io/iximiuz/labs/nginx:alpine
    command: ["sh", "-c", "sleep infinity"]
EOF
kubectl apply -f api.yaml
kubectl wait --for=condition=Ready pod/api -n tienda --timeout=60s
kubectl exec api -n tienda -- sh -c '
  if [ -d /var/run/secrets/kubernetes.io/serviceaccount ]; then
    echo "SÍ hay directorio de token:"
    ls /var/run/secrets/kubernetes.io/serviceaccount/
  else
    echo "NO existe /var/run/secrets/kubernetes.io/serviceaccount"
  fi
'

NO existe. Y no es que el directorio esté vacío: no llega a crearse. Sin el volumen proyectado, Kubernetes no monta nada bajo /var/run/secrets, así que ahí dentro no hay ni la carpeta.

Pásale el mismo comando al Pod de antes, para ver la diferencia de lado a lado:

kubectl exec agent -n tienda -- sh -c '
  if [ -d /var/run/secrets/kubernetes.io/serviceaccount ]; then
    echo "SÍ hay directorio de token:"
    ls /var/run/secrets/kubernetes.io/serviceaccount/
  else
    echo "NO existe /var/run/secrets/kubernetes.io/serviceaccount"
  fi
'

, con sus tres ficheros. Mismo comando, mismo clúster, misma imagen: la única diferencia es una línea del manifiesto. No hay token, no hay credencial que robar.

¿Dónde se pone ese campo? En el Pod, como aquí, o directamente en la ServiceAccount (automountServiceAccountToken: false en su metadata), y entonces afecta a todos los Pods que la usen. Ponerlo en la ServiceAccount default de un namespace es una de las medidas de endurecimiento más baratas y efectivas que existen: nadie recibe un token por accidente, y quien lo necesite tendrá que pedirlo explícitamente.

Note

💡 La ServiceAccount default de cada namespace no tiene permisos en un clúster bien configurado: no puede listar nada. Pero es la primera parada de cualquiera que consiga ejecución dentro de un Pod, así que comprueba de vez en cuando que nadie le ha atado un ClusterRoleBinding por comodidad. kubectl auth can-i --list --as=system:serviceaccount:default:default te responde en un segundo.

Resumen

  • Una ServiceAccount es la identidad de un Pod frente a la API. Todos los Pods llevan una, aunque no la usen.
  • El token se monta como volumen proyectado, caduca y se renueva solo. Ya no vive en un Secret eterno.
  • Lo que un Pod puede hacer con su token lo decide RBAC, exactamente igual que con un usuario.
  • automountServiceAccountToken: false en los Pods (o en la propia ServiceAccount) para todo lo que no hable con la API.