Tutorial

Docker Secrets en producción: proteger credenciales en VPS

Seguridad y monitorización7 min de lectura5 pasos

Un docker-compose.yml con claves API en texto plano es una filtración esperando al primer escáner. Este artículo explica cómo eliminar ese riesgo en un VPS root: Docker Secrets nativo (sin Swarm desde Compose v2.24), .env.vault y SOPS — cada método con sus comandos exactos y sus limitaciones reales.

Contenido· El riesgo real: tus secretos viajan dentro de tus imágenes1/9
  1. 01El riesgo real: tus secretos viajan dentro de tus imágenes
  2. 025 errores de configuración que exponen tus secretos
  3. 03Requisitos previos
  4. 04Método 1 — Docker Secrets en Compose sin Swarm (paso a paso)
  5. 05Docker Secrets vs .env.vault vs SOPS — qué método para qué contexto
  6. 06Configuración complementaria: rotación y cifrado en reposo
  7. 07Lo que Docker Secrets no protege
  8. 08Resolución de problemas — errores comunes
  9. 09Tus secretos bajo control — y los siguientes pasos

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.ymlenvironment: DB_PASSWORD: hunter2 es legible por cualquiera que acceda al archivo o la imagen.
  • Archivo .env confirmado en el repositorio — un error en .gitignore y el secreto queda en el historial de git para siempre (incluso después de git rm, accesible vía git log).
  • ARG pasado en el build y copiado en la imagen — los ARG quedan grabados en los metadatos de la capa de imagen y son legibles con docker 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 /root o el directorio del proyecto — un archivo .env montado 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)

  1. 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.1

    Si estás por debajo de v2.24, actualiza Compose antes de continuar (apt-get install docker-compose-plugin en Debian/Ubuntu).

  2. 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 -n de echo evita el salto de línea final — algunas aplicaciones leen el archivo completo incluido el salto de línea, lo que invalida la clave.

  3. 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_key

    Observa el uso de la convención *_FILE: tu aplicación debe leer la variable DB_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.

  4. 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.

  5. 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ía docker 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

CriterioDocker Secrets (Compose).env.vault (dotenvx)SOPS + age
Swarm requeridoNo (desde Compose v2.24)NoNo
Secreto almacenado en texto plano en hostSí (archivo fuente)No (cifrado en repositorio)No (cifrado en repositorio)
KMS remoto requeridoNoNo (clave simétrica local posible)No (age funciona offline)
Rotación sin redespliegueNo (reinicio necesario)No (rebuild .env)No (rebuild .env)
CI/CD: inyección en el pipelineComplejo (archivos a provisionar)Simple (variable `DOTENV_PRIVATE_KEY`)Medio (clave age como secreto CI)
Curva de aprendizajeBaja (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 app

Cifrar 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_password

Auditorí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.

Controla tu stack, controla tus secretos

Un VPS root con acceso Docker completo: tú decides la gestión de secretos, la superficie de ataque y cada capa de tu infraestructura.

¿Necesita ayuda?

Consulte nuestro centro de ayuda y nuestra FAQ, o contacte con nuestro equipo: llamada, WhatsApp o correo electrónico. Soporte en francés, inglés y árabe.

Escribir por WhatsAppse abre en una pestaña nueva