Contenedores seguros y ECR
Imágenes mínimas, usuario no root y sin secretos en capas, escaneadas en ECR con Inspector y Trivy, con tags inmutables.
La API de Citas corre en ECS Fargate a partir de una imagen que vive en Amazon ECR. Esa imagen es, a la vez, tu artefacto de despliegue y tu mayor superficie de ataque: todo lo que metas en ella —el sistema base, las dependencias, un .env olvidado— viaja a producción y queda expuesto a quien pueda leer una capa o escapar del contenedor. Endurecer la imagen no es cosmética: es reducir drásticamente lo que un atacante encuentra cuando llega. La idea central es simple: mete lo mínimo, córrelo con el mínimo privilegio y verifica continuamente lo que queda.
De una imagen gorda a una imagen mínima#
El punto de partida típico es una imagen construida sobre node:18 o python:3.12 “a secas”: más de 1 GB de sistema operativo completo, compiladores, gestores de paquetes y utilidades (curl, bash, apt) que tu aplicación no necesita en runtime pero que sí le sirven a un atacante para moverse. Cada binario extra es una CVE potencial y una herramienta regalada.
Paso 1 de 6Imagen gorda
La imagen parte de node:18 completo (1,1 GB): arrastra compiladores y librerías que no usas en runtime, corre como root y lleva un .env horneado en una capa. Resultado: 182 CVE y un secreto a un `docker history` de distancia.
Dos técnicas recortan esa superficie:
- Multi-stage builds: compilas en una etapa con todo el toolchain y copias solo el artefacto a una etapa final limpia. El compilador y las dependencias de desarrollo no llegan a la imagen que publicas.
- Bases mínimas:
distroless(sin shell ni gestor de paquetes, solo el runtime) oalpine(musl, unos pocos MB). Menos paquetes significan menos CVEs y menos binarios con los que pivotar. Distroless además complica elkubectl execinteractivo de un atacante: no hay shell que lanzar.
Vulnerable
Seguro
FROM node:18WORKDIR /appCOPY . .RUN npm install# Secreto horneado: queda en una capa para siempreENV DB_PASSWORD=supersecreto-de-prodEXPOSE 3000# Sin USER: el proceso corre como root dentro del contenedorCMD ["node", "server.js"]# 1) Build con todo el toolchainFROM node:18 AS buildWORKDIR /appCOPY package*.json ./RUN npm ci --omit=devCOPY . .
# 2) Imagen final mínima, sin shell ni gestor de paquetesFROM gcr.io/distroless/nodejs18-debian12 AS runtimeWORKDIR /appCOPY --from=build /app /appUSER 10001EXPOSE 3000# El secreto se inyecta en runtime desde Secrets Manager, nunca en la imagenCMD ["server.js"]Usuario no root#
Por defecto, el proceso de un contenedor corre como root dentro de su namespace. Si alguien explota un fallo en tu aplicación y además existe una vía de escape del contenedor (una CVE del runtime, un montaje peligroso, privileged), ser root dentro facilita mucho el salto al host. Declarar un USER sin privilegios es una de las barreras más baratas y efectivas: cumple el mínimo privilegioMínimo privilegioCada identidad (persona, servicio o máquina) tiene solo los permisos imprescindibles para su tarea, durante el tiempo imprescindible. y es requisito del perfil restricted de Kubernetes.
Nada de secretos en las capas#
Un error recurrente es hornear credenciales en la imagen con ENV, ARG o copiando un .env. El problema: las capas son inmutables y públicas para quien tenga la imagen. Borrar el fichero en una capa posterior no lo elimina; sigue en el historial.
docker history --no-trunc citas/api:1.4.2# ENV DB_PASSWORD=supersecreto-de-prod ← visibledocker save citas/api:1.4.2 | tar -x # y cada capa es un tar que se puede leerLa defensa es sacar los secretos de la imagen por completo: inyéctalos en runtime desde Secrets Manager o SSM Parameter Store, montados como variables de entorno o ficheros temporales. La imagen no contiene nada sensible; cada entorno recibe su secreto.
Escaneo: ECR con Inspector y Trivy#
Una imagen mínima sigue teniendo CVEs, y aparecen nuevas cada día sobre imágenes que ya publicaste. Por eso el escaneo es continuo, no un sello de una sola vez. Combina dos puntos de control:
- En el pipeline (shift-left): Trivy escanea la imagen antes del push y rompe el build si hay hallazgos graves. Falla rápido, cerca de quien introdujo la dependencia.
- En ECR (enhanced scanning): el enhanced scanning de ECR está impulsado por Amazon InspectorAmazon InspectorServicio de AWS que escanea de forma continua EC2, imágenes de ECR y funciones Lambda en busca de CVEs y problemas de red., que reescanea las imágenes de forma continua según se publican nuevas CVEs y envía los hallazgos a Security Hub y EventBridge. Una imagen que ayer estaba limpia puede marcarse mañana.
# Rompe el build ante CVEs HIGH o CRITICAL con fix disponibletrivy image \ --severity HIGH,CRITICAL \ --ignore-unfixed \ --exit-code 1 \ 123456789012.dkr.ecr.eu-west-1.amazonaws.com/citas/api:1.4.2resource "aws_ecr_repository" "api" { name = "citas/api" image_tag_mutability = "IMMUTABLE"
image_scanning_configuration { scan_on_push = true }}
# Enhanced scanning (Amazon Inspector) a nivel de registroresource "aws_ecr_registry_scanning_configuration" "this" { scan_type = "ENHANCED"
rule { scan_frequency = "CONTINUOUS_SCAN" repository_filter { filter = "citas/*" filter_type = "WILDCARD" } }}El patrón robusto es un gate: la promoción a producción exige cero CVEs críticas (o altas) explotables. Si Inspector marca una crítica después de desplegar, el gate bloquea la siguiente promoción hasta parchear y reescanear.
Tags inmutables#
Si tu pipeline despliega citas/api:latest o reescribe el tag 1.4.2, pierdes la trazabilidad: la imagen que escaneaste puede no ser la que corre. Con image_tag_mutability = "IMMUTABLE", un tag no se puede reutilizar: cada versión apunta a un digest fijo (sha256:…). Despliega siempre por digest o por tag inmutable, nunca por latest.
Firma y procedencia#
El escaneo dice qué hay dentro; la firma dice de dónde viene. Firmar la imagen (con Cosign/Sigstore o AWS Signer) y verificar la firma en el clúster evita que se despliegue una imagen que no salió de tu pipeline. Es la base para exigir, en el control de admisión, que solo se ejecuten imágenes firmadas y procedentes de tu ECR — lo verás en EKS seguro.
Tu turno
Audita y endurece una de tus imágenes
Elige una imagen de contenedor de tu trabajo o tu lab y mídela antes y después:
docker history --no-trunc <imagen>: ¿hay algún secreto, token o.enven alguna capa?- ¿Corre como root? Añade un
USERsin privilegios y comprueba que la app sigue arrancando. - Pásala a multi-stage con una base
distrolessoalpine. Compara el tamaño condocker images. - Escanéala con
trivy image <imagen>antes y después. ¿Cuántas CVEs HIGH/CRITICAL te has quitado solo con cambiar la base?
Imagen: ...
| Métrica | Antes | Después || ------------------- | ----- | ------- || Tamaño | | || ¿Root? | | || ¿Secretos en capas? | | || CVE HIGH/CRITICAL | | |Reto
Marcas el repositorio de ECR como privado. ¿Ya puedes hornear el secreto de la BD en la imagen?
▸Mostrar solución
No. “Privado” solo controla quién puede hacer pull vía IAM, pero cualquiera con ese permiso (una
tarea, un rol de CI amplio, un atacante que robe esas credenciales) puede descargar la imagen y
leer el secreto de las capas. Un secreto en una imagen es un secreto filtrado; sácalo a Secrets
Manager e inyéctalo en runtime.
Reto
Tu imagen distroless pasó el escaneo hace una semana. ¿Está a salvo?
▸Mostrar solución
Solo en ese momento. Las CVEs se publican a diario sobre software que ya tienes dentro, así que una imagen “limpia” puede volverse vulnerable sin que la toques. Por eso el enhanced scanning de ECR (Inspector) reescanea de forma continua y por eso el gate de promoción se evalúa contra el estado actual, no contra el del día del build.
En resumen#
- La imagen es tu artefacto y tu superficie de ataque: mete lo mínimo con multi-stage y bases distroless/alpine.
- Corre como usuario no root, con raíz de solo lectura y sin escalada de privilegios.
- Nunca hornees secretos: las capas son inmutables y legibles. Inyéctalos en runtime desde Secrets Manager.
- Escanea en dos puntos: Trivy en el pipeline (shift-left) y enhanced scanning de ECR (Amazon Inspector) de forma continua.
- Usa tags inmutables y despliega por digest; un gate bloquea la promoción si hay una CVE crítica.