De Docker Compose a Kubernetes: La Realidad Operativa en Producción
El uso de Docker y Docker Compose es idóneo para entornos de desarrollo local y prototipos monomáquina, como vimos al analizar el despliegue de MySQL con Compose. Sin embargo, cuando las organizaciones escalan hacia arquitecturas distribuidas de alta disponibilidad, Docker Compose revela limitaciones estructurales: carece de auto-reparación nativa frente a fallos de hardware del nodo, no ofrece balanceo de carga interno dinámico y no gestiona actualizaciones graduales sin tiempo de inactividad (zero-downtime rolling updates).
Kubernetes (K8s) resuelve estos desafíos orquestando flotas heterogéneas de servidores como un plano de cómputo unificado. No obstante, la complejidad inherente a Kubernetes expande radicalmente la superficie de ataque si se despliega con las configuraciones por defecto:
- *Redes Planas por Defecto (Default-Allow Network):* En Kubernetes, cualquier Pod de cualquier namespace puede comunicarse libremente por TCP/UDP con cualquier otro Pod del clúster, facilitando el movimiento lateral inmediato tras el compromiso de un único servicio.
- *Contenedores con Privilegios Excesivos (Privileged Pods):* Despliegues que ejecutan procesos como usuario
root(UID 0), con el sistema de archivos en modo lectura/escritura o con capacidades de Linux no restringidas (CAPSYSADMIN), permitiendo el escape del contenedor hacia el kernel del nodo host, según el modelo de aislamiento analizado en qué es Docker. - Tokens de ServiceAccount Expuestos: Por defecto, Kubernetes monta un token JWT con la identidad del ServiceAccount en
/var/run/secrets/kubernetes.io/serviceaccount/tokendentro de cada contenedor. Si un atacante compromete un Pod y el ServiceAccount tiene permisos amplios en la API, puede escalar privilegios en todo el clúster. - Acceso No Restringido al API Server: Servidores API expuestos públicamente sin autenticación OIDC estricta o con configuraciones RBAC laxas donde usuarios y bots reciben el rol
cluster-admin.
Asegurar Kubernetes requiere una estrategia de defensa en profundidad estructurada en capas concéntricas: desde el control de admisión de la API hasta la monitorización eBPF en tiempo de ejecución.
Arquitectura de Defensa en Profundidad en Kubernetes
La seguridad en Kubernetes no depende de una única barrera, sino de controles interconectados que aíslan la identidad, el tráfico de red y la ejecución del kernel:
graph TD
subgraph Control_Plane ["🛡️ Plano de Control (API Server)"]
User["Desarrollador / CI/CD"]
OIDC["Autenticación OIDC / IAM"]
RBAC["Autorización RBAC (Mínimo Privilegio)"]
PSA["Pod Security Admission (Enforce: Restricted)"]
end
subgraph Data_Plane ["⚙️ Plano de Datos (Worker Node)"]
subgraph CNI_Layer ["Filtrado de Red CNI (NetworkPolicy)"]
NetPol["Default-Deny Ingress & Egress"]
end
subgraph Pod_Runtime ["Aislamiento de Pod (Hardened Runtime)"]
SecComp["Perfil Seccomp: RuntimeDefault"]
NonRoot["runAsNonRoot: true (UID >= 10000)"]
ReadOnly["readOnlyRootFilesystem: true"]
DropCaps["capabilities.drop: ['ALL']"]
end
subgraph Kernel_Audit ["Auditoría en Tiempo de Ejecución (eBPF)"]
Falco["Falco eBPF: Detección de Shells & Anomalías"]
end
end
User -->|TLS 1.3 + Bearer Token| OIDC
OIDC --> RBAC
RBAC --> PSA
PSA -->|Manifest Validado| CNI_Layer
CNI_Layer -->|Tráfico Whitelist| Pod_Runtime
Pod_Runtime --> Kernel_Audit
Kernel_Audit -.->|Alerta Security Incident| SIEM["SIEM / SOC Alert"]
classDef control fill:#e1f5fe,stroke:#0288d1,stroke-width:2px;
classDef data fill:#e8f5e9,stroke:#388e3c,stroke-width:2px;
classDef runtime fill:#fff3e0,stroke:#f57c00,stroke-width:2px;
classDef audit fill:#ffebee,stroke:#d32f2f,stroke-width:2px;
class User,OIDC,RBAC,PSA control;
class CNI_Layer data;
class SecComp,NonRoot,ReadOnly,DropCaps runtime;
class Falco,SIEM audit;
1. Pod Security Standards (PSS) y Pod Security Admission (PSA)
Tras la depreciación de las complejas PodSecurityPolicies (PSP) en Kubernetes 1.25, el estándar oficial integrado es Pod Security Admission (PSA), que clasifica las cargas de trabajo en tres niveles de seguridad:
- Privileged: Sin restricciones. Destinado exclusivamente a agentes de infraestructura a nivel de sistema (CNI plugins, agentes de almacenamiento CSI).
- Baseline: Previene escaladas de privilegios evidentes con una configuración mínima por defecto.
- Restricted: Aplica las mejores prácticas de hardening más estrictas del estándar CIS Kubernetes Benchmark.
Aplicación de PSS Restricted a Nivel de Namespace
Para asegurar que ningún contenedor vulnerable pueda ser desplegado en el namespace de producción, se etiquetan los namespaces con las directivas de cumplimiento:
apiVersion: v1
kind: Namespace
metadata:
name: production-apps
labels:
# Bloquear el despliegue de cualquier Pod que viole el estándar Restricted
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: latest
# Advertir a los ingenieros si alguna plantilla incumple
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/warn-version: latest
# Registrar auditorías detalladas
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/audit-version: latest
Manifiesto de Pod con Hardening Completo
Bajo el perfil restricted, cada Pod debe definir explícitamente su bloque securityContext:
apiVersion: apps/v1
kind: Deployment
metadata:
name: secure-backend-service
namespace: production-apps
spec:
replicas: 3
selector:
matchLabels:
app: backend-service
template:
metadata:
labels:
app: backend-service
spec:
# Desactivar el montaje del token de ServiceAccount si la app no llama a la API de K8s
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: application
image: ghcr.io/empresa/backend-service:v1.4.2
imagePullPolicy: IfNotPresent
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
limits:
cpu: "1"
memory: "1Gi"
requests:
cpu: "250m"
memory: "512Mi"
volumeMounts:
# Directorio temporal efímero en memoria para escrituras necesarias
- name: tmp-volume
mountPath: /tmp
volumes:
- name: tmp-volume
emptyDir:
medium: Memory
sizeLimit: 64Mi
Parámetros clave de seguridad:
allowPrivilegeEscalation: false: Impide que binarios con el bitsetuideleven privilegios dentro del contenedor.readOnlyRootFilesystem: true: Convierte el sistema de archivos en solo lectura. Si un atacante explota una vulnerabilidad en la aplicación web, no puede descargar herramientas de red (curl,nmap), compilar exploits o modificar binarios del sistema.capabilities.drop: ["ALL"]: Despoja al proceso de cualquier capacidad administrativa de Linux (comoCAPNETRAWpara escanear redes oCAPSYSPTRACEpara inspeccionar memoria de otros procesos).seccompProfile: RuntimeDefault: Activa el filtrado de llamadas al sistema (syscalls) nativo de Linux para bloquear syscalls peligrosas que no sean requeridas por runtimes modernos.
2. Microsegmentación de Red con NetworkPolicies
Por diseño, el modelo de red de Kubernetes es completamente plano: cualquier Pod puede conectarse a cualquier otro Pod. Para implementar una arquitectura Zero Trust, el primer paso en cada namespace debe ser establecer una política de Default-Deny total tanto para tráfico entrante (Ingress) como saliente (Egress).
Política 1: Default-Deny Global en el Namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production-apps
spec:
podSelector: {} # Aplica a todos los pods del namespace
policyTypes:
- Ingress
- Egress
Una vez aplicada esta política, ningún Pod del namespace puede comunicarse con el exterior ni recibir peticiones, neutralizando intentos de escaneo lateral.
Política 2: Whitelist Granular para el Microservicio Backend
A continuación, se autoriza explícitamente únicamente el tráfico estrictamente necesario:
- Ingress: Tráfico TCP en el puerto 8080 procedente exclusivamente de pods con la etiqueta
role: ingress-controllerubicados en el namespaceingress-nginx. - Egress: Tráfico hacia el servidor DNS interno de Kubernetes (
kube-dnsen puerto 53 UDP/TCP) y hacia la base de datos PostgreSQL privada en el puerto 5432:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: backend-service-policy
namespace: production-apps
spec:
podSelector:
matchLabels:
app: backend-service
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
podSelector:
matchLabels:
app.kubernetes.io/name: ingress-nginx
ports:
- protocol: TCP
port: 8080
egress:
# Permitir resolución DNS
- to:
- namespaceSelector: {}
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# Permitir conexión a la base de datos en namespace específico
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: database-tier
podSelector:
matchLabels:
app: postgres-cluster
ports:
- protocol: TCP
port: 5432
Nota Crítica de Implementación: Las
NetworkPoliciessolo son efectivas si el clúster utiliza un CNI (Container Network Interface) con soporte de políticas de red, como Cilium, Calico o AWS VPC CNI con Network Policy Engine activado. El plugin de red básico de Kubernetes no implementa filtrado.
3. RBAC y Workload Identity: Eliminación de Credenciales Estáticas
El control de acceso basado en roles (Role-Based Access Control - RBAC) debe estructurarse mediante roles locales en namespaces (Role) en lugar de roles a nivel de clúster (ClusterRole), restringiendo estrictamente los verbos y recursos autorizados:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: production-apps
name: deployment-operator
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "update", "patch"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
Integración de Workload Identity (AWS EKS Pod Identity / IRSA)
En entornos cloud, los microservicios desplegados en Kubernetes a menudo necesitan interactuar con servicios externos como Amazon S3, DynamoDB o Secrets Manager.
El antipatrón consiste en crear claves de acceso estáticas de AWS (AWSACCESSKEYID y AWSSECRETACCESSKEY) y almacenarlas como Secrets de Kubernetes. Si el clúster es comprometido, las claves se filtran y persisten de forma indefinida.
La solución de seguridad es Workload Identity (EKS Pod Identity o IAM Roles for Service Accounts - IRSA):
- Se asocia un ServiceAccount de Kubernetes con un rol de IAM en AWS mediante un token OIDC emitido por el propio clúster.
- El SDK de AWS dentro del contenedor asume el rol de IAM automáticamente mediante
sts:AssumeRoleWithWebIdentity. - Las credenciales generadas son temporales, se rotan automáticamente cada hora y carecen de persistencia física en disco.
4. Detección de Amenazas en Tiempo de Ejecución con Falco eBPF
Incluso con controles estáticos de admisión y microsegmentación de red, es imprescindible monitorizar la actividad en tiempo de ejecución. Falco es el estándar de facto de la CNCF para detección de anomalías y amenazas de seguridad en Kubernetes, utilizando sondas eBPF (Extended Berkeley Packet Filter) directamente en el kernel de Linux.
Falco intercepta llamadas al sistema en tiempo real y evalúa reglas de comportamiento sospechoso:
- Ejecución de un shell interactivo dentro de un contenedor en producción (
execde/bin/basho/bin/sh). - Modificación de archivos en directorios binarios críticos (
/bin,/sbin,/usr/bin). - Apertura de sockets de red hacia rangos de IPs externas inesperadas.
- Acceso anómalo al token del ServiceAccount.
Ejemplo de regla de Falco para alertar de la ejecución de una terminal interactiva en un Pod:
- rule: Terminal shell in container
desc: A shell was spawned inside a container with an attached terminal
condition: >
spawned_process and container
and shell_procs and proc.tty != 0
and container.name != "debug-sidecar"
output: >
Alerta Crítica: Terminal abierta en contenedor
(user=%user.name pod=%k8s.pod.name ns=%k8s.ns.name
cmd=%proc.cmdline image=%container.image.repository)
priority: WARNING
tags: [container, shell, mitre_execution]
La señal generada por Falco se exporta en formato JSON hacia el SIEM corporativo o canales de respuesta ante incidentes, permitiendo al equipo de operaciones revocar Pods comprometidos en cuestión de segundos.
Conclusión y Checklist de Hardening para Clústeres Kubernetes
La seguridad en Kubernetes no es un estado binario, sino un proceso continuo de reducción de privilegios y observabilidad activa:
- [x] Pod Security Admission configurado en modo
enforce: restricteden todos los namespaces de producción. - [x] Contenedores configurados con
readOnlyRootFilesystem: true,runAsNonRoot: trueycapabilities.drop: ["ALL"]. - [x] Montaje automático de tokens desactivado (
automountServiceAccountToken: false) en Pods que no gestionan la API. - [x] Microsegmentación de red implementada con NetworkPolicies aplicando
default-deny-allpor defecto. - [x] Eliminación de credenciales estáticas mediante Workload Identity / EKS Pod Identity federado con IAM.
- [x] CNI con soporte de filtrado stateful (Cilium o Calico) verificado en el clúster.
- [x] Detección en tiempo de ejecución desplegada con Falco eBPF para alertar de intrusiones y shells no autorizadas.
Si necesitas auditar la arquitectura cloud de tu organización, implementar bastiones Zero Trust o securizar clústeres de contenedores y cargas de trabajo en Kubernetes, puedes consultar mis Servicios de Ciberseguridad y Consultoría o ponerte en contacto conmigo a través de Sobre mí.