Seguridad en Kubernetes
La seguridad en Kubernetes es un tema muy amplio. En este capítulo veremos los fundamentos con ejemplos y ejercicios que podemos lanzar en nuestro propio clúster. Luego los ampliaremos en la especialización CKS del curso.
La agruparemos en tres bloques:
- Accesos e identidades: quién eres y qué puedes hacer (autenticación y autorización).
- Security Context: con qué privilegios se ejecuta cada contenedor.
- Pod Security Admission: qué pods se dejan entrar en cada namespace.
- Y un cuarto, Network Policies (quién puede hablar con quién), que tiene capítulo propio.
Antes de entrar en materia, conviene conocer el modelo de las 4C de la seguridad cloud native. Son cuatro capas, cada una dentro de la anterior, como unas muñecas rusas:
| Capa | Qué protege | Ejemplo |
|---|---|---|
| Cloud | La infraestructura | Firewalls del proveedor, acceso a las máquinas |
| Cluster | Kubernetes | RBAC, Pod Security (este capítulo), NetworkPolicies (el siguiente) |
| Container | Las imágenes y su ejecución | Usuario no root, imágenes mínimas |
| Code | Nuestra aplicación | Dependencias, secretos, validación de entradas |
De poco sirve un código perfecto si cualquiera puede entrar al clúster, y viceversa.
Preparar el laboratorio
Todos los ejercicios del capítulo usan un namespace propio, así que al final basta con borrarlo:
kubectl create namespace seguridad
Autenticación, autorización y control de admisión
Cada petición a la API de Kubernetes pasa por tres controles, en este orden. Una buena analogía es el control de un aeropuerto:
- Autenticación: ¿quién eres? Es enseñar el DNI.
- Autorización: ¿puedes hacer esto? Es la tarjeta de embarque: estás identificado, pero solo puedes subir a tu avión.
- Control de admisión: ¿lo que llevas cumple las normas? Es el escáner del equipaje: puede rechazar la petición o incluso modificarla antes de guardarla en etcd.
Autenticación
Kubernetes no tiene una base de datos de usuarios. Identifica a quien llama por sus credenciales: certificados x509, tokens, OpenID Connect (OIDC)... Lo veremos a fondo en el capítulo de usuarios.
Autorización
Una vez identificados, Kubernetes decide si podemos hacer lo que pedimos. El modo estándar es RBAC (Role Based Access Control, control de acceso basado en roles): definimos roles con permisos y se los asignamos a usuarios, grupos o ServiceAccounts.
Los permisos se definen como verbos (get, list, create, delete...) sobre recursos (pods, secrets...). Así aplicamos el principio de mínimo privilegio: cada identidad tiene solo los permisos que necesita, ni uno más. Lo profundizaremos en el capítulo de RBAC.
Control de admisión
Los admission controllers interceptan la petición después de autorizarla y antes de guardarla. Pueden validarla (rechazarla si no cumple una norma) o mutarla (modificarla, por ejemplo añadiendo valores por defecto, como hacen los LimitRange).
Ejercicio 1: ¿quién soy y qué puedo hacer?
Pregúntale a Kubernetes con qué identidad estás trabajando:
kubectl auth whoami
Username kubernetes-admin
Groups [kubeadm:cluster-admins system:authenticated]
Ahora, ¿qué puede hacer la ServiceAccount default, la que usan los pods si no indicamos otra? --as permite hacerse pasar por otra identidad:
kubectl auth can-i create pods --as=system:serviceaccount:default:default
kubectl auth can-i --list --as=system:serviceaccount:default:default
no
La lista solo muestra unos pocos permisos básicos, como preguntar por los suyos propios. Por defecto, un pod no puede tocar la API.
Por último, en el control plane, mira qué modos de autorización y qué admission controllers tiene activados el API server:
sudo grep -E "authorization-mode|admission-plugins" /etc/kubernetes/manifests/kube-apiserver.yaml
- --authorization-mode=Node,RBAC
- --enable-admission-plugins=NodeRestriction
Además de los que se activan aquí, hay varios que vienen activados por defecto, entre ellos el de Pod Security que veremos más adelante. En la especialización CKS veremos los admission controllers en profundidad.
Truco de examen:
kubectl auth can-icon--ases la forma más rápida de comprobar un permiso sin tener que crear credenciales.
Security Context
El securityContext define con qué privilegios se ejecuta un contenedor: con qué usuario, si puede escalar privilegios, qué capacidades del kernel tiene o si puede escribir en su sistema de ficheros.
Se puede definir a nivel de pod (afecta a todos sus contenedores) y a nivel de contenedor. Si ambos definen el mismo campo, gana el del contenedor.
Los campos más importantes:
| Campo | Qué hace |
|---|---|
runAsUser / runAsGroup | UID y GID con los que se ejecuta el proceso |
runAsNonRoot | Impide arrancar si el contenedor fuera a ejecutarse como root |
fsGroup | Grupo propietario de los volúmenes montados |
allowPrivilegeEscalation | Impide que el proceso gane más privilegios que su padre (bloquea binarios como sudo) |
readOnlyRootFilesystem | Sistema de ficheros raíz en solo lectura |
capabilities | Añade o quita capacidades del kernel de Linux. Buena práctica: drop: ["ALL"] y añadir solo las imprescindibles |
privileged | Acceso completo al host. Es una de las principales vías de escape de contenedores: evitémoslo siempre que podamos |
seccompProfile | Filtro de llamadas al sistema. RuntimeDefault aplica el perfil por defecto del runtime |
Ejercicio 2: por defecto, root
Lanza un contenedor sin ninguna configuración de seguridad y pregúntale quién es:
kubectl -n seguridad run curioso --rm -i --restart=Never --image=busybox:1.36 -- \
sh -c 'id; touch /fichero && echo "puedo escribir en /"'
uid=0(root) gid=0(root) groups=0(root),10(wheel)
puedo escribir en /
Root y escribiendo donde quiere. Es el punto de partida de casi todas las imágenes.
Ejercicio 3: un pod endurecido
Guarda esto como seguro.yaml:
apiVersion: v1
kind: Pod
metadata:
name: seguro
namespace: seguridad
spec:
securityContext: # a nivel de pod
runAsUser: 1000
runAsGroup: 3000
fsGroup: 2000
volumes:
- name: tmp
emptyDir: {}
containers:
- name: app
image: busybox:1.36
command: ["sleep", "3600"]
securityContext: # a nivel de contenedor
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: tmp # un sitio donde sí se puede escribir
mountPath: /tmp
kubectl apply -f seguro.yaml
Ahora comprueba cada medida por separado:
kubectl -n seguridad exec seguro -- id
uid=1000 gid=3000 groups=2000,3000
El usuario 1000, el grupo 3000 y el grupo extra 2000 del fsGroup.
kubectl -n seguridad exec seguro -- touch /fichero
touch: /fichero: Read-only file system
kubectl -n seguridad exec seguro -- sh -c 'touch /tmp/ok && ls -ln /tmp'
-rw-r--r-- 1 1000 2000 0 Oct 18 16:34 ok
En /tmp sí puede escribir, porque es un volumen, y el fichero pertenece al grupo 2000 del fsGroup.
kubectl -n seguridad exec seguro -- grep Cap /proc/1/status
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 0000000000000000
CapAmb: 0000000000000000
Ceros: ninguna capacidad del kernel.
Ejercicio 4: las capabilities, en acción
Root puede cambiar el propietario de un fichero gracias a la capacidad CHOWN. Compruébalo con y sin capabilities:
kubectl -n seguridad run con-caps --rm -i --restart=Never --image=busybox:1.36 -- \
chown 1000 /etc/hostname
No dice nada: ha funcionado. Ahora el mismo comando quitando todas las capacidades. kubectl run no tiene una opción para el securityContext, así que se lo pasamos con --overrides:
kubectl -n seguridad run sin-caps --rm -i --restart=Never --image=busybox:1.36 \
--overrides='{"spec":{"containers":[{"name":"sin-caps","image":"busybox:1.36","command":["chown","1000","/etc/hostname"],"securityContext":{"capabilities":{"drop":["ALL"]}}}]}}'
chown: /etc/hostname: Operation not permitted
Sigue siendo root, pero un root sin poderes. Esa es la idea de drop: ["ALL"].
Ejercicio 5: cuando la imagen no colabora
runAsNonRoot: true no cambia el usuario: solo comprueba que no sea root. Prueba con la imagen oficial de nginx, que arranca como root:
kubectl -n seguridad run nginx-nonroot --image=nginx:alpine \
--overrides='{"spec":{"containers":[{"name":"nginx-nonroot","image":"nginx:alpine","securityContext":{"runAsNonRoot":true}}]}}'
kubectl -n seguridad get pod nginx-nonroot
NAME READY STATUS RESTARTS AGE
nginx-nonroot 0/1 CreateContainerConfigError 0 15s
¿Por qué? Los eventos lo dicen claro:
kubectl -n seguridad describe pod nginx-nonroot
Error: container has runAsNonRoot and image will run as root
Pregunta: ¿cómo lo arreglarías?
Solución
Hay dos caminos:
- Usar una imagen preparada para no ser root, como
nginxinc/nginx-unprivileged, que escucha en el puerto 8080. - Indicar un usuario con
runAsUser. Con nginx normal no basta: necesita root para el puerto 80 y para escribir en sus directorios. Por eso existe la versión unprivileged.
La lección: el securityContext no hace milagros. La imagen tiene que estar pensada para ejecutarse sin root.
Pod Security Standards y Pod Security Admission
Quizá nos suenen las Pod Security Policies (PSP): permitían definir políticas de seguridad globales para todos los pods. Quedaron obsoletas en Kubernetes 1.21 y fueron eliminadas en la 1.25. Si nos las encontramos en un tutorial, es señal de que está desactualizado.
Su sustituto nativo es Pod Security Admission (PSA), un admission controller integrado que aplica los Pod Security Standards (PSS). Son tres niveles predefinidos:
| Nivel | Para qué | Qué bloquea, entre otras cosas |
|---|---|---|
| privileged | Cargas del sistema que de verdad lo necesitan | Nada |
| baseline | Aplicaciones normales, con mínima fricción | Contenedores privilegiados, hostNetwork, hostPath, añadir capacidades peligrosas |
| restricted | Lo más seguro | Todo lo anterior, y además obliga a runAsNonRoot, allowPrivilegeEscalation: false, drop: ["ALL"] y un seccompProfile |
Se aplica etiquetando el namespace, en tres modos que se pueden combinar:
| Modo | Qué hace con un pod que incumple |
|---|---|
enforce | Lo rechaza |
warn | Lo crea, pero avisa a quien lo crea |
audit | Lo crea, y lo anota en el log de auditoría del clúster |
kubectl label namespace mi-app \
pod-security.kubernetes.io/enforce=baseline \
pod-security.kubernetes.io/warn=restricted \
pod-security.kubernetes.io/audit=restricted
Esta combinación es la forma habitual de endurecer poco a poco: bloqueamos lo grave y nos avisa de lo que nos falta para llegar a restricted.
Ejercicio 6: un namespace estricto
Crea un namespace con el nivel restricted en modo enforce e intenta lanzar un nginx normal:
kubectl create namespace seguridad-psa
kubectl label namespace seguridad-psa pod-security.kubernetes.io/enforce=restricted
kubectl -n seguridad-psa run nginx --image=nginx:alpine
Error from server (Forbidden): pods "nginx" is forbidden: violates PodSecurity "restricted:latest":
allowPrivilegeEscalation != false (container "nginx" must set securityContext.allowPrivilegeEscalation=false),
unrestricted capabilities (container "nginx" must set securityContext.capabilities.drop=["ALL"]),
runAsNonRoot != true (pod or container "nginx" must set securityContext.runAsNonRoot=true),
seccompProfile (pod or container "nginx" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")
Rechazado, con la lista exacta de lo que falta. Es casi una receta.
Ahora cambia a baseline en enforce y deja restricted en warn:
kubectl label namespace seguridad-psa --overwrite \
pod-security.kubernetes.io/enforce=baseline \
pod-security.kubernetes.io/warn=restricted
kubectl -n seguridad-psa run nginx --image=nginx:alpine
Warning: would violate PodSecurity "restricted:latest": allowPrivilegeEscalation != false, ...
pod/nginx created
Creado, pero con aviso. ¿Y un contenedor privilegiado?
kubectl -n seguridad-psa run privi --image=busybox:1.36 \
--overrides='{"spec":{"containers":[{"name":"privi","image":"busybox:1.36","command":["sleep","3600"],"securityContext":{"privileged":true}}]}}'
Error from server (Forbidden): pods "privi" is forbidden: violates PodSecurity "baseline:latest":
privileged (container "privi" must not set securityContext.privileged=true)
Eso ya lo bloquea baseline.
Ejercicio 7: un pod que cumple restricted
Vuelve a poner el namespace en restricted. Escribe un pod de busybox que cumpla ese nivel y compruébalo.
kubectl label namespace seguridad-psa --overwrite pod-security.kubernetes.io/enforce=restricted
Solución
Basta con seguir la lista del error del ejercicio 6:
apiVersion: v1
kind: Pod
metadata:
name: cumple
namespace: seguridad-psa
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: busybox:1.36
command: ["sleep", "3600"]
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
kubectl apply -f cumple.yaml
kubectl -n seguridad-psa get pod cumple
Arranca sin errores. Fíjate en que, al cambiar la etiqueta del namespace, Kubernetes avisa de los pods que ya existían y no cumplen (el nginx de antes): no los borra, pero te lo dice.
Truco antes de endurecer un namespace en producción:
--dry-run=serverte dice qué pods existentes incumplirían el nivel, sin cambiar nada:kubectl label --dry-run=server --overwrite namespace mi-app \pod-security.kubernetes.io/enforce=restricted
Para necesidades más avanzadas (políticas a medida, mutación), se usan motores de políticas como OPA Gatekeeper o Kyverno, que veremos en la especialización CKS.
Limpiar el laboratorio
kubectl delete namespace seguridad seguridad-psa
Retos para practicar
Sin solución: con lo visto en el capítulo, deberías poder resolverlos.
- Crea un Deployment de
nginxinc/nginx-unprivileged:alpineque arranque en un namespace conrestricteden modoenforce, con el sistema de ficheros de solo lectura. Pista: nginx necesita escribir en algún sitio. - Averigua qué puede hacer en el clúster la ServiceAccount de CoreDNS (
kube-system:coredns) conkubectl auth can-i --list.
Resumen
- Toda petición a la API pasa por autenticación, autorización (RBAC) y control de admisión.
kubectl auth can-i --ascomprueba permisos sin crear credenciales. - El securityContext limita los privilegios de cada contenedor: usuario no root, sin escalada de privilegios, sin capacidades y sistema de ficheros en solo lectura. Pero la imagen tiene que colaborar.
- Las Pod Security Policies ya no existen. El mecanismo actual es Pod Security Admission, por namespace:
enforcepara bloquear,warnyauditpara avisar.baselineenenforceyrestrictedenwarnes un buen punto de partida.
En el siguiente capítulo cerramos la red con las Network Policies. Después profundizaremos en usuarios y service accounts y en roles y permisos RBAC.
- Lista de vídeos en Youtube: Curso Kubernetes