Cómo proteger contenedores Docker en producción
Técnicas prácticas para endurecer contenedores Docker: análisis de imágenes, mínimo privilegio, gestión de secretos, redes y monitoreo en ejecución.

Ejecutar contenedores Docker en producción introduce una serie de riesgos de seguridad muy particulares. Los contenedores comparten el kernel del host, las imágenes suelen incluir más software del necesario, y una mala configuración puede exponer todo el host. La mayoría de las brechas de seguridad en contenedores explotan errores simples: ejecutar como root, usar imágenes base sin parches o montar el socket de Docker dentro de los contenedores.
Esta guía repasa los pasos prácticos que eliminan las vulnerabilidades más comunes en contenedores. Ninguno de ellos requiere herramientas exóticas — solo disciplina y una buena configuración.
Cómo construir imágenes seguras
La seguridad empieza por la imagen. Cada paquete que incluyas suma superficie de ataque. Una imagen mínima que contenga solo el runtime y el código de tu aplicación es mucho más difícil de explotar que una basada en una distribución completa del sistema operativo.
# ❌ Bloated image with unnecessary attack surface
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y \
python3 python3-pip curl wget vim git openssh-client
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
CMD ["python3", "app.py"]
# Result: 850MB image with SSH client, git, vim, wget
# Every extra binary is a potential exploit vector
# ✅ Multi-stage build with minimal runtime image
FROM python:3.11-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir --target=/deps -r requirements.txt
FROM python:3.11-slim
RUN groupadd -r appuser && useradd -r -g appuser appuser
WORKDIR /app
COPY --from=builder /deps /usr/local/lib/python3.11/site-packages/
COPY --chown=appuser:appuser . .
USER appuser
EXPOSE 8000
CMD ["python3", "app.py"]
# Result: 180MB image, no extra tools, non-root user# Even better: distroless images (no shell, no package manager)
FROM python:3.11-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir --target=/deps -r requirements.txt
FROM gcr.io/distroless/python3-debian12
WORKDIR /app
COPY --from=builder /deps /usr/local/lib/python3.11/site-packages/
COPY . .
USER nonroot
CMD ["app.py"]
# No shell means an attacker who gets code execution
# cannot spawn a reverse shell or run arbitrary commandsAnálisis de imágenes y seguridad de la cadena de suministro
Analiza las imágenes en busca de vulnerabilidades conocidas antes de que lleguen a producción. Integra el análisis en tu pipeline de CI para que ninguna imagen vulnerable llegue a desplegarse.
# GitHub Actions: scan images on every push
name: Container Security Scan
on: [push]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
format: table
exit-code: 1
severity: CRITICAL,HIGH
# Fail the build if CRITICAL or HIGH vulnerabilities found# Local scanning with Trivy
trivy image myapp:latest
# Scan for misconfigurations in Dockerfile
trivy config --severity HIGH,CRITICAL .
# Pin base image digests to prevent supply chain attacks
# Instead of: FROM python:3.11-slim
# Use: FROM python:3.11-slim@sha256:abc123...
# This ensures the exact same base image every build
docker inspect --format='{{index .RepoDigests 0}}' python:3.11-slimEjecutar contenedores con mínimo privilegio
La configuración predeterminada de Docker otorga a los contenedores más privilegios de los que necesitan. Limita cada contenedor al acceso mínimo indispensable.
# docker-compose.yml with security hardening
version: "3.8"
services:
api:
image: myapp:latest
user: "1000:1000" # Run as non-root
read_only: true # Read-only filesystem
tmpfs:
- /tmp:noexec,nosuid,size=100m # Writable tmp with limits
security_opt:
- no-new-privileges:true # Prevent privilege escalation
cap_drop:
- ALL # Drop all Linux capabilities
cap_add:
- NET_BIND_SERVICE # Only add what's needed
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
reservations:
cpus: "0.25"
memory: 128M
networks:
- frontend
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8000/health"]
interval: 30s
timeout: 5s
retries: 3
database:
image: postgres:15-alpine
user: "999:999"
read_only: true
tmpfs:
- /tmp:noexec,nosuid
- /var/run/postgresql:noexec,nosuid
volumes:
- db-data:/var/lib/postgresql/data
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
networks:
- backend # Database only accessible from backend network
networks:
frontend:
backend:
internal: true # No external access
volumes:
db-data:// ❌ Common Docker security mistakes
// Mounting the Docker socket — gives container full host control
// -v /var/run/docker.sock:/var/run/docker.sock
// An attacker can create privileged containers, access host filesystem
// Running as root (the default)
// Any container escape gives root on the host
// Using --privileged flag
// Disables all security features: capabilities, seccomp, AppArmor
// Hardcoded secrets in environment variables
// docker run -e DATABASE_PASSWORD=hunter2 myapp
// ✅ Secure alternatives
const securityChecklist = {
noDockerSocket: "Never mount Docker socket into application containers",
nonRootUser: "Always specify USER in Dockerfile",
noPrivileged: "Never use --privileged; add specific capabilities instead",
secretsManagement: "Use Docker secrets or external vault for credentials",
readOnlyFs: "Use read_only: true with explicit tmpfs for writable paths",
resourceLimits: "Always set CPU and memory limits",
networkSegmentation: "Use separate networks; mark internal where possible",
};Gestión de secretos en contenedores
Los secretos nunca deben incluirse directamente en las imágenes ni pasarse como variables de entorno en texto plano. Los secrets de Docker, los archivos montados o los vaults externos ofrecen un mejor aislamiento.
# Docker Swarm secrets (encrypted at rest and in transit)
version: "3.8"
services:
api:
image: myapp:latest
secrets:
- db_password
- api_key
environment:
DB_PASSWORD_FILE: /run/secrets/db_password
API_KEY_FILE: /run/secrets/api_key
secrets:
db_password:
external: true
api_key:
external: trueimport { readFileSync } from "fs";
// Read secrets from mounted files (works with Docker secrets,
// Kubernetes secrets, and Vault agent injector)
function getSecret(name: string): string {
const filePath = process.env[`${name}_FILE`];
if (filePath) {
return readFileSync(filePath, "utf-8").trim();
}
// Fallback to environment variable for local development
const envValue = process.env[name];
if (envValue) {
return envValue;
}
throw new Error(`Secret ${name} not configured`);
}
const dbPassword = getSecret("DB_PASSWORD");
const apiKey = getSecret("API_KEY");Monitoreo de seguridad en tiempo de ejecución
El análisis de imágenes detecta vulnerabilidades conocidas. El monitoreo en tiempo de ejecución detecta comportamientos inesperados — procesos que no deberían estar corriendo, conexiones de red hacia destinos inesperados o modificaciones del sistema de archivos en contenedores de solo lectura.
# Falco rules for runtime container monitoring
# Falco watches syscalls and alerts on suspicious activity
- rule: Unexpected outbound connection
desc: Detect container connecting to IP not in allowlist
condition: >
evt.type=connect and fd.typechar=4
and container.id != host
and not fd.sip in (allowed_outbound_ips)
output: >
Unexpected outbound connection
(container=%container.name image=%container.image.repository
connection=%fd.name user=%user.name)
priority: WARNING
- rule: Shell spawned in container
desc: Detect shell execution inside a container
condition: >
spawned_process and container
and proc.name in (bash, sh, zsh, dash, csh)
output: >
Shell spawned in container
(container=%container.name shell=%proc.name
parent=%proc.pname user=%user.name)
priority: WARNING# Health check script that verifies container security posture
#!/bin/bash
set -euo pipefail
echo "=== Container Security Audit ==="
# Check if running as root
if [ "$(id -u)" -eq 0 ]; then
echo "FAIL: Running as root"
else
echo "PASS: Running as non-root (uid=$(id -u))"
fi
# Check if filesystem is read-only
if touch /test-write 2>/dev/null; then
rm /test-write
echo "FAIL: Root filesystem is writable"
else
echo "PASS: Root filesystem is read-only"
fi
# Check for unnecessary capabilities
if capsh --print 2>/dev/null | grep -q "cap_sys_admin"; then
echo "FAIL: Container has CAP_SYS_ADMIN"
else
echo "PASS: No dangerous capabilities"
fi
# Check for mounted Docker socket
if [ -e /var/run/docker.sock ]; then
echo "FAIL: Docker socket is mounted"
else
echo "PASS: No Docker socket"
fiPuntos clave
- Usa builds multietapa e imágenes distroless — cada binario de tu imagen es superficie de ataque; incluye solo lo que tu aplicación necesita para ejecutarse
- Nunca ejecutes contenedores como root — agrega un usuario sin privilegios en tu Dockerfile y define
USER; combínalo conno-new-privilegespara evitar la escalada de privilegios - Elimina todas las capabilities y agrégalas de vuelta de forma selectiva —
cap_drop: ALLjunto con entradas específicas decap_addes más seguro que el conjunto de capabilities predeterminado - Analiza las imágenes en CI y bloquea los builds vulnerables — integra Trivy u otro escáner similar en tu pipeline con
exit-code: 1para las vulnerabilidades críticas - Monta los secretos como archivos, no como variables de entorno — las variables de entorno se filtran en logs, listas de procesos y volcados de memoria; los secretos basados en archivos están mejor contenidos
- Monitorea el comportamiento en tiempo de ejecución — el análisis de imágenes encuentra CVEs conocidos, pero no puede detectar zero-days ni comportamientos inesperados; el monitoreo en tiempo de ejecución detecta anomalías en el momento en que ocurren


