Dominando las Variables de Entorno en Docker: Configuración vs. Secretos
Desbloquea despliegues seguros y flexibles en Docker dominando las variables de entorno. Esta guía completa aclara la distinción crítica entre usar variables de entorno para la configuración general de la aplicación y gestionar de forma segura datos sensibles como claves API y contraseñas. Aprende métodos prácticos para pasar configuraciones no sensibles, comprende los graves riesgos de exponer secretos a través de variables de entorno y descubre cómo aprovechar Docker Secrets y Compose para una gestión robusta y cifrada de secretos. Eleva tu conocimiento de Docker y protege tus aplicaciones.
Dominando las Variables de Entorno en Docker: Configuración vs. Secretos
Las variables de entorno son convenientes en Docker porque permiten que la misma imagen se ejecute en desarrollo, staging y producción con diferentes configuraciones. Esa conveniencia se vuelve riesgosa cuando los equipos colocan contraseñas, claves de firma y tokens de API en el mismo lugar que los niveles de registro y los números de puerto.
El modelo mental claro es simple: las variables de entorno son adecuadas para la configuración de tiempo de ejecución no sensible. Los secretos deben provenir de un almacén de secretos o de un archivo de secretos montado, con acceso limitado y un plan de rotación.
Comprendiendo las Variables de Entorno para la Configuración
Las variables de entorno son un método directo y ampliamente adoptado para pasar configuraciones de tiempo de ejecución a las aplicaciones, incluidas las que se ejecutan en contenedores Docker. Permiten modificar el comportamiento de una aplicación sin reconstruir la imagen Docker, haciendo que tus contenedores sean más flexibles y portátiles. Esto es ideal para configuraciones dinámicas no sensibles, como números de puerto de la aplicación, indicadores de depuración o URLs de servicios de terceros.
Métodos para Pasar Variables de Configuración
Docker proporciona varias formas de definir e inyectar variables de entorno en tus contenedores:
1. Instrucción ENV en el Dockerfile
La instrucción ENV establece una variable de entorno predeterminada que estará disponible dentro del contenedor cuando se ejecute. Esto es adecuado para variables que es poco probable que cambien o que proporcionan valores predeterminados sensatos para tu aplicación.
FROM alpine:latest
ENV APP_PORT=8080
ENV DEBUG_MODE=false
COPY ./app /app
WORKDIR /app
CMD ["/app/start.sh"]
Consejo: Aunque ENV establece valores predeterminados, estos pueden ser sobrescritos en tiempo de ejecución.
2. Indicador -e o --env con docker run
Al lanzar un solo contenedor, puedes usar el indicador -e o --env para pasar variables de entorno directamente. Esto es común para pruebas ad-hoc o para proporcionar configuraciones específicas que difieren de los valores predeterminados del Dockerfile.
docker run -d -p 80:8080 --name my_app_instance \
-e APP_PORT=80 \
-e DEBUG_MODE=true \
my_app_image:latest
3. env_file en Docker Compose
Para gestionar múltiples variables de entorno, especialmente en varios servicios definidos en un archivo docker-compose.yml, la opción env_file es muy conveniente. Permite cargar variables desde uno o más archivos .env, manteniendo tu docker-compose.yml más limpio.
docker-compose.yml:
version: '3.8'
services:
webapp:
image: my_app_image:latest
ports:
- "80:8080"
env_file:
- ./config/app.env
./config/app.env:
APP_PORT=8080
DEBUG_MODE=false
API_ENDPOINT=https://api.example.com/v1
4. Clave environment en Docker Compose
Alternativamente, puedes definir variables de entorno directamente dentro de la sección environment de un servicio en docker-compose.yml. Esto a menudo se prefiere para un pequeño número de variables o para variables que son específicas de un solo servicio.
version: '3.8'
services:
webapp:
image: my_app_image:latest
ports:
- "80:8080"
environment:
APP_PORT: 8080
DEBUG_MODE: false
Los Peligros de Usar Variables de Entorno para Secretos
Si bien las variables de entorno son excelentes para la configuración, fundamentalmente no son seguras para gestionar datos sensibles (secretos) como contraseñas de bases de datos, claves API o claves SSH privadas. Esta es una vulnerabilidad de seguridad crítica que a menudo se pasa por alto, especialmente en entornos de desarrollo.
Por qué las Variables de Entorno no son Seguras para Secretos:
Visibilidad a través de
docker inspect: Cualquier persona con acceso al host Docker puede ver fácilmente las variables de entorno de un contenedor en ejecución usandodocker inspect <container_id>. Esto significa que tus secretos son visibles en texto plano.# Ejemplo de exposición de un secreto (NO HAGAS ESTO EN PRODUCCIÓN) docker run -d -e DB_PASSWORD=mysecretpassword --name insecure_app nginx:latest # Cualquiera puede ver la contraseña docker inspect insecure_app | grep DB_PASSWORDEspionaje de Procesos: Dentro del contenedor, otros procesos o usuarios (si existen múltiples usuarios) podrían leer las variables de entorno, especialmente si la aplicación se ejecuta como root o tiene privilegios elevados.
Registros e Historial: Las variables de entorno pueden terminar inadvertidamente en registros, historial de tuberías CI/CD o historial de shell, lo que lleva a una exposición accidental.
Capas de Imagen: Si usas
ENVen un Dockerfile con un secreto, ese secreto se hornea en una capa de imagen y permanece allí, incluso si intentasunseten una capa posterior. Esto hace que el secreto sea recuperable de la propia imagen.Compartición Accidental: Los archivos
.envodocker-compose.ymlque contienen secretos a menudo se comprometen en sistemas de control de versiones o se comparten inapropiadamente, lo que lleva a una exposición generalizada.
Advertencia: Tratar la información sensible como variables de entorno regulares es un error de seguridad común. Siempre asume que las variables de entorno son públicamente visibles en el host y dentro del contenedor.
Gestionando Secretos de Forma Segura en Docker
Para abordar las deficiencias de seguridad de las variables de entorno para datos sensibles, Docker proporciona capacidades dedicadas de gestión de secretos, principalmente a través de Docker Secrets (para Docker Swarm) y herramientas externas como Docker Compose con funcionalidad secrets (que puede aprovechar los secretos de Docker Swarm o simplemente montar archivos).
Docker Secrets (Modo Docker Swarm)
Docker Secrets es una función integrada con el modo Docker Swarm que proporciona una forma segura de transmitir y almacenar datos sensibles para servicios. Los secretos son:
- Cifrados en reposo en los registros Raft del administrador de Swarm.
- Transmitidos de forma segura a las tareas de servicio autorizadas.
- Montados como archivos en memoria dentro del sistema de archivos del contenedor, típicamente en
/run/secrets/<secret_name>, en lugar de exponerse como variables de entorno. - Solo accesibles por servicios a los que se les ha otorgado acceso explícitamente.
Cómo Usar Docker Secrets (Modo Swarm)
- Inicializar Swarm (si aún no lo está):
docker swarm init ```
- Crear un Secreto: Los secretos se crean a partir de un archivo o entrada estándar.
echo "my_secure_db_password" | docker secret create db_password_secret - echo "SG.your_api_key_here" | docker secret create sendgrid_api_key - ```
- Desplegar un Servicio con el Secreto: Los servicios hacen referencia a los secretos por nombre. Docker monta el secreto en el contenedor.
docker service create --name my-webapp
--secret db_password_secret
--secret sendgrid_api_key
my_app_image:latest
```
- Accediendo a los Secretos en el Contenedor: Las aplicaciones leen el secreto desde la ruta del archivo montado.
En tu código de aplicación Python (o similar para otros lenguajes)
with open('/run/secrets/db_password_secret', 'r') as f: db_password = f.read().strip()
with open('/run/secrets/sendgrid_api_key', 'r') as f: sendgrid_key = f.read().strip() ```
Docker Compose y Secretos (para un solo host o Swarm)
Docker Compose versión 3.1+ introdujo una sección secrets, que te permite definir y hacer referencia a secretos dentro de tu docker-compose.yml. Cuando se ejecuta en modo Swarm, Compose aprovecha los secretos nativos de Docker Swarm. Cuando se ejecuta en un solo host sin modo Swarm, Compose aún admite secretos montando archivos del host en el contenedor de forma segura, aunque sin el cifrado en reposo proporcionado por Swarm.
Usando secrets en docker-compose.yml
Definir Secretos: Puedes definir secretos ya sea haciendo referencia a un archivo externo o convirtiéndolo en un secreto externo (secreto de Swarm pre-creado).
# docker-compose.yml version: '3.8' services: webapp: image: my_app_image:latest ports: - "80:8080" secrets: - db_password - sendgrid_api_key secrets: db_password: file: ./secrets/db_password.txt # Ruta a un archivo en el host que contiene la contraseña sendgrid_api_key: external: true # Se refiere a un secreto de Docker Swarm preexistente llamado 'sendgrid_api_key'Crear Archivos de Secretos Locales (si se usa
file):
mkdir secrets echo "my_local_db_password" > ./secrets/db_password.txt ```
Desplegar con Compose:
docker compose up -ddesplegará tus servicios, haciendo que los secretos estén disponibles en/run/secrets/<secret_name>dentro de los contenedores.# Dentro del contenedor, el contenido de ./secrets/db_password.txt estará en: # /run/secrets/db_password
Eligiendo la Herramienta Correcta: Configuración vs. Secretos
La decisión de usar una variable de entorno para la configuración o una solución de gestión de secretos dedicada se reduce a una pregunta principal:
¿Son los datos sensibles?
- Si es Sí (datos sensibles): Usa Docker Secrets (con Swarm) o un sistema de gestión de secretos similar (por ejemplo, Kubernetes Secrets, HashiCorp Vault). Para configuraciones de Compose de un solo host, usa la sección
secretspara montar archivos de forma segura. - Si es No (configuración no sensible): Usa variables de entorno (a través de
ENVen Dockerfile, indicador-e,env_fileoenvironmenten Compose).
| Característica | Variables de Entorno (para configuración) | Docker Secrets (para datos sensibles) |
|---|---|---|
| Propósito | Configuración de aplicación no sensible | Datos sensibles como contraseñas y claves API |
| Visibilidad | Visible a través de docker inspect y a menudo inspección de procesos |
Montado como archivos; no se muestra como valores de entorno normales |
| Seguridad | No apropiado para datos sensibles | Manejo más fuerte; los secretos de Swarm están cifrados en reposo, mientras que los secretos de archivos de Compose locales dependen de la protección del archivo host |
| Acceso en la App | Leer desde os.environ o similar |
Leer desde el archivo /run/secrets/<secret_name> |
| Gestionado por | Tiempo de ejecución de Docker, Docker Compose | Docker Swarm, Docker Compose o un gestor de secretos externo |
| Casos de Uso | Números de puerto, indicadores de depuración, URLs no sensibles | Contraseñas de bases de datos, tokens API, claves privadas |
Mejores Prácticas para Ambos
Para Configuración (Variables de Entorno):
- Proporciona valores predeterminados sensatos en tu Dockerfile usando
ENV. Esto hace que tus imágenes sean ejecutables de inmediato y documenta claramente las variables esperadas. - Externaliza la configuración cuando sea posible. Usa archivos
.envcondocker composeo servicios de configuración externos para despliegues más grandes. - Documenta todas las opciones de configuración y sus valores esperados, quizás en un
README.mdo documentación de la aplicación. - Evita codificar valores que puedan cambiar entre entornos (desarrollo, staging, producción).
Para Secretos (Docker Secrets y más allá):
- Nunca comprometas secretos (por ejemplo, archivos
.envque contengan secretos,db_password.txt) en sistemas de control de versiones como Git. - Rota los secretos regularmente. Esto minimiza la ventana de exposición si un secreto se ve comprometido.
- Otorga el menor privilegio. Solo da acceso a los servicios a los secretos que absolutamente necesitan.
- Evita registrar valores de secretos. Asegúrate de que el registro de tu aplicación e infraestructura no imprima el contenido de los secretos.
- Para despliegues a gran escala de nivel empresarial, considera soluciones de gestión de secretos dedicadas como HashiCorp Vault, AWS Secrets Manager o Azure Key Vault, que ofrecen características más avanzadas como auditoría, generación dinámica de secretos e integración con Identity and Access Management (IAM).
Una Regla Práctica para Decidir Qué Va Dónde
Antes de agregar un valor a environment, pregúntate qué pasaría si un compañero de equipo lo pegara en un ticket de soporte. Si la respuesta es "nada grave", probablemente sea configuración. Si la respuesta es "rotaríamos las credenciales", es un secreto.
Buenas variables de entorno:
APP_ENV=production
LOG_LEVEL=info
PUBLIC_BASE_URL=https://example.com
FEATURE_SIGNUP_ENABLED=false
REDIS_HOST=redis
Malas variables de entorno:
DATABASE_PASSWORD=...
STRIPE_SECRET_KEY=...
JWT_SIGNING_KEY=...
AWS_SECRET_ACCESS_KEY=...
PRIVATE_SSH_KEY=...
Hay áreas grises. Un nombre de host de base de datos suele ser configuración. Una URL completa de base de datos que incluya nombre de usuario y contraseña es un secreto. Una clave de análisis pública puede ser segura para una aplicación de navegador, mientras que un token API privado para el mismo proveedor no lo es. En caso de duda, trata el valor como sensible hasta que demuestres lo contrario.
Los Archivos .env de Compose son Fáciles de Malinterpretar
Docker Compose usa .env de dos maneras diferentes que la gente a menudo confunde.
Primero, Compose lee un archivo .env a nivel de proyecto para la sustitución de variables dentro de compose.yml:
services:
web:
image: "${APP_IMAGE}"
ports:
- "${HOST_PORT}:8080"
Segundo, env_file pasa variables al contenedor:
services:
web:
image: my-app
env_file:
- ./app.env
Esos archivos pueden verse similares, pero sirven para diferentes propósitos. El primero ayuda a Compose a renderizar la configuración. El segundo se convierte en entorno de tiempo de ejecución dentro del contenedor. No asumas que un valor en el .env del proyecto aparece automáticamente dentro del contenedor a menos que lo pases explícitamente.
Para el desarrollo local, un archivo de ejemplo verificado es útil:
# .env.example
APP_ENV=development
LOG_LEVEL=debug
PUBLIC_BASE_URL=http://localhost:3000
Luego mantén el .env real fuera de Git:
.env
*.env.local
secrets/
El archivo de ejemplo documenta lo que la aplicación espera sin exponer valores privados.
Leyendo Secretos Basados en Archivos en una Aplicación
Muchas aplicaciones ya esperan secretos en variables de entorno. Pasar a secretos basados en archivos es más fácil si soportas ambos patrones durante un tiempo.
Por ejemplo, un ayudante de Node.js:
import fs from "node:fs";
function readSecret(name) {
const filePath = process.env[`${name}_FILE`];
if (filePath) {
return fs.readFileSync(filePath, "utf8").trim();
}
return process.env[name];
}
const databasePassword = readSecret("DATABASE_PASSWORD");
Luego tu archivo Compose puede apuntar a un archivo de secreto montado:
services:
web:
image: my-app
environment:
DATABASE_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt
Este patrón funciona bien porque la aplicación aún puede ejecutarse en entornos más antiguos mientras mueves la producción hacia secretos montados en archivos. Muchas imágenes oficiales ya soportan variables que terminan en _FILE por esta razón.
No Pongas Secretos en los Argumentos de Construcción Tampoco
Las variables de entorno no son la única trampa. Los argumentos de construcción también pueden filtrarse si los usas para obtener paquetes privados o clonar repositorios:
ARG NPM_TOKEN
RUN npm config set //registry.npmjs.org/:_authToken=$NPM_TOKEN
Incluso si el contenedor final no muestra NPM_TOKEN, el historial de construcción y las capas intermedias pueden exponer más de lo que esperas. Con BuildKit, usa montajes de secretos para secretos en tiempo de construcción:
# syntax=docker/dockerfile:1.7
FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=secret,id=npm_token \
NPM_TOKEN="$(cat /run/secrets/npm_token)" npm ci
Constrúyelo así:
docker build \
--secret id=npm_token,src=.npm-token \
-t my-app .
Eso mantiene el token fuera del Dockerfile y evita hornearlo en una capa normal. Aún necesitas proteger el archivo .npm-token local y el almacenamiento de secretos de CI.
Kubernetes, Gestores de Secretos en la Nube y Docker
Docker Secrets son útiles en Swarm, y los secretos de Compose son útiles para configuraciones locales o de un solo host. En Kubernetes, normalmente usarás Kubernetes Secrets, un operador de secretos externo o una integración con un gestor de secretos en la nube. En AWS, los equipos a menudo usan AWS Secrets Manager o Systems Manager Parameter Store. En Azure, Azure Key Vault es común. En Google Cloud, Secret Manager cumple el mismo rol.
El principio es el mismo en todas las plataformas:
- Almacena valores sensibles en un sistema diseñado para secretos.
- Otorga a la identidad de tiempo de ejecución acceso solo a los secretos que necesita.
- Monta o inyecta secretos en tiempo de ejecución.
- Rota los secretos sin reconstruir la imagen.
- Mantén los secretos fuera del control de fuentes, las capas de imagen, los registros y los paneles.
Kubernetes Secrets están codificados por defecto, no automáticamente cifrados en cada configuración de clúster. Muchos clústeres gestionados soportan cifrado en reposo, pero verifica la configuración real del clúster en lugar de asumir. Para credenciales de alto riesgo, usa un gestor de secretos en la nube o una herramienta dedicada con registros de auditoría y soporte de rotación.
La Rotación es Parte del Diseño
Una estrategia de secretos que no puede rotar está incompleta. Haz estas preguntas antes de producción:
- ¿Podemos cambiar la contraseña de la base de datos sin reconstruir la imagen?
- ¿Pueden dos credenciales válidas superponerse durante un despliegue?
- ¿La aplicación relee los secretos o necesita un reinicio?
- ¿Dónde se registran, almacenan en caché o almacenan las credenciales antiguas?
- ¿Quién recibe una notificación cuando un secreto cambia?
Para bases de datos, la rotación a menudo significa crear una segunda credencial, desplegar la aplicación con la nueva credencial, verificar el tráfico y luego revocar la antigua. Para claves API, depende del proveedor. Algunos servicios permiten múltiples claves activas; otros fuerzan una transición. Diseña tu proceso de despliegue en torno a la dependencia menos flexible.
Limpia la Exposición Accidental
Si un secreto ya se ha comprometido en Git o se ha horneado en una imagen, eliminar la línea no es suficiente. Trátalo como expuesto.
La respuesta habitual es:
- Revocar o rotar la credencial.
- Eliminarla del código o imagen actual.
- Revisar los registros de CI, registros de imágenes, rastreadores de problemas y mensajes de chat en busca de copias.
- Reescribir el historial de Git solo si tu organización está preparada para manejar la coordinación; la rotación sigue siendo necesaria.
- Agregar escaneo o comprobaciones previas al commit para reducir errores repetidos.
Las herramientas pueden ayudar, pero no reemplazan los hábitos. Mantén los archivos de secretos claramente nombrados, ignóralos en Git y evita imprimir objetos de configuración en su totalidad al inicio.
El Patrón de Trabajo
Usa variables de entorno para valores que describen cómo debe ejecutarse la aplicación en este entorno: puertos, niveles de registro, indicadores de características, nombres de host de servicios y URLs no sensibles. Usa secretos para valores que prueban identidad u otorgan acceso: contraseñas, tokens, claves de firma, claves privadas y credenciales de proveedores.
La imagen Docker limpia es la misma en todos los entornos. Desarrollo, staging y producción cambian el comportamiento en tiempo de ejecución. La configuración puede viajar como variables de entorno. Los secretos deben provenir de un almacén de secretos o un archivo de secretos montado con acceso limitado. Esa separación mantiene los despliegues flexibles sin convertir cada inspección de contenedor, línea de registro o capa de imagen en una fuga de credenciales.