Endurece tu Dockerfile: los anti-patrones que delatan un contenedor inseguro
Un contenedor que arranca no es un contenedor seguro. La mayoría de los Dockerfiles pasan CI, sirven la app… y a la vez corren como root, arrastran secretos en sus capas y confían en imágenes que cambian bajo tus pies. Estos son los anti-patrones que revisa un linter, por qué importan y cómo arreglarlos.
1. Correr como root (el más común)
Por defecto, un contenedor corre como root. Si un atacante logra ejecución dentro del contenedor y luego encuentra un escape (o un volumen montado), hereda root en el host. La defensa es una línea:
RUN adduser --system --no-create-home app
USER app
Declara USER hacia el final, tras instalar dependencias (que sí necesitan privilegios).
2. Imagen base sin fijar (:latest o sin etiqueta)
FROM node o FROM node:latest significan «lo que haya hoy». Mañana el build es distinto y no sabes qué cambió: reproducibilidad rota y superficie que se mueve sola. Fija una etiqueta concreta —o mejor, un digest inmutable:
FROM node:20.11-alpine
# o, a prueba de balas:
FROM node:20.11-alpine@sha256:e4b2f...
3. Secretos en ENV / ARG
Un token en ENV DB_PASSWORD=... no «desaparece» en tiempo de ejecución: queda grabado en las capas de la imagen y en su historial. Cualquiera con la imagen lo extrae con docker history. No pongas secretos en el Dockerfile: usa build secrets (--secret) o inyéctalos en runtime.
4. curl | sh y ADD remoto
RUN curl https://get.ejemplo.com | sh ejecuta código remoto sin verificarlo: si comprometen ese servidor, comprometen tu build. Igual con ADD https://…, que descarga y confía. Descarga, verifica el checksum y luego ejecuta; y usa COPY (no ADD) para ficheros locales.
5. Imagen gorda = más superficie
apt-get install sin --no-install-recommends arrastra paquetes de más (más CVEs que parchear). Instala lo justo y limpia en la misma capa:
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
6. Sin HEALTHCHECK
Sin healthcheck, el orquestador no sabe si tu app está viva o colgada. Un contenedor «arriba» pero roto sigue recibiendo tráfico. Añade uno.
docker-compose o manifiesto de Kubernetes en el linter de Dockerfile / IaC y te da una nota A–F con estos anti-patrones marcados. Todo en tu navegador; la config no se envía a ningún sitio.Y al desplegar: compose y Kubernetes
El endurecimiento no acaba en el Dockerfile. En docker-compose y K8s, los sospechosos habituales son:
privileged: true: acceso casi total al host. Es root en la máquina; quítalo salvo que sea imprescindible.hostPath/hostNetwork: montar rutas del host o compartir su red rompe el aislamiento.allowPrivilegeEscalation: true: permite ganar privilegios por setuid. Ponlo enfalse.- Sin
resources.limits: un pod puede agotar el nodo (DoS por «vecino ruidoso»). - Sin
runAsNonRoot: true: el mismo problema del punto 1, pero a nivel de orquestador.
Un Dockerfile decente, de referencia
FROM node:20.11-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY . .
USER node
HEALTHCHECK CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "server.js"]
Base fijada, sin secretos, usuario no root, healthcheck y solo dependencias de producción. Nada exótico: son cinco hábitos.
Checklist
- ✅
USERno privilegiado; nada corre como root. - ✅ Imagen base fijada por etiqueta o digest, nunca
:latest. - ✅ Cero secretos en
ENV/ARG: build secrets o runtime. - ✅ Nada de
curl|shniADDremoto sin checksum. - ✅ Instala lo justo y limpia la caché de paquetes.
- ✅
HEALTHCHECKdefinido. - ✅ En K8s: sin
privileged, conrunAsNonRootylimits.
Un buen Dockerfile no es más largo: es más aburrido. Y aburrido, en seguridad, es exactamente lo que quieres.