← Blog

Mejores prácticas de seguridad en Dockerfile

Mejores prácticas de seguridad en Dockerfile: fija imágenes base confiables, ejecuta como non-root, usa multi-stage, deja los secretos fuera y escanea en CI.

Bruno Baldo·Oct 5, 2026·Actualizado el Sep 14, 2026·10 min de lectura·Revisado por Rainforest Technologies
Un Dockerfile es un archivo pequeño con un radio de impacto desproporcionado. Esas pocas docenas de líneas deciden qué código, qué paquetes del sistema operativo y qué privilegios terminan ejecutándose en producción, a menudo en miles de instancias de contenedor. Por eso la seguridad en Dockerfile merece el mismo cuidado que le das al código de la aplicación: una imagen base descuidada, un COPY . . extraviado o un secreto pasado por ARG pueden entregar en silencio a un atacante un punto de apoyo que ninguna cantidad de herramientas de runtime deshará por completo. Acertar con el Dockerfile es el lugar más barato y más temprano para fortalecer un contenedor. Lo alentador es que un Dockerfile seguro es, en gran medida, una cuestión de hábitos bien comprendidos y no de herramientas exóticas. Este análisis a fondo recorre las prácticas que más importan para el endurecimiento de contenedores: elegir imágenes base mínimas y confiables, ejecutar como un usuario sin privilegios, usar builds multi-stage para reducir la superficie de ataque, mantener los secretos fuera de las layers, minimizar lo que instalas, copiar solo lo necesario, fijar versiones, agregar un healthcheck, restringir el runtime y escanear cada image antes de distribuirla. Forma parte de nuestra guía más amplia de Docker container security, que conecta estas mejores prácticas de Dockerfile a nivel de la image con las preocupaciones de registro y de runtime. Parte de una imagen base mínima y confiable Cada línea de tu Dockerfile se apoya en la imagen base, así que la imagen base es tu mayor riesgo heredado. Una image completa de ubuntu o node trae un shell, un gestor de paquetes y decenas de bibliotecas que nunca vas a invocar, pero que un atacante sí podría. Prefiere una variante mínima, una tag -slim o, mejor aún, una image distroless que contenga únicamente el runtime de tu lenguaje y sus dependencias, sin shell y sin gestor de paquetes del que abusar. Igual de importante: sé exacto sobre lo que estás descargando. Usa imágenes oficiales o de publishers verificados y fíjalas por digest en lugar de una tag móvil. Una tag como :latest o incluso :20 puede apuntar mañana a un contenido distinto del de hoy, lo que rompe en silencio la reproducibilidad y permite que un cambio upstream se cuele sin revisión. # Fragile: the tag can move under you FROM node:latest # Better: a slim, digest-pinned base you can reproduce and audit FROM node:20.11.1-slim@sha256:2b3f1c... Fijar por digest significa que el build es reproducible byte a byte y que cualquier cambio en la imagen base se convierte en un commit deliberado y revisable, en lugar de una sorpresa silenciosa. Ejecuta como un usuario non-root De forma predeterminada, los procesos dentro de un contenedor se ejecutan como root. Si un atacante escapa de la aplicación, root en el contenedor sumado a un bug del kernel o a una mala configuración puede convertirse en root en el host. La mayoría de las cargas de trabajo nunca lo necesitan. Crea un usuario sin privilegios y cambia a él antes de que arranque el proceso principal del contenedor. # Create a dedicated, unprivileged user and own the app directory RUN groupadd --system app && useradd --system --gid app --no-create-home app WORKDIR /app COPY --chown=app:app . . USER app USER app garantiza que el entrypoint se ejecute sin privilegios. Combínalo con la eliminación de las capabilities de Linux que no necesitas, para que incluso los permisos que root normalmente tendría queden despojados. Las capabilities se aplican en runtime, pero el Dockerfile es donde señalas y documentas la intención, y donde un USER non-root hace que el valor predeterminado de mínimo privilegio se mantenga. Dos detalles son fáciles de pasar por alto. Primero, coloca la instrucción USER después de cualquier paso que genuinamente necesite derechos elevados, como instalar paquetes, para que esos pasos sigan funcionando mientras el proceso en ejecución permanece sin privilegios. Segundo, asegúrate de que el usuario sin privilegios sea realmente el dueño de los archivos y directorios en los que debe escribir en runtime; el COPY --chown se encarga de los archivos que copias, y un chown explícito cubre los directorios en los que la aplicación crea datos. Si te saltas eso, el contenedor arranca con el usuario correcto, pero falla en el momento en que intenta escribir, lo que suele empujar a los equipos de vuelta a root por frustración. Usa builds multi-stage Construir software requiere compiladores, headers, paquetes de desarrollo y, a veces, credenciales. Nada de eso pertenece a la image que despliegas. Un build multi-stage te permite hacer el trabajo pesado en una etapa y copiar solo el artefacto final a una etapa final limpia y mínima. # Stage 1: build with the full toolchain FROM golang:1.22 AS build WORKDIR /src COPY . . RUN CGO_ENABLED=0 go build -o /bin/app ./cmd/app # Stage 2: ship only the binary on a distroless runtime FROM gcr.io/distroless/static-debian12@sha256:... COPY --from=build /bin/app /bin/app USER nonroot:nonroot ENTRYPOINT ["/bin/app"] La image de runtime no tiene compilador, ni shell, ni caché de build, así que la superficie de ataque se reduce drásticamente. Los builds multi-stage también son la manera más limpia de asegurarte de que las herramientas de build y los archivos intermedios nunca se filtren a lo que se ejecuta en producción. Mantén los secretos totalmente fuera de la image Este es el error que más duele, porque es invisible en el contenedor en ejecución pero permanente en la image. Los secretos pasados por ARG o ENV, o copiados como archivos, se escriben en las layers de la image, y esas layers viven para siempre en el historial de la image. Borrar un archivo en una layer posterior no lo elimina: cualquiera que pueda descargar la image puede ejecutar docker history o desempacar las layers y leerlo. # Wrong: the token is now baked into a layer for good ARG NPM_TOKEN RUN npm ci En su lugar, usa los secret mounts de BuildKit. El secreto está disponible únicamente durante esa única instrucción RUN y nunca se escribe en una layer. # syntax=docker/dockerfile:1 FROM node:20.11.1-slim@sha256:... WORKDIR /app COPY package*.json ./ RUN --mount=type=secret,id=npm_token \     NPM_TOKEN="$(cat /run/secrets/npm_token)" npm ci Luego lo suministras en el momento del build con docker build --secret id=npm_token,src=./npm_token.txt. La credencial cumple su función y no deja rastro en la image final. Para los secretos de runtime, inyéctalos a través de tu orquestador o de un gestor de secretos, en lugar del Dockerfile, para que nada sensible forme parte jamás del artefacto. Minimiza layers, paquetes y cachés Cada paquete que instalas es más código que puede acarrear una vulnerabilidad, y cada caché sobrante es peso muerto y superficie de ataque adicional. Instala solo lo que necesitas, evita los extras recomendados pero innecesarios y haz la limpieza en el mismo RUN, para que la limpieza caiga en la misma layer. RUN apt-get update \     && apt-get install -y --no-install-recommends ca-certificates curl \     && rm -rf /var/lib/apt/lists/* --no-install-recommends evita que apt arrastre paquetes opcionales, y eliminar las listas de paquetes en la misma layer impide que inflen la image. Menos paquetes significan menos CVE que clasificar y una superficie menor que defender. Copia solo lo necesario y usa un .dockerignore COPY . . es cómodo y peligroso. Barre alegremente todo tu directorio de trabajo hacia la image, incluyendo el historial .git, archivos .env locales, credenciales y artefactos de CI. Copia rutas específicas y respáldalo con un .dockerignore, para que los archivos sensibles nunca entren en el contexto de build en primer lugar. # Copy dependency manifests first, then only the source you need COPY package*.json ./ RUN npm ci COPY src ./src # .dockerignore .git .env node_modules *.log **/secrets/* Un buen .dockerignore también acelera los builds al mantener el contexto pequeño, así que se paga a sí mismo dos veces. Fija las versiones de dependencias y paquetes La reproducibilidad es una propiedad de seguridad. Si un build puede descargar una versión distinta de una dependencia o de un paquete del SO en cada ejecución, no puedes razonar sobre lo que hay realmente en la image, y una release upstream comprometida puede colarse sin ser notada. Fija las dependencias de la aplicación mediante un lockfile versionado e instala a partir de él, y fija los paquetes del SO a versiones conocidas donde tu distribución base lo permita. # Install from the committed lockfile, not "whatever is latest" COPY package.json package-lock.json ./ RUN npm ci npm ci (y sus equivalentes en otros ecosistemas) instala exactamente lo que el lockfile especifica y falla si ambos difieren, que es precisamente el comportamiento que quieres en un pipeline de build. Agrega un healthcheck y restringe el runtime Un HEALTHCHECK le da a tu orquestador una señal confiable de cuándo un contenedor está genuinamente atendiendo, y no solo iniciado, lo que lo ayuda a reemplazar con prontitud las instancias comprometidas o atascadas. HEALTHCHECK --interval=30s --timeout=3s --retries=3 \     CMD ["/bin/app", "healthcheck"] El Dockerfile también prepara el terreno para un runtime restringido. Diseña la image de modo que pueda ejecutarse con un sistema de archivos raíz de solo lectura, escribiendo únicamente en volúmenes montados explícitamente o en tmpfs, y de modo que no necesite binarios setuid ni setgid. Los binarios setuid y setgid permiten que un programa se ejecute con los privilegios del dueño del archivo sin importar quién lo invoque, que es exactamente el tipo de vía de escalada latente que no quieres tener alojada en un contenedor. Puedes encontrar y eliminar esos bits durante el build y, después, imponer el sistema de archivos raíz de solo lectura y la eliminación de capabilities cuando ejecutes el contenedor. # Remove setuid/setgid bits from binaries you do not need them on RUN find / -xdev -perm /6000 -type f -exec chmod a-s {} + || true Una image construida para ejecutarse en solo lectura y sin privilegios, sin vías de escalada por setuid, le da a un atacante mucho menos espacio para persistir, manipular el sistema de archivos o escalar privilegios. El Dockerfile no puede activar por sí mismo la bandera de solo lectura, eso es una configuración de runtime, pero construir la image para tolerarla es lo que hace indoloro activarla después. Verifica, escanea y etiqueta en CI La última salvaguarda es la automatización. Un Dockerfile que se veía bien el trimestre pasado puede estar plagado de CVE recién divulgadas hoy, porque se descubren vulnerabilidades en dependencias que ya distribuiste. Escanea cada image en cada build en CI, haz que el pipeline falle ante hallazgos de alta severidad y agrega labels de procedencia para que puedas rastrear cualquier image hasta su origen. LABEL org.opencontainers.image.source="https://example.com/org/repo" \       org.opencontainers.image.revision="$GIT_SHA" El escaneo automatizado es lo que convierte todas las prácticas anteriores de buenas intenciones en un estándar obligatorio. Detecta la imagen base vulnerable que nadie notó, el secreto que se coló en una layer y el paquete que adquirió una CVE crítica desde la última release. Los valores predeterminados inseguros y las fallas de tipo inyección siguen siendo un tema recurrente en el OWASP Top 10 (2025), y las imágenes de contenedor son uno de los lugares donde esas debilidades se acumulan en silencio. Cómo ayuda Rainforest Rainforest lleva tus imágenes de contenedor al mismo flujo de seguridad que el resto de tu código. Nuestro container security scanning analiza las imágenes en busca de imágenes base vulnerables y desactualizadas, secretos expuestos incrustados en layers, configuración riesgosa y valores predeterminados inseguros, y luego expone los hallazgos con la severidad y el contexto que tu equipo necesita para actuar. Como se ejecuta como parte de la más amplia plataforma de application security testing, el mismo escaneo se dispara en cada build, así que una imagen riesgosa se señala antes de llegar a un registro, y no después de un incidente. La recompensa son imágenes en las que realmente puedes confiar: bases mínimas y fijadas, non-root de forma predeterminada, sin secretos en las layers y una compuerta de CI que mantiene honesto cada build. ¿Quieres verlo contra tus propias imágenes? Agenda una demo y lo recorreremos con tu stack.

Preguntas frecuentes

¿Qué hace que un Dockerfile sea inseguro?

Los culpables habituales son una imagen base inflada o no confiable, ejecutar como root, secretos pasados por ARG, ENV o archivos copiados (que persisten en las layers de la image), un COPY . . amplio que barre el .git y el .env, imágenes base y dependencias sin fijar y saltarse el escaneo de imágenes en CI. Cada uno de ellos o bien amplía la superficie de ataque o bien deja datos sensibles en la image. Seguir las mejores prácticas de seguridad en Dockerfile, bases mínimas y fijadas, un usuario non-root, builds multi-stage, secretos de BuildKit y escaneo en CI, elimina la mayor parte de ese riesgo.

¿Debo usar :latest o fijar digests de imagen?

Fija digests. Una tag como :latest es un puntero móvil que puede resolverse a contenidos distintos con el tiempo, lo que rompe la reproducibilidad y permite que cambios upstream entren en tu build sin revisión. Fijar por digest (image@sha256:...) hace que los builds sean reproducibles byte a byte y convierte cualquier cambio en la imagen base en un commit deliberado y revisable. Usa una tag de versión legible para humanos si quieres, pero anclada a un digest.

¿Por qué los contenedores no deben ejecutarse como root?

Porque root dentro del contenedor está a un bug de distancia de root en el host. Si un atacante compromete la aplicación, ejecutar como root le da el apalancamiento para explotar una falla del kernel o una mala configuración y escapar del contenedor. La mayoría de las cargas de trabajo nunca necesitan root, así que crea un USER dedicado y sin privilegios, usa COPY --chown para darle los archivos que necesita y elimina las capabilities de Linux para imponer el mínimo privilegio.

¿Cómo mantengo los secretos fuera de una imagen Docker?

Nunca pases secretos por ARG, ENV o archivos de credenciales copiados: se escriben en las layers de la image y siguen siendo legibles en el historial de la image aunque los borres después. Para los secretos de build, usa --mount=type=secret de BuildKit, que expone el valor únicamente durante un solo RUN y nunca lo escribe en una layer. Para los secretos de runtime, inyéctalos a través de tu orquestador o de un gestor de secretos, en lugar de la image. Agrega un .dockerignore para que los archivos de credenciales nunca entren en el contexto de build y escanea las imágenes en CI para detectar filtraciones.

¿Qué es un build multi-stage?

Un build multi-stage usa más de un FROM en un solo Dockerfile. Una etapa de build inicial lleva toda la toolchain, compiladores, gestores de paquetes, dependencias de desarrollo, y hace el trabajo pesado; luego, una etapa final copia solo el artefacto terminado a una image de runtime limpia y mínima. El resultado se distribuye sin herramientas de build ni archivos intermedios, así que la superficie de ataque es mucho menor y los secretos de build nunca llegan a producción.

Bruno Baldo

Escrito por

Bruno Baldo

CMO

Um pouco de marketing e um pouco de curiosidade e temos a receita pra criar um apaixonado por cyber!

Siga leyendo