Seguridad en Kubernetes:
RBAC, network policies y por qué un pod no debería ser root
Linux avanzado / Cloud native | unfantasmaenelsistema.com
Kubernetes hereda el problema de Docker y lo multiplica. Si un contenedor es un proceso del host con un traje de restricciones, un clúster de Kubernetes es una flota de esos procesos repartida por varias máquinas, coordinada por una API que lo controla todo y guardada en una base de datos que, si alguien la lee, entrega el clúster entero. La superficie de ataque no es un contenedor: es la API, el almacenamiento de estado, los nodos, la red interna y la identidad de cada carga de trabajo, todo a la vez.
Este artículo recorre los controles que de verdad contienen un incidente en Kubernetes: RBAC para que una carga comprometida no pueda hablar con la API, Pod Security y SecurityContext para que un pod no corra como root ni pida privilegios de más, NetworkPolicies para cortar el movimiento lateral este-oeste, y el manejo de secretos, que en Kubernetes no están cifrados por defecto. Es la continuación directa del artículo de seguridad en Docker, ahora al nivel del orquestador.
Qué aprenderás en este artículo
Qué componentes forman la superficie del clúster: API server, etcd, kubelet, dashboard · Cómo funciona RBAC y por qué el token de la service account montado por defecto es un problema · Pod Security Standards (privileged / baseline / restricted) y el SecurityContext que los cumple · Por qué en Kubernetes todo el tráfico entre pods está permitido hasta que escribes una NetworkPolicy · Por qué un Secret de Kubernetes es solo base64 y cómo se cifra de verdad · Escapes de contenedor a nodo y de nodo a clúster, y herramientas de auditoría (kube-bench, Trivy, Falco)
Encuadre legal y alcance — léelo antes de seguir
Todo lo que sigue está pensado para endurecer clústeres propios o de tu organización, con autorización: entornos de los que eres responsable como administrador, SRE o equipo de plataforma · Auditar la seguridad de un clúster ajeno, probar escapes o abusar de RBAC laxo sin permiso escrito es acceso ilícito y daños informáticos (en España, arts. 197 bis, 197 ter y 264 del Código Penal) · Los ejemplos usan manifiestos y clústeres de laboratorio (minikube, kind, k3s o un clúster de pruebas propio); no hay exploits contra CVE concretas ni recetas de intrusión · El objetivo es que quien opera un clúster reconozca y cierre estos fallos en su propio entorno
1. La superficie del clúster: qué hay que proteger
Antes de hablar de controles conviene tener claro qué piezas, si caen, comprometen el clúster. No son los pods: son el plano de control y los nodos.
| Componente | Qué es | Qué pasa si un atacante lo alcanza |
|---|---|---|
| API server | El único punto de entrada a todo el clúster; todo pasa por su API REST | Con credenciales suficientes, control total: crear pods, leer secretos, escalar privilegios |
| etcd | Base de datos clave-valor con TODO el estado del clúster, secretos incluidos | Lectura directa de etcd = todos los secretos y toda la configuración, sin pasar por RBAC |
| kubelet | Agente en cada nodo que arranca contenedores y expone una API en el puerto 10250 | Un kubelet con autenticación anónima permite ejecutar comandos en los pods de ese nodo |
| Dashboard / addons | Interfaz web y componentes añadidos (ingress, métricas, operadores) | Un dashboard expuesto con una service account potente fue el vector del incidente de Tesla en 2018 |
| Nodos (kubelet + runtime + SO) | Las máquinas Linux donde corren los pods | Root en un nodo = todos los pods de ese nodo, sus secretos montados y el token del kubelet |
- El API server debe tener autenticación fuerte, autorización RBAC (nunca el modo AlwaysAllow) y auditoría activada (audit log)
- etcd debe ir cifrado en reposo, con TLS mutuo entre pares y accesible solo desde el plano de control
- El kubelet no debe permitir autenticación anónima (`–anonymous-auth=false`) y su autorización debe ser `Webhook`, no `AlwaysAllow`
- El dashboard, si se usa, detrás de autenticación y con una service account de solo lectura acotada a un namespace
2. RBAC: el permiso mínimo, y el token que nadie pidió
El control de acceso basado en roles (RBAC) de Kubernetes tiene cuatro objetos. Un Role (o ClusterRole si es de ámbito clúster) es una lista de permisos: qué verbos (get, list, create, delete…) sobre qué recursos (pods, secrets, deployments…). Un RoleBinding (o ClusterRoleBinding) asigna ese rol a un sujeto: un usuario, un grupo o una service account. No hay permisos implícitos: lo que no está concedido, está denegado.
El punto ciego está en las service accounts. Cada pod recibe una identidad de service account, y por defecto Kubernetes monta su token dentro del pod en `/var/run/secrets/kubernetes.io/serviceaccount/token`. Si ese pod es una aplicación web y alguien consigue ejecución de código en ella, ya tiene un token válido para hablar con el API server. Lo que pueda hacer con él depende de a qué rol esté vinculada esa service account.
| Error de RBAC | Por qué es peligroso | Mitigación |
|---|---|---|
| Vincular una SA a cluster-admin «para que funcione» | Cualquier pod con esa SA controla el clúster entero | Roles a medida por aplicación; cluster-admin solo para humanos administradores |
| Token de SA montado en pods que no llaman a la API | Superficie regalada: un RCE en la app entrega un token de API | `automountServiceAccountToken: false` en la SA o en el pod |
| Verbos amplios: `secrets: [list]` a nivel de clúster | Permite leer todos los secretos de todos los namespaces | Acotar por namespace y a recursos concretos con `resourceNames` |
| Permiso `create` sobre `pods` o `pods/exec` | Equivale a ejecución de código en el nodo; vía de escalada conocida | Restringir; auditar quién puede crear pods, deployments, daemonsets y jobs |
| `escalate` / `bind` sobre roles | Permite auto-concederse más permisos de los que se tienen | Nunca conceder salvo a controladores del plano de control |
Cómo auditar RBAC en tu clúster
`kubectl auth can-i –list –as=system:serviceaccount:<ns>:<sa>` muestra qué puede hacer esa identidad · Herramientas como rbac-tool, kubectl-who-can o krane dibujan quién puede llegar a los recursos sensibles (secrets, exec, nodes) · Revisa periódicamente los ClusterRoleBindings a cluster-admin: deberían caber en una mano
3. Pod Security Standards: privileged, baseline, restricted
Las antiguas PodSecurityPolicy se eliminaron en la versión 1.25. Su sustituto es el Pod Security Admission, un controlador integrado que aplica los tres Pod Security Standards a nivel de namespace mediante etiquetas. No hay que instalar nada.
| Nivel | Qué permite | Uso recomendado |
|---|---|---|
| privileged | Sin restricciones: –privileged, hostPath, hostNetwork, cualquier capability | Solo namespaces de infraestructura muy concretos (CNI, CSI), nunca cargas de aplicación |
| baseline | Bloquea lo más peligroso (privileged, hostPID/IPC, hostPath, la mayoría de hostPorts) | Mínimo aceptable para cualquier carga; punto de partida al migrar |
| restricted | Además exige runAsNonRoot, drop de ALL capabilities, seccomp RuntimeDefault, rootfs de solo lectura recomendable | El objetivo para todas las cargas de aplicación en producción |
Se activa etiquetando el namespace. El modo `enforce` rechaza los pods que no cumplen; `warn` y `audit` solo avisan, útiles para medir antes de forzar.
# Namespace que exige el nivel 'restricted' y avisa de lo que no cumpliria
apiVersion: v1
kind: Namespace
metadata:
name: apps
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: latest
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/audit: restrictedPara políticas más finas que estos tres escalones (por ejemplo, permitir una capability concreta y nada más) se usan controladores de admisión externos: Kyverno o OPA Gatekeeper, que evalúan cada manifiesto contra reglas propias antes de admitirlo.
4. SecurityContext: por qué un pod no debería ser root
El `securityContext` es el bloque del manifiesto donde se declara con qué privilegios corre el contenedor. Igual que en Docker, el UID 0 dentro del pod es, salvo user namespace, el UID 0 del nodo: si el proceso escapa del contenedor, aterriza como root en la máquina. Correr como usuario sin privilegios convierte un escape en una molestia en lugar de un desastre.
# Contenedor endurecido: sin root, sin privilegios nuevos, rootfs de solo lectura,
# todas las capabilities descartadas y perfil seccomp por defecto del runtime
securityContext:
runAsNonRoot: true
runAsUser: 10001
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefault- runAsNonRoot: true — el kubelet rechaza el pod si la imagen intenta arrancar como UID 0; combínalo con `USER` en el Dockerfile
- allowPrivilegeEscalation: false y no_new_privs — ni un binario setuid dentro del contenedor puede elevar privilegios
- readOnlyRootFilesystem: true — el sistema de ficheros de la imagen es inmutable; da rutas de escritura con `emptyDir` montados donde de verdad haga falta
- capabilities.drop: [ALL] — parte de cero y añade solo lo imprescindible (casi nunca nada para una app web en puerto alto)
- Evita `hostPath`, `hostNetwork`, `hostPID`, `hostIPC` y `privileged`: cada uno perfora el aislamiento entre pod y nodo
- El user namespace de pods (`hostUsers: false`, estable a partir de versiones recientes) mapea el root del contenedor a un UID sin privilegios en el nodo
5. NetworkPolicies: la red plana es el regalo al atacante
Por defecto, en Kubernetes cualquier pod puede abrir una conexión con cualquier otro pod del clúster, en cualquier namespace. No hay segmentación. Un atacante que compromete un pod de frontend puede intentar hablar directamente con la base de datos, con el API server interno o con los pods de otro equipo. Es exactamente el escenario de movimiento lateral que la segmentación de red evita en una red tradicional.
Una NetworkPolicy es un objeto que selecciona pods por etiqueta y define qué tráfico de entrada (ingress) y de salida (egress) se les permite. En cuanto una política selecciona un pod, todo lo que no esté explícitamente permitido para ese pod queda denegado. Requiere un plugin de red (CNI) que las implemente: Calico, Cilium o similar; con algunos CNI básicos las políticas se ignoran en silencio.
# 1) Denegar por defecto TODO el trafico de entrada y salida en el namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: apps
spec:
podSelector: {}
policyTypes: ["Ingress", "Egress"]
---
# 2) Permitir solo: frontend -> api en el puerto 8080, y salida DNS
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-api
namespace: apps
spec:
podSelector:
matchLabels: { app: api }
policyTypes: ["Ingress"]
ingress:
- from:
- podSelector:
matchLabels: { app: frontend }
ports:
- protocol: TCP
port: 8080Detalles que rompen las NetworkPolicies en la práctica
Si ningún pod está seleccionado por alguna política, ese pod sigue completamente abierto: hay que desplegar un `default-deny` por namespace · Al denegar egress se corta también el DNS (puerto 53 a kube-dns): hay que permitirlo explícitamente o nada resuelve nombres · Las NetworkPolicies operan por etiquetas de pod/namespace, no por IP: etiquetar bien los namespaces es parte del control · Para reglas L7 (rutas HTTP, métodos) o identidad criptográfica entre servicios hace falta una service mesh o las políticas extendidas de Cilium
6. Secretos: base64 no es cifrado
Un objeto Secret de Kubernetes parece protegido porque su valor aparece codificado, pero eso es base64, una codificación reversible con un comando. Por defecto, los secretos se guardan en etcd en claro: quien lea etcd, o quien tenga permiso RBAC para listar secretos, los ve todos.
| Capa | Qué añade | Cómo |
|---|---|---|
| Cifrado en reposo de etcd | Los secretos dejan de estar en claro en el disco del plano de control | EncryptionConfiguration en el API server, idealmente con un proveedor KMS (nube o Vault) como raíz |
| RBAC estricto sobre `secrets` | Menos identidades pueden leerlos | Sin `list`/`get` de secrets salvo a quien lo necesita; nada a nivel de clúster |
| Token de SA no montado | Un pod comprometido no obtiene credenciales de API | `automountServiceAccountToken: false` |
| Gestor externo | Los secretos viven fuera de Kubernetes y se inyectan bajo demanda | External Secrets Operator + Vault/nube; o CSI Secrets Store montando el secreto como volumen efímero |
| No en el manifiesto ni en Git | El secreto no se filtra por el repositorio | sops+age o Sealed Secrets para lo que sí deba versionarse cifrado |
Higiene mínima de secretos
Activa el cifrado en reposo de etcd hoy: sin él, un backup de etcd filtrado entrega todas las credenciales del clúster · Prefiere montarlos como volumen (fichero) antes que como variable de entorno: las env se ven en `kubectl describe`, en logs de arranque y en un `/proc/1/environ` · Rota los secretos tras cualquier incidente y trata el token de cada service account como una credencial de pleno derecho
7. Del contenedor al nodo, del nodo al clúster
Los incidentes graves en Kubernetes encadenan pasos: se compromete una aplicación, se abusa de una configuración débil del pod para llegar al nodo, y desde el nodo se pivota al plano de control. A alto nivel:
| Salto | Idea a alto nivel | Qué lo habilita |
|---|---|---|
| App -> API del clúster | Usar el token de service account montado para consultar o modificar recursos | Token automontado + RBAC generoso en esa SA |
| Pod -> nodo | Escapar del contenedor al sistema de ficheros o a los procesos del nodo | privileged, hostPath a `/`, hostPID, capabilities amplias, runtime sin parchear |
| Nodo -> otros pods | Leer los secretos montados y los tokens de todos los pods de ese nodo | Root en el nodo; falta de user namespace |
| Nodo -> plano de control | Usar las credenciales del kubelet o de un pod del sistema para hablar con el API server | Nodos que ejecutan cargas de aplicación junto a componentes privilegiados; sin aislamiento de nodos |
| Namespace -> namespace | Alcanzar servicios de otro equipo por la red plana interna | Ausencia de NetworkPolicies default-deny |
7.1 Auditoría y detección
- kube-bench — comprueba el clúster contra el CIS Kubernetes Benchmark (flags del API server, kubelet, permisos de ficheros del plano de control)
- Trivy — escanea imágenes por CVE y también manifiestos y el propio clúster en busca de misconfiguraciones (`trivy k8s`)
- kubeaudit, Polaris, Kyverno en modo audit — revisan que los pods declaren SecurityContext, límites de recursos y no corran como root
- Falco — detección en tiempo de ejecución: shell interactivo dentro de un pod, lectura del token de service account, contacto inesperado con el API server, escritura en rutas sensibles
- Audit log del API server — quién hizo qué y cuándo; imprescindible para investigar un incidente
- Herramientas ofensivas en modo auditoría sobre tu propio clúster: kube-hunter (descubrimiento de superficie) y BloodHound/KubeHound para dibujar rutas de escalada de RBAC
Checklist de endurecimiento de Kubernetes
API server con RBAC, audit log y sin autenticación anónima; kubelet con `–anonymous-auth=false` y autorización Webhook · etcd cifrado en reposo (mejor con KMS), TLS mutuo y acceso restringido al plano de control · Pod Security Admission en `enforce: restricted` para todos los namespaces de aplicación · Cada pod con runAsNonRoot, allowPrivilegeEscalation:false, drop ALL capabilities, seccomp RuntimeDefault y rootfs de solo lectura · Nada de privileged, hostPath, hostNetwork, hostPID/IPC en cargas de aplicación · NetworkPolicy default-deny por namespace + reglas explícitas, incluida la salida DNS · Service accounts con permiso mínimo y `automountServiceAccountToken: false` salvo que se llame a la API · Secretos vía gestor externo o CSI; nunca en Git en claro; preferir volumen a variable de entorno · Imágenes mínimas firmadas y escaneadas en CI (Trivy/Grype, cosign); admission control que verifique la firma · kube-bench + Trivy en CI/cron; Falco vigilando el runtime; nodos y runtime parcheados
Conclusión
La seguridad de Kubernetes no es una casilla que se marca, sino la suma de cuatro contenciones que hay que poner una por una: RBAC para que una carga comprometida no pueda hablar con la API, Pod Security y SecurityContext para que ningún pod corra como root ni pida privilegios que no usa, NetworkPolicies para que un pod caído no alcance a los demás, y cifrado real de los secretos para que un backup de etcd no sea un llavero maestro. Ninguna es difícil de aplicar; lo difícil es que el valor por defecto de casi todas ellas es «abierto».
El patrón mental útil es el mismo que en Docker, escalado: cada permiso de RBAC, cada capability, cada montaje del host y cada ruta de red abierta es superficie que alguien dentro de un pod puede intentar usar contra el nodo y contra el clúster. Empieza por lo que más contiene un incidente —restricted, default-deny, token no automontado, etcd cifrado— y verifícalo de forma continua con kube-bench, Trivy y Falco. La factura de hacerlo es una tarde de manifiestos; la de no hacerlo la pone el atacante.
Referencias
Kubernetes docs — Security, Pod Security Standards, RBAC Authorization, Network Policies, Encrypting Secret Data at Rest · CIS Kubernetes Benchmark y kube-bench (aquasecurity/kube-bench) · NSA/CISA — Kubernetes Hardening Guidance · OWASP Kubernetes Security Cheat Sheet y el OWASP Kubernetes Top Ten · Trivy (aquasecurity/trivy), Kyverno (kyverno.io), OPA Gatekeeper, Falco (falco.org) · Nuestro artículo sobre seguridad en Docker, para el contexto de contenedores y capabilities
unfantasmaenelsistema.com | Seguridad, sistemas y todo lo que no debería funcionar (pero funciona)

