Saltar al contenido

Temas

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.

IntermedioEn progreso 5 min de lectura1 oct 2026#contenedores#docker#ecr#imagenes#escaneo#supply-chain#aws

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.

Endurecer una imagen de contenedor
Build inseguroBuild endurecidoECRregistroProdsecreto .envUSER rootnode:18 · 1,1 GB182 CVEsin secretoUSER 10001distroless · 90 MB3 CVEInspector + Trivytag inmutableCVE crítica · 9.8ALLOWDENYbuildpushdeploy

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) o alpine (musl, unos pocos MB). Menos paquetes significan menos CVEs y menos binarios con los que pivotar. Distroless además complica el kubectl exec interactivo de un atacante: no hay shell que lanzar.

Vulnerable

Seguro

Dockerfile
FROM node:18
WORKDIR /app
COPY . .
RUN npm install
# Secreto horneado: queda en una capa para siempre
ENV DB_PASSWORD=supersecreto-de-prod
EXPOSE 3000
# Sin USER: el proceso corre como root dentro del contenedor
CMD ["node", "server.js"]
Dockerfile
# 1) Build con todo el toolchain
FROM node:18 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
# 2) Imagen final mínima, sin shell ni gestor de paquetes
FROM gcr.io/distroless/nodejs18-debian12 AS runtime
WORKDIR /app
COPY --from=build /app /app
USER 10001
EXPOSE 3000
# El secreto se inyecta en runtime desde Secrets Manager, nunca en la imagen
CMD ["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.

el secreto sigue ahí aunque lo 'borres'
docker history --no-trunc citas/api:1.4.2
# ENV DB_PASSWORD=supersecreto-de-prod ← visible
docker save citas/api:1.4.2 | tar -x # y cada capa es un tar que se puede leer

La 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.
paso del pipeline
# Rompe el build ante CVEs HIGH o CRITICAL con fix disponible
trivy image \
--severity HIGH,CRITICAL \
--ignore-unfixed \
--exit-code 1 \
123456789012.dkr.ecr.eu-west-1.amazonaws.com/citas/api:1.4.2
ecr.tf
resource "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 registro
resource "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 .env en alguna capa?
  • ¿Corre como root? Añade un USER sin privilegios y comprueba que la app sigue arrancando.
  • Pásala a multi-stage con una base distroless o alpine. Compara el tamaño con docker images.
  • Escanéala con trivy image <imagen> antes y después. ¿Cuántas CVEs HIGH/CRITICAL te has quitado solo con cambiar la base?
endurecer-imagen.md
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.

Referencias#

Notas relacionadas

ESC
↑↓ para navegar↵ para abriresc para cerrar