EKS seguro
Identidad por pod con Pod Identity e IRSA, RBAC mínimo, Pod Security Standards, network policies, control de admisión con Kyverno y GuardDuty EKS Protection.
Mover la API de Citas de ECS Fargate a EKS te da flexibilidad, pero añade un plano de control entero —el de Kubernetes— con su propia identidad, su propia autorización y sus propias formas de escalar privilegios. Un clúster de EKS tiene dos capas de seguridad que deben encajar: la de Kubernetes (RBAC, admisión, aislamiento de pods) y la de AWS (qué permisos IAM recibe cada pod). Si cualquiera de las dos es laxa, comprometer un pod puede convertirse en comprometer la cuenta. Este es el recorrido para endurecer ambas.
Identidad: credenciales por pod, no por nodo#
El error más caro en EKS es dejar que los pods usen el rol del nodo (el instance profile de la EC2). Ese rol suele tener permisos para la VPC CNI, ECR, logs… y, si un pod puede llegar al IMDS del nodo, hereda todos esos permisos. Un solo pod comprometido pasa a hablar con la API de AWS como el nodo entero. La solución es dar a cada pod su propia identidad, acotada a lo que ese servicio necesita.
Hay dos mecanismos, y conviene saber cuándo usar cada uno:
- IRSAIRSAMecanismo de EKS que federa un service account de Kubernetes con un rol IAM mediante un proveedor OIDC, dando credenciales temporales por pod. (IAM Roles for Service Accounts): federa un service account con un
rol IAM a través de un proveedor OIDC por clúster. Anotas el service account con el ARN del
rol y los SDK asumen el rol vía
sts:AssumeRoleWithWebIdentity. Es la opción válida en Fargate. - EKS Pod IdentityEKS Pod IdentityMétodo de EKS que asocia un rol IAM a un service account sin proveedor OIDC; un agente entrega credenciales por pod a través de la API de EKS Auth.: más reciente y simple. No usa OIDC: defines
una asociación rol ↔ service account y añades el principal
pods.eks.amazonaws.coma la política de confianza del rol. Un agente (DaemonSet) entrega las credenciales por nodo. Un mismo rol se reutiliza entre clústeres sin tocar su política de confianza.
apiVersion: v1kind: ServiceAccountmetadata: name: citas-api namespace: citas annotations: # La política de confianza del rol condiciona el 'sub' a este SA+namespace eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/citas-apiaws eks create-pod-identity-association \ --cluster-name citas-prod \ --namespace citas \ --service-account citas-api \ --role-arn arn:aws:iam::123456789012:role/citas-api# La política de confianza del rol confía en pods.eks.amazonaws.comEn ambos casos, el rol citas-api solo concede lo que la API necesita (leer su secreto en Secrets Manager, su cola SQS), siguiendo la lógica de evaluación de IAM. Y para cerrar el círculo, restringe el acceso al IMDS del nodo (HttpPutResponseHopLimit = 1) para que un pod no pueda robar el rol del nodo aunque lo intente.
Admisión: la política que decide qué entra#
Antes de que un pod llegue a etcd, el API server pasa la petición por autenticación, por RBAC y, por último, por el control de admisiónControl de admisiónFase de la API de Kubernetes que intercepta cada objeto tras autenticarlo y autorizarlo, para validarlo o mutarlo antes de persistirlo en etcd.. Ese último filtro es donde impones tus reglas de seguridad sobre todo lo que entra al clúster, venga del pipeline o de un kubectl apply manual.
Paso 1 de 5Despliegue
Un pipeline (o un kubectl apply) manda dos manifiestos al API server de EKS. Primero pasan por autenticación y por RBAC: ¿puede este sujeto crear pods en ese namespace?
RBAC con mínimo privilegio#
El RBACRBAC de KubernetesModelo de autorización de Kubernetes que concede permisos sobre la API uniendo sujetos (usuarios, grupos, service accounts) a Roles mediante RoleBindings. de Kubernetes es puramente aditivo: no hay reglas de denegación, así que el mínimo privilegio se consigue concediendo poco. Evita cluster-admin por comodidad y no montes el service account default. Un rol típico para la API de Citas no necesita casi nada del plano de control:
apiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata: namespace: citas name: citas-apirules: - apiGroups: [''] resources: ['configmaps'] verbs: ['get', 'list'] # solo lectura, solo su namespacePod Security Standards#
Los Pod Security StandardsPod Security StandardsTres perfiles de Kubernetes (privileged, baseline, restricted) que limitan lo que puede pedir un pod; se aplican por namespace con Pod Security Admission. definen tres perfiles —privileged, baseline, restricted— que se aplican por namespace con Pod Security Admission (integrado y estable desde Kubernetes 1.25). Son el suelo de seguridad barato: una etiqueta en el namespace rechaza pods privilegiados sin instalar nada.
apiVersion: v1kind: Namespacemetadata: name: citas labels: pod-security.kubernetes.io/enforce: restricted pod-security.kubernetes.io/enforce-version: latest pod-security.kubernetes.io/warn: restrictedEl perfil restricted obliga a runAsNonRoot, allowPrivilegeEscalation: false, seccompProfile: RuntimeDefault y capabilities drop: ALL. Es exactamente lo que ya preparaste al endurecer la imagen en contenedores y ECR.
Políticas a medida con Kyverno u OPA Gatekeeper#
PSA solo tiene tres plantillas fijas. Para reglas propias —exigir que las imágenes vengan de tu ECR, que estén firmadas, que lleven límites de recursos— necesitas un controlador de admisión como KyvernoKyvernoMotor de políticas para Kubernetes que valida, muta, genera y verifica firmas de imágenes como controlador de admisión, con políticas escritas en YAML. u OPA Gatekeeper. Kyverno usa políticas en YAML (sin lenguaje aparte como el Rego de Gatekeeper) y puede validar, mutar y verificar firmas de imágenes.
apiVersion: kyverno.io/v1kind: ClusterPolicymetadata: name: solo-imagenes-ecr-confiablesspec: rules: - name: exigir-registro-ecr match: any: - resources: kinds: ['Pod'] validate: # En Kyverno reciente la acción se fija por regla (Audit | Enforce). # El antiguo spec.validationFailureAction quedó obsoleto en 1.13. failureAction: Enforce message: 'Solo se permiten imágenes de nuestro ECR.' pattern: spec: containers: - image: '123456789012.dkr.ecr.eu-west-1.amazonaws.com/*'Red: nada de “todos hablan con todos”#
Por defecto, en Kubernetes cualquier pod puede conectarse a cualquier otro. Es la cáscara de huevo aplicada a la red del clúster: basta comprometer un pod para escanear y alcanzar la base de datos o el plano de control. Una NetworkPolicy de default-deny por namespace, y luego permisos explícitos, aplica zero trust dentro del clúster. (En EKS necesitas un CNI que las implemente: el VPC CNI ya soporta network policies, o usa Calico/Cilium.)
apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: default-deny namespace: citasspec: podSelector: {} # todos los pods del namespace policyTypes: [Ingress, Egress] # sin reglas = se deniega todoDetección: GuardDuty EKS Protection#
Prevenir no basta; necesitas ver qué pasa dentro. GuardDuty EKS Protection aporta dos capas:
- EKS Audit Log Monitoring: analiza los logs de auditoría del API server (sin que tengas que habilitarlos ni gestionarlos) y detecta patrones sospechosos: uso de credenciales anónimas, peticiones desde IPs maliciosas conocidas, intentos de escalada de privilegios.
- EKS Runtime Monitoring: con un agente, observa el comportamiento a nivel de sistema operativo en los nodos (procesos, conexiones, acceso a ficheros) y detecta actividad propia de un contenedor comprometido, como la ejecución de una reverse shell o el minado de criptomonedas.
Sus hallazgos llegan a Security Hub y a EventBridge, igual que los del resto de la cuenta; encaja con lo que ves en GuardDuty y Detective. Es la capa que te avisa cuando la prevención ha fallado.
Tu turno
Endurece un namespace de tu clúster de prácticas
En un clúster de EKS (o kind/minikube para la parte de Kubernetes) de tu propio lab:
- Aplica
pod-security.kubernetes.io/enforce: restricteda un namespace e intenta desplegar un pod conprivileged: true. ¿Qué mensaje devuelve el API server? - Crea un service account con un
Rolede solo lectura y comprueba, conkubectl auth can-i, que no puede listar secrets suplantándolo (--as=system:serviceaccount:NAMESPACE:SA). - Si tienes EKS: asocia ese service account a un rol IAM con Pod Identity o IRSA y verifica desde el pod (
aws sts get-caller-identity) que la identidad es la del rol del pod y no la del nodo. - Aplica una
NetworkPolicydefault-deny y observa qué se rompe. Eso te dice qué comunicaciones estabas permitiendo sin saberlo.
Reto
Pones RBAC estricto y Pod Security Standards restricted. ¿Hace falta algo más para que un pod no toque toda tu cuenta de AWS?
▸Mostrar solución
Sí: la identidad de AWS. RBAC y PSS son controles de Kubernetes; no limitan qué permisos IAM
recibe el pod. Si el pod usa el rol del nodo (o un rol con *), un atacante dentro del pod habla
con la API de AWS con esos permisos. Dale a cada pod su rol acotado con Pod Identity o IRSA y
restringe el IMDS del nodo.
Reto
¿Por qué no basta con Pod Security Admission para exigir que las imágenes estén firmadas y vengan de tu ECR?
▸Mostrar solución
Porque PSA solo conoce tres plantillas fijas (privileged, baseline, restricted) centradas en
el securityContext del pod. No sabe nada de registros ni de firmas. Esa clase de regla
—procedencia e integridad de la imagen— es trabajo de un controlador de admisión con políticas
propias, como Kyverno (verifyImages) u OPA Gatekeeper.
En resumen#
- Da a cada pod su propia identidad IAM con Pod Identity (o IRSA en Fargate); nunca dejes que use el rol del nodo, y restringe el IMDS.
- RBAC es aditivo: concede poco, evita
cluster-adminy vigila los verbos que escalan (secrets,pods/exec,bind/escalate). - Pon el suelo con Pod Security Standards
restricted(PSA, estable desde 1.25) y añade reglas propias con Kyverno u OPA Gatekeeper en el control de admisión. - Aplica NetworkPolicies default-deny: en un clúster, por defecto todos los pods se hablan.
- Detecta con GuardDuty EKS Protection (audit logs + runtime monitoring): la capa que avisa cuando la prevención falla.