El riesgo real: tus secretos viajan dentro de tus imágenes
En 2024, GitGuardian detectó más de 12,8 millones de secretos expuestos en repositorios públicos de GitHub — un aumento del 28% respecto al año anterior según el State of Secrets Sprawl 2025. Las imágenes de Docker Hub son uno de los vectores más subestimados.
Cuando escribes ARG API_KEY en un Dockerfile o pasas -e DB_PASSWORD=hunter2 al iniciar un contenedor, ese secreto no queda confinado al runtime. Puede terminar:
- en las capas de la imagen (inspeccionable con docker history --no-trunc);
- en los metadatos de la imagen exportada a Docker Hub (docker inspect);
- en tu archivo .env confirmado por error durante un git add . apresurado.
Los escáneres automáticos (Trivy, Grype, GitGuardian) recorren Docker Hub continuamente. Un repositorio público con un secreto en texto plano se indexa en minutos. La ventana de exposición es prácticamente nula.
5 errores de configuración que exponen tus secretos
- Variables de entorno en texto plano en
docker-compose.yml—environment: DB_PASSWORD: hunter2es legible por cualquiera que acceda al archivo o la imagen. - Archivo
.envconfirmado en el repositorio — un error en.gitignorey el secreto queda en el historial de git para siempre (incluso después degit rm, accesible víagit log). ARGpasado en el build y copiado en la imagen — losARGquedan grabados en los metadatos de la capa de imagen y son legibles condocker history.- Secretos en los logs — una aplicación que registra sus variables de entorno al arrancar (Spring Boot, Rails en modo debug, algunos servidores Node) imprime las credenciales en
docker logs. - Volúmenes bind-mount en
/rooto el directorio del proyecto — un archivo.envmontado desde el host sigue siendo accesible para cualquier proceso del contenedor con privilegios root.
Requisitos previos
Para seguir este artículo necesitas:
- Un VPS Linux (Debian 12 o Ubuntu 22.04+) con acceso root.
- Docker Engine ≥ 24 y Docker Compose ≥ 2.24 (verifica con docker compose version).
- Para SOPS: age instalado (paquete age en Debian/Ubuntu, o binario desde github.com/FiloSottile/age).
- Para .env.vault: Node.js ≥ 18 y el CLI dotenvx (npm install -g @dotenvx/dotenvx).
No se requiere ningún clúster Swarm para ninguno de los métodos presentados aquí.
Método 1 — Docker Secrets en Compose sin Swarm (paso a paso)
Verificar la versión de Compose
Docker Secrets funciona sin Swarm desde Docker Compose v2.24.0 (lanzado el 11 de enero de 2024). Verifica:
docker compose version # Docker Compose version v2.27.1Si estás por debajo de v2.24, actualiza Compose antes de continuar (
apt-get install docker-compose-pluginen Debian/Ubuntu).Crear los archivos de secretos
Los secretos de Docker Compose en modo no-Swarm son archivos en el host, montados como tmpfs dentro del contenedor. Créalos fuera del directorio del proyecto:
mkdir -p /etc/myapp/secrets echo -n 'contraseña-db-segura' > /etc/myapp/secrets/db_password echo -n 'clave-api-stripe-xxxxx' > /etc/myapp/secrets/stripe_key chmod 600 /etc/myapp/secrets/* chown root:root /etc/myapp/secrets/*La opción
-ndeechoevita el salto de línea final — algunas aplicaciones leen el archivo completo incluido el salto de línea, lo que invalida la clave.Declarar los secretos en docker-compose.yml
services: app: image: miapp:latest secrets: - db_password - stripe_key environment: # indicamos la RUTA, no el valor DB_PASSWORD_FILE: /run/secrets/db_password STRIPE_KEY_FILE: /run/secrets/stripe_key secrets: db_password: file: /etc/myapp/secrets/db_password stripe_key: file: /etc/myapp/secrets/stripe_keyObserva el uso de la convención
*_FILE: tu aplicación debe leer la variableDB_PASSWORD_FILE, abrir el archivo indicado y leer su contenido. Las imágenes oficiales de PostgreSQL, MySQL, Redis y la mayoría de imágenes Bitnami soportan esta convención de forma nativa — consulta la documentación de tu imagen.Verificar el montaje dentro del contenedor
Tras
docker compose up -d, inspecciona el montaje:docker compose exec app ls -la /run/secrets/ # -r-------- 1 root root 20 Sep 20 08:12 db_password # -r-------- 1 root root 28 Sep 20 08:12 stripe_key docker inspect miapp_app_1 | grep -A5 Mounts # "Type": "tmpfs", # "Destination": "/run/secrets",El tipo de montaje es tmpfs: el contenido vive en RAM, nunca escrito en el disco del contenedor. Desaparece al detener el contenedor.
Lo que Docker Secrets NO hace — entiéndelo antes de continuar
Docker Secrets no es una caja fuerte hermética. Lo que no protege:
- El archivo fuente (
/etc/myapp/secrets/db_password) permanece en el disco del host en texto plano — root en la VM siempre tiene acceso.
- Cualquier proceso del contenedor (PID 1 o un subproceso lanzado por la app) puede leer/run/secrets/*.
- Una variable de entorno derivada del secreto (DB_PASSWORD=$(cat /run/secrets/db_password)en un entrypoint) vuelve a poner el secreto en el entorno, visible víadocker inspect.Docker Secrets protege contra fugas en capas de imagen y en
docker-compose.yml. No protege contra un proceso comprometido dentro del contenedor.
Docker Secrets vs .env.vault vs SOPS — qué método para qué contexto
Desplace la tabla
| Criterio | Docker Secrets (Compose) | .env.vault (dotenvx) | SOPS + age |
|---|---|---|---|
| Swarm requerido | No (desde Compose v2.24) | No | No |
| Secreto almacenado en texto plano en host | Sí (archivo fuente) | No (cifrado en repositorio) | No (cifrado en repositorio) |
| KMS remoto requerido | No | No (clave simétrica local posible) | No (age funciona offline) |
| Rotación sin redespliegue | No (reinicio necesario) | No (rebuild .env) | No (rebuild .env) |
| CI/CD: inyección en el pipeline | Complejo (archivos a provisionar) | Simple (variable `DOTENV_PRIVATE_KEY`) | Medio (clave age como secreto CI) |
| Curva de aprendizaje | Baja (nativo Compose) | Baja (CLI dotenvx) | Media (sintaxis YAML + claves age/GPG) |
Configuración complementaria: rotación y cifrado en reposo
Rotación de secretos en Docker Compose. Docker Compose no-Swarm no soporta rotación en caliente (a diferencia de Swarm, que puede actualizar un secreto sin detener el servicio). Para cambiar un secreto:
# 1. Escribir el nuevo valor
echo -n 'nueva-contraseña' > /etc/myapp/secrets/db_password
# 2. Reiniciar el servicio afectado
docker compose restart appCifrar los archivos fuente con age. Si quieres cifrar los archivos en el host (contra robo de snapshot o backup comprometido), SOPS + age permite almacenar archivos cifrados y descifrarlos al arrancar:
# Generar una clave age
age-keygen -o /root/.config/sops/age/keys.txt
# Cifrar el archivo de secreto
sops --encrypt --age $(age-keygen -y /root/.config/sops/age/keys.txt) \
/etc/myapp/secrets/db_password > /etc/myapp/secrets/db_password.enc
# En el script de arranque, descifrar antes de docker compose up
sops --decrypt /etc/myapp/secrets/db_password.enc > /etc/myapp/secrets/db_passwordAuditoría de accesos. Activa los logs de Docker con journald (--log-driver=journald en /etc/docker/daemon.json) para mantener un registro de quién lanzó qué contenedores y cuándo.
Lo que Docker Secrets no protege
Docker Secrets monta el secreto como tmpfs en /run/secrets/: es una red de seguridad contra fugas en imágenes y archivos Compose, no contra un proceso comprometido dentro del contenedor. Cualquier proceso que corra en el contenedor — incluyendo un shell obtenido vía RCE — puede leer /run/secrets/*. Y root en el host siempre tiene acceso al archivo fuente.
Si tu modelo de amenaza incluye un contenedor comprometido, la respuesta correcta es un gestor de secretos externo (HashiCorp Vault, AWS Secrets Manager, Infisical self-hosted) que entregue secretos vía API con autenticación, sin escribirlos nunca en el disco del contenedor.
Resolución de problemas — errores comunes
unknown shorthand flag: 's' in -s durante docker compose up
Estás usando el comando antiguo docker-compose (v1, Python). Cambia a docker compose (v2, plugin Go) con apt-get install docker-compose-plugin.
secrets are only supported when deploying to a swarm
Tu versión de Docker Compose es inferior a v2.24. Verifica con docker compose version y actualiza.
El contenedor arranca pero /run/secrets/db_password está vacío
Verifica que el archivo fuente existe y no está vacío: cat /etc/myapp/secrets/db_password | wc -c. Un archivo vacío crea un montaje tmpfs vacío, sin error.
permission denied al leer /run/secrets/
Los archivos se montan con los permisos del archivo fuente. Si tu proceso corre como usuario no-root en el contenedor, ajusta los permisos del archivo en el host: chmod 640 /etc/myapp/secrets/db_password y verifica el GID del proceso.
docker inspect sigue mostrando la variable de entorno en texto plano
Declaraste el secreto pero también pasaste el valor en environment:. Elimina la entrada con valor directo y usa solo la convención *_FILE en environment:.
Tus secretos bajo control — y los siguientes pasos
Docker Secrets en modo Compose no-Swarm elimina la principal causa de filtración: credenciales en texto plano en archivos de configuración y capas de imagen. El método cabe en cinco pasos, funciona sin infraestructura externa y se integra en cualquier flujo de trabajo existente.
Para ir más lejos:
- Checklist de producción: los 10 puntos imprescindibles para un docker-compose.yml listo para producción.
- Primeros pasos con Docker en VPS: empezar con Docker en VPS si estás construyendo tu primer entorno.
- Hardening del SO: checklist de hardening Linux para asegurar la capa host sobre la que corre Docker.