Saltar al contenido principal

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:

CapaQué protegeEjemplo
CloudLa infraestructuraFirewalls del proveedor, acceso a las máquinas
ClusterKubernetesRBAC, Pod Security (este capítulo), NetworkPolicies (el siguiente)
ContainerLas imágenes y su ejecuciónUsuario no root, imágenes mínimas
CodeNuestra aplicaciónDependencias, 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:

  1. Autenticación: ¿quién eres? Es enseñar el DNI.
  2. Autorización: ¿puedes hacer esto? Es la tarjeta de embarque: estás identificado, pero solo puedes subir a tu avión.
  3. 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.

Diagrama de autenticación, autorización y control de admisión

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-i con --as es 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:

CampoQué hace
runAsUser / runAsGroupUID y GID con los que se ejecuta el proceso
runAsNonRootImpide arrancar si el contenedor fuera a ejecutarse como root
fsGroupGrupo propietario de los volúmenes montados
allowPrivilegeEscalationImpide que el proceso gane más privilegios que su padre (bloquea binarios como sudo)
readOnlyRootFilesystemSistema de ficheros raíz en solo lectura
capabilitiesAñade o quita capacidades del kernel de Linux. Buena práctica: drop: ["ALL"] y añadir solo las imprescindibles
privilegedAcceso completo al host. Es una de las principales vías de escape de contenedores: evitémoslo siempre que podamos
seccompProfileFiltro 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:

NivelPara quéQué bloquea, entre otras cosas
privilegedCargas del sistema que de verdad lo necesitanNada
baselineAplicaciones normales, con mínima fricciónContenedores privilegiados, hostNetwork, hostPath, añadir capacidades peligrosas
restrictedLo más seguroTodo 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:

ModoQué hace con un pod que incumple
enforceLo rechaza
warnLo crea, pero avisa a quien lo crea
auditLo 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=server te 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.

  1. Crea un Deployment de nginxinc/nginx-unprivileged:alpine que arranque en un namespace con restricted en modo enforce, con el sistema de ficheros de solo lectura. Pista: nginx necesita escribir en algún sitio.
  2. Averigua qué puede hacer en el clúster la ServiceAccount de CoreDNS (kube-system:coredns) con kubectl 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 --as comprueba 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: enforce para bloquear, warn y audit para avisar. baseline en enforce y restricted en warn es 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.


Volver al índice