El informe de incidentes siempre empieza igual: “Puede que se hayan expuesto credenciales.” Rara vez dice “se expusieron” al principio. Damos vueltas,
tratando de comprar certeza con tiempo. Entonces alguien encuentra la evidencia: un archivo .env horneado en una capa de imagen, impreso por
una instrucción de depuración, o subido a un ticket de soporte porque “era más fácil”.
Los contenedores no crearon la proliferación de secretos. Solo la hicieron más rápida. Si todavía usas variables de entorno (o peor, .env)
como tu mecanismo principal de distribución de secretos, no estás “manteniéndolo simple”. Te estás escribiendo tu propio postmortem.
Qué sale mal con .env y las variables de entorno
Seamos específicos. Las variables de entorno no son “inherentemente inseguras”. Son inherentemente promiscuas.
Se propagan. Se copian, se registran y se muestran en sitios que no pretendías. La diferencia importa.
1) El runtime del contenedor trata env como metadatos, y a los metadatos les encanta viajar
El entorno de un contenedor termina en múltiples lugares: en la definición del orquestador, en la salida de inspección, en listados de procesos,
en volcados de fallos y en páginas de depuración “útiles”. Algunos de esos lugares tienen sus propios controles de acceso. Muchos no.
2) Las variables env son fáciles de exfiltrar una vez que tienes cualquier punto de apoyo
Si un atacante obtiene ejecución de código dentro de un contenedor, leer archivos bajo un montaje de secretos dedicado puede requerir esfuerzo,
pero leer env es trivial: ya está ahí, heredado por el proceso, normalmente legible por ese proceso y a menudo impreso
accidentalmente por herramientas estándar.
3) Los archivos .env son una trampa de flujo de trabajo
Un archivo .env hace agradable el desarrollo local. También hace que “simplemente haz commit para la demo” esté a un mensaje de Slack.
Incluso si tu repositorio es privado, los secretos no permanecen privados. Se bifurcan, se espejean, se cachean y se clonan en portátiles
que viajan por aeropuertos y cafeterías.
Uno de los modos de falla más comunes no es un atacante. Es un ingeniero sobrecargado que añade printenv para depurar,
y luego olvida quitarlo. Así es como tu contraseña “privada” de la base de datos termina en logs centralizados, buscable por cualquiera
con acceso de lectura al sistema de logs.
Chiste corto #1: Si tu secreto está en .env, no es un secreto; es un estado de ánimo.
4) Las capas de imagen y las caches de build te traicionarán
Si pasas secretos como argumentos de build o copias .env en el contexto de build, puedes acabar con credenciales dentro
de capas de imagen. Incluso si las borras después, el historial de capas puede seguir conteniéndolas. No te “rm -f” de un registro
direccionado por contenido.
5) Las variables env difuminan la frontera entre configuración y material secreto
Un número de puerto es configuración. Una contraseña de base de datos es material secreto. Trátalos distinto. El problema operativo es que
ENV= se ve igual para ambos, así que los equipos terminan dándole a los secretos el mismo ciclo de vida que a la configuración:
registrados en el repo, plantillas en CI, copiados entre entornos. Así los secretos de dev se convierten en secretos de prod, y los secretos de prod
acaban en portátiles de desarrollo.
Hechos y contexto histórico (lo que seguimos re-aprendiendo)
Aquí hay hechos concretos y puntos de contexto que importan cuando decides cómo gestionar secretos, y por qué “pero todo el mundo usa vars env”
no es un argumento serio.
-
La “app 12-factor” popularizó las env vars para configuración para mantener builds inmutables y despliegues portables. No
fue escrita como una aprobación absoluta para secretos de producción de larga duración. -
Docker Swarm introdujo secretos integrados (como montajes de archivo) para abordar el problema real de la fuga de env a través de
docker inspecty logs. -
Los Secrets de Kubernetes históricamente estaban solo codificados en base64 por defecto; el cifrado en reposo requería configuración
explícita. La industria aprendió (otra vez) que “codificado” no es “cifrado”. -
Los sistemas de build mejoraron porque tenían que hacerlo: BuildKit añadió montajes de secretos específicamente porque pasar
secretos como build args los filtraba en capas de imagen. -
Los registros son para siempre: una vez que un secreto llega a una capa de registro o a una cache, eliminar la etiqueta no
remueve confiablemente los datos de todos los espejos y caches. -
Los entornos de procesos son observables en muchos sistemas. En Linux, el entorno de un proceso puede leerse vía
/proc/<pid>/environpor usuarios con privilegios suficientes. No necesitas ser “root” en abstracto; necesitas
la combinación adecuada de capacidades y acceso a namespaces. -
Los sistemas de logs cambiaron el radio de impacto: una contraseña filtrada en logs de contenedores ahora se vuelve un artefacto
buscable replicado a través de clusters y niveles de retención. -
La rotación de secretos es una característica de fiabilidad, no solo teatro de seguridad. Los equipos que nunca rotan lo aprenden
durante un incidente, bajo la peor presión de tiempo posible.
Un modelo de amenaza práctico: cómo se filtran secretos en sistemas con contenedores
El modelado de amenazas no necesita una pizarra y un taller de tres días. Necesitas listar las formas en que los secretos escapan y decidir cuáles
puedes prevenir, cuáles puedes detectar y cuáles solo puedes reducir.
Ruta de fuga A: control de código fuente y proliferación de artefactos
- Dónde ocurre:
.envcomprometido, archivo de ejemplo con valores reales, rama “temporal”, pegado en wiki interna. - Por qué ocurre: conveniencia, confusión entre configuración y secreto, falta de puertas de escaneo.
- Tipo de solución: prevenir (gitignore + escaneo + política) y detectar (scanners de secretos, monitorización de repos).
Ruta de fuga B: logs de CI y metadatos de build
- Dónde ocurre: paso de pipeline que hace echo de env, tests que imprimen cadenas de conexión, build args almacenados en logs.
- Por qué ocurre: “depuración”, verbosidad mal configurada, scripts ingenuos.
- Tipo de solución: prevenir (enmascarado, no imprimir, montajes de archivos) y detectar (depuración de logs y alertas).
Ruta de fuga C: inspección de contenedores y APIs de orquestación
- Dónde ocurre:
docker inspect, configs de Compose, especificaciones de servicio de Swarm, specs de pods de Kubernetes. - Por qué ocurre: el acceso a metadatos es más amplio de lo que debería ser el acceso a secretos.
- Tipo de solución: prevenir (usar objetos secret) y reducir (ajustar RBAC para inspect/describe).
Ruta de fuga D: compromiso en tiempo de ejecución y movimiento lateral
- Dónde ocurre: RCE en la app, SSRF hacia endpoints de metadata, endpoints de depuración expuestos.
- Por qué ocurre: las apps son apps; se rompen.
- Tipo de solución: reducir (mínimos privilegios, credenciales de corta duración, identidad separada por servicio) y detectar (detección de anomalías).
Ruta de fuga E: backups y snapshots
- Dónde ocurre: volúmenes con secretos embebidos, dumps de bases de datos que contienen credenciales, snapshots del sistema de archivos.
- Por qué ocurre: secretos almacenados como archivos regulares sin controles de ciclo de vida.
- Tipo de solución: prevenir (no almacenar secretos en rutas de datos de la app), reducir (cifrar backups), detectar (auditoría).
La verdad operativa es esta: no solo proteges secretos de atacantes. Los proteges de la tendencia de tus propios sistemas a copiar, cachear e indexar todo.
Una cita, porque aplica a secretos tanto como a outages: “La esperanza no es una estrategia.”
— Vince Lombardi
Qué hacer en su lugar: secretos como archivos, secretos Docker y configuraciones sensatas
El objetivo no es la pureza. El objetivo es reducir la probabilidad y el radio de impacto de las fugas. El mejor valor predeterminado en plataformas de contenedores es:
montar secretos como archivos, mantenerlos fuera de capas de imagen, fuera de logs y convertir la rotación en un despliegue normal.
Opción 1: secretos de Docker Swarm (útiles incluso si no “usas Swarm”)
Los secretos de Docker en Swarm son un mecanismo de primera clase: cifrados en reposo en el log raft de Swarm, entregados a tasks sobre mutual TLS,
y montados en contenedores como archivos en memoria (típicamente bajo /run/secrets). No son visibles en
docker inspect como lo son las env vars.
El gran beneficio: los secretos son datos con ciclo de vida, no strings esparcidos en YAML.
Opción 2: secretos en Docker Compose (con matices)
Compose soporta secretos en la especificación, pero el comportamiento depende del backend. Con el engine local de Docker (no-Swarm),
los secretos de Compose a menudo se mapean a bind mounts, lo cual es mejor que las env vars pero aún significa que el secreto existe en disco en algún lugar.
Eso puede ser aceptable para desarrollo y despliegues pequeños si se hace con consciencia.
Opción 3: gestores de secretos externos (lo mejor para producción seria)
Si tienes más de un cluster, más de un equipo o requisitos de cumplimiento, quieres un gestor de secretos dedicado
(almacén de secretos del proveedor cloud, sistema estilo Vault o servicio respaldado por HSM). El runtime entonces obtiene credenciales de corta duración
usando una identidad, no una contraseña estática compartida.
Este artículo está enfocado en Docker, pero el principio es universal: el acceso basado en identidad supera a los secretos estáticos compartidos.
Opción 4: si debes usar env vars, contiene el daño
- Usa env vars solo para configuración no secreta.
- Si tienes que pasar un secreto vía env (aplicaciones legacy ocurren), mantenlo de corta duración y rota agresivamente.
- Nunca imprimas el entorno en logs.
- Restringe quién puede inspeccionar contenedores y leer logs. “Solo lectura” a menudo no es inofensivo.
Chiste corto #2: Poner contraseñas en .env es como etiquetar tu llave de casa “LLAVE DE LA CASA” y esconderla bajo el felpudo.
Cómo los secretos basados en archivos cambian el diseño de tu app (de forma positiva)
Leer un secreto desde un archivo te obliga a confrontar el ciclo de vida. Puedes intercambiar el archivo. Puedes rotarlo. Puedes controlar permisos.
Y tu app puede recargarlo sin una reconstrucción completa.
Un patrón práctico:
- Montar el secreto como
/run/secrets/db_password - La app lo lee al iniciar, opcionalmente lo vuelve a leer en SIGHUP o en un intervalo
- La rotación de secretos se vuelve: actualizar el objeto secreto → reiniciar tasks o disparar recarga
Tareas prácticas: comandos, salidas y decisiones (12+)
Esta es la parte que la gente se salta. No lo hagas. La diferencia entre “usamos secretos” y “realmente no los filtramos” es la verificación.
Cada tarea abajo incluye un comando, salida de ejemplo, qué significa la salida y la decisión que tomas.
Tarea 1: Encontrar archivos .env que no deberían existir
cr0x@server:~$ find . -maxdepth 4 -type f -name ".env" -o -name "*.env"
./.env
./services/api/.env
Significado: Tienes archivos de entorno en el árbol; al menos uno está en la raíz del repo, que es donde ocurren los accidentes.
Decisión: Mantén solo archivos de ejemplo (.env.example) en el repo; elimina los reales del control de versiones
y rota cualquier credencial que haya vivido ahí.
Tarea 2: Verificar si los secretos están en el historial de git
cr0x@server:~$ git log --name-only --pretty=format: | grep -E '(^|/)\.env$' | head
.env
services/api/.env
Significado: Esos archivos se comprometieron en algún momento. Incluso si los borraste ahora, pueden estar en la historia y en clones.
Decisión: Trata todas las credenciales que aparecieron allí como comprometidas; róta. Luego limpia la historia solo si entiendes el
impacto operativo de reescribir el historial de git.
Tarea 3: Buscar patrones comunes de secretos en el repo
cr0x@server:~$ grep -RIn --exclude-dir=.git -E "(PASSWORD=|API_KEY=|SECRET=|TOKEN=|BEGIN PRIVATE KEY)" .
./services/api/.env:3:DB_PASSWORD=summer2023
Significado: Tienes al menos un secreto literal en texto plano. Si está en el árbol de trabajo, probablemente esté en otros sitios también.
Decisión: Elimínalo, róta la clave, añade puertas de escaneo y deja de tratar a grep como tu programa de seguridad.
Tarea 4: Inspeccionar un contenedor en ejecución por filtración de secretos vía env
cr0x@server:~$ docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"
NAMES IMAGE STATUS
api-1 myorg/api:1.8.2 Up 3 hours
db-1 postgres:16 Up 3 hours
cr0x@server:~$ docker inspect api-1 --format '{{json .Config.Env}}'
["NODE_ENV=production","DB_USER=app","DB_PASSWORD=summer2023","DB_HOST=db"]
Significado: La contraseña es visible para cualquiera con permiso para inspeccionar contenedores. Ese permiso suele ser más amplio de lo que crees.
Decisión: Elimina vars de entorno secretas; reemplázalas con montajes de archivos secretos; restringe el acceso a la API de Docker.
Tarea 5: Comprobar si el secreto está presente dentro del contenedor como archivo (el patrón mejor)
cr0x@server:~$ docker exec api-1 ls -l /run/secrets
total 4
-r--r----- 1 root root 16 Jan 3 10:12 db_password
Significado: Tienes un archivo de secreto montado con permisos restrictivos. Buen comienzo.
Decisión: Asegúrate de que tu app se ejecute como un usuario que pueda leerlo (pertenencia a grupo), no como root “porque funciona”.
Tarea 6: Verificar que la aplicación no esté registrando variables de entorno
cr0x@server:~$ docker logs --tail 200 api-1 | grep -E "(DB_PASSWORD|API_KEY|SECRET|TOKEN)" || echo "no obvious secrets in tail"
no obvious secrets in tail
Significado: No hay cadenas secretas obvias en las últimas 200 líneas. Eso no es prueba, pero es una verificación básica de cordura.
Decisión: Sigue: revisa logs estructurados, rutas de error y banners de inicio. Luego añade comprobaciones automáticas en CI.
Tarea 7: Detectar exposición de secretos en el renderizado de la config de Compose
cr0x@server:~$ docker compose config | sed -n '1,120p'
services:
api:
environment:
DB_HOST: db
DB_PASSWORD: ${DB_PASSWORD}
Significado: Compose sigue cableando secretos a través de environment. Incluso si provienen de tu shell, terminan en la config.
Decisión: Mueve secretos fuera de environment y dentro de secrets: con montajes de archivos.
Tarea 8: Crear y usar un secreto de Swarm (ciclo de vida real de un secreto)
cr0x@server:~$ docker swarm init
Swarm initialized: current node (r8k3t2...) is now a manager.
cr0x@server:~$ printf "correct-horse-battery-staple\n" | docker secret create db_password -
z1p8kq3m9gq9u5a0l0xw2v3p1
Significado: El secreto ahora existe en el plano de control de Swarm. No lo escribiste en disco.
Decisión: Usa esto para la distribución de secretos en producción si Swarm encaja en tu modelo operativo; de lo contrario, usa un gestor externo.
Tarea 9: Confirmar que los secretos están montados donde esperas (y no en env)
cr0x@server:~$ docker service create --name api --secret db_password --env DB_USER=app alpine:3.20 sh -c "env | grep -E 'DB_PASSWORD' || echo 'no DB_PASSWORD in env'; ls -l /run/secrets"
no DB_PASSWORD in env
total 4
-r--r----- 1 root root 29 Jan 3 10:20 db_password
Significado: El secreto está disponible como archivo, no como variable de entorno. Eso es lo que quieres.
Decisión: Actualiza la aplicación para leer el archivo y deja de esperar una env var.
Tarea 10: Rotar un secreto de Swarm (la parte dolorosa pero correcta)
cr0x@server:~$ docker secret ls
ID NAME CREATED UPDATED
z1p8kq3m9gq9u5a0l0xw2v3p1 db_password 5 minutes ago 5 minutes ago
cr0x@server:~$ printf "new-password-value\n" | docker secret create db_password_v2 -
m2c1v0b8n7x6a5s4d3f2g1h0j9
cr0x@server:~$ docker service update --secret-rm db_password --secret-add source=db_password_v2,target=db_password api
api
overall progress: 1 out of 1 tasks
1/1: running [==================================================>]
verify: Service converged
Significado: Creaste un nuevo secreto y actualizaste el servicio para usarlo. Los secretos de Swarm son inmutables; la rotación es reemplazar y redeplegar.
Decisión: Incorpora la rotación en los playbooks de despliegue. Si tu sistema no tolera un reinicio de task, arregla eso primero.
Tarea 11: Verificar si un secreto entró accidentalmente en una capa de imagen
cr0x@server:~$ docker history --no-trunc myorg/api:1.8.2 | head -n 8
IMAGE CREATED CREATED BY SIZE COMMENT
sha256:3b1c... 2 days ago /bin/sh -c #(nop) COPY . /app 14.2MB
sha256:a9f0... 2 days ago /bin/sh -c #(nop) RUN npm ci 48.1MB
Significado: Si tu .env está en el contexto de build y se copia en la imagen, podría estar embebido en esa capa COPY.
Decisión: Asegura que .dockerignore excluya archivos secretos; reconstruye y redepliega; rota cualquier cosa que pudiera haber sido copiada.
Tarea 12: Validar que .dockerignore bloquee archivos secretos obvios
cr0x@server:~$ cat .dockerignore
.env
*.env
**/.env
**/*.env
id_rsa
*.pem
Significado: Estás excluyendo explícitamente portadores comunes de secretos del contexto de build.
Decisión: Manténlo. También valida que tu CI no inyecte secretos en el contexto de build de todas formas.
Tarea 13: Comprobar quién puede hablar con el socket de Docker (aka “quién puede convertirse en root”)
cr0x@server:~$ ls -l /var/run/docker.sock
srw-rw---- 1 root docker 0 Jan 3 08:01 /var/run/docker.sock
cr0x@server:~$ getent group docker
docker:x:998:deploy,ci-runner
Significado: Los miembros del grupo docker efectivamente tienen control equivalente a root en el host.
Si pueden inspeccionar contenedores, pueden leer env, montar sistemas de archivos y extraer secretos.
Decisión: Trata el acceso al socket de Docker como privilegiado; reduce la membresía, audítala e aísla runners de CI.
Tarea 14: Confirmar que un secreto no se está pasando vía Compose con --env-file
cr0x@server:~$ ps aux | grep -E "docker compose.*--env-file" | grep -v grep
deploy 21984 0.2 0.1 23844 9152 ? Ss 10:02 0:00 docker compose --env-file /srv/app/.env up -d
Significado: Alguien está inyectando explícitamente un env file en tiempo de ejecución. Ese archivo probablemente vive en disco en el host y puede estar en backups.
Decisión: Reemplázalo por secretos montados desde una ubicación protegida y elimina archivos env en texto plano de rutas de backup del host.
Tarea 15: Escanear logs por patrones de alto riesgo sin volcarlo todo
cr0x@server:~$ docker logs api-1 2>&1 | grep -E "password=|Authorization: Bearer|BEGIN PRIVATE KEY" | head
Authorization: Bearer eyJhbGciOi...
Significado: Tienes tokens bearer en logs. Esa es una credencial activa en muchos sistemas, y ahora es un artefacto buscable.
Decisión: Trátalo como incidente: rota tokens, depura/limita logs, añade filtros de logs y arregla la ruta de código que registra cabeceras.
Guion de diagnóstico rápido
Cuando sospechas fuga de secretos, no tienes tiempo para filosofía. Necesitas un bucle cerrado: confirmar, acotar, contener, rotar y prevenir recurrencias.
Aquí está el orden que gana la mayoría de las veces.
Primero: confirmar rutas de exposición (minutos)
-
Revisar metadatos de contenedores:
docker inspectpara env secretos. Si están presentes, asume exposición a cualquiera con acceso a la API de Docker. - Revisar logs por texto claro: busca patrones de tokens/contraseñas. Si los encuentras, trata los logs como almacenes de datos comprometidos.
- Revisar salida de CI: la última pipeline exitosa por pasos de depuración “útiles” o fallos de enmascarado de secretos.
Segundo: acotar el radio de impacto (decenas de minutos)
- ¿Dónde más fue el secreto? capas de imagen, almacenamientos de artefactos, bundles de soporte, páginas wiki, logs de chat.
- ¿Quién tiene acceso? usuarios del socket de Docker, lectores de la API del orquestador, lectores de logs, lectores del registro.
- ¿Es reutilizable? las contraseñas estáticas son peores que tokens de corta duración. Pero los tokens de corta duración en logs siguen siendo malos.
Tercero: contener y rotar (horas)
- Rotar credenciales comenzando por las más poderosas (claves cloud, credenciales admin de BD, claves de firma).
- Redeploy para eliminar uso de env y cambiar a secretos basados en archivos o recuperación externa de secretos.
-
Reducir superficies de observabilidad: ajustar RBAC, reducir verbosidad de logs, bloquear patrones
printenvvía linting.
Realidad de cuello de botella: la mayoría de equipos pasa la primera hora discutiendo si es “una fuga real”. Deja de hacerlo. Rota primero, debate después.
Errores comunes: síntomas → causa raíz → solución
Error 1: “Usamos .env pero no está comprometido”
Síntomas: el secreto aparece en un diff de PR, o la comprometida de un portátil de un desarrollador desencadena una rotación de credenciales.
Causa raíz: secretos guardados como archivos en directorios de trabajo se copian, se comprimen, se adjuntan y se respaldan.
Solución: Usa .env solo para configuración local no secreta; mueve secretos al keychain del OS, gestor externo o archivos secretos de Compose/Docker.
Error 2: “Está bien, solo ops pueden ejecutar docker inspect”
Síntomas: auditores preguntan quién tiene acceso a Docker y no puedes responder rápido; contratistas pueden leer env de contenedores.
Causa raíz: el acceso al socket de Docker es más amplio de lo previsto (runners CI, accesos “temporales”, jump boxes compartidos).
Solución: trata la API de Docker como privilegiada; minimiza la membresía del grupo docker; aísla CI; usa secretos no env.
Error 3: Pasar secretos en tiempo de build
Síntomas: la contraseña aparece en docker history o es recuperable de capas del registro.
Causa raíz: usar ARG o copiar archivos de secreto en el contexto de build; borrarlos luego no borra las capas.
Solución: usa montajes de secretos de BuildKit para necesidades de build; nunca hornees secretos de runtime en imágenes; aplica .dockerignore.
Error 4: “Montamos un archivo secreto, así que ya está”
Síntomas: el secreto aún termina en logs, o la app falla y vuelca config incluyendo el contenido del archivo secreto.
Causa raíz: la app lee el secreto y lo imprime (directa o indirectamente) durante el manejo de errores, o endpoints de depuración exponen la config.
Solución: redacta campos sensibles; deshabilita endpoints de depuración en prod; añade tests que fallen si los secretos aparecen en logs.
Error 5: Contraseñas compartidas y de larga duración entre entornos
Síntomas: una fuga en dev desencadena rotación en prod; los equipos temen rotar porque rompe todo.
Causa raíz: una única credencial reutilizada entre dev/stage/prod y entre servicios; sin límites de identidad.
Solución: credenciales únicas por entorno y por servicio; preferir tokens de corta duración; implementar rotación como despliegue rutinario.
Error 6: Almacenar secretos en volúmenes que se respaldan
Síntomas: restauraciones de backup revelan credenciales antiguas; seguridad pregunta por qué secretos están en snapshots.
Causa raíz: secretos escritos en directorios de datos de la app; el sistema de backup captura todo.
Solución: monta secretos en rutas de runtime dedicadas como /run/secrets; excluye rutas de secretos de backups; cifra backups de todos modos.
Tres mini-historias corporativas desde el terreno
Mini-historia 1: Un incidente causado por una suposición errónea
Una empresa SaaS mediana tenía una separación limpia entre equipos de “plataforma” y “aplicación”. El equipo de plataforma poseía los hosts Docker y CI.
El equipo de aplicación poseía el código del servicio y los archivos Compose. Todos creían que su frontera era segura.
El equipo de aplicación estableció DB_PASSWORD vía variables de entorno en Compose, obtenidas desde una tienda de variables protegida en CI. Asumieron:
“La tienda de CI es segura, por lo tanto el secreto es seguro.” No estaban siendo imprudentes. Eran literales.
El equipo de plataforma añadió un trabajo de troubleshooting para on-call: un script que ejecutaba docker inspect y archivaba la salida durante incidentes.
Ayudó a diagnosticar límites de memoria, bucles de reinicio y tags de imagen. También archivó cada variable de entorno de cada contenedor, incluyendo
contraseñas de bases de datos y tokens API. El archivo fue a un object store interno usado para artefactos de incidentes.
Semanas después, a un contratista se le concedió acceso de lectura a artefactos de incidentes para ayudar con investigaciones de performance. No hizo nada malicioso.
Simplemente tuvo acceso a lo que había allí. Una revisión de seguridad encontró secretos dentro de esos artefactos, y ahora la empresa tenía
un problema de divulgación más un proyecto de rotación bajo plazo.
La suposición errónea no era “los contratistas son malos”. Era “los secretos en env son solo visibles en runtime”. En la práctica, los secretos en env se vuelven
metadatos, y los metadatos se vuelven artefactos. La solución fue aburrida: cambiar a secretos basados en archivos, redactar bundles diagnósticos y tratar
la “salida de inspección” como sensible.
Mini-historia 2: Una optimización que salió mal
Otra compañía ejecutaba una flota de servicios pequeños. Los despliegues eran frecuentes y querían despliegues más rápidos. Alguien propuso una “optimización”:
compilar una vez, desplegar en todas partes y evitar reinicios haciendo que la app recargue configuración dinámicamente desde variables de entorno. Sonaba ingenioso.
Añadieron un endpoint ligero para soporte: /debug/config. Devolvía la configuración efectiva para ayudar a diagnosticar rutas, flags y endpoints upstream.
Estaba protegido por una red interna y “solo SREs” podían alcanzarlo. Ya puedes ver por dónde va esto.
Un cambio de ruteo expuso accidentalmente ese endpoint a través de un proxy interno compartido por varios equipos. No al internet público, pero a una audiencia amplia.
Alguien depurando su propio servicio llegó al endpoint, vio un blob JSON con credenciales y lo reportó.
La optimización salió mal porque la “config dinámica” basada en env empujó secretos por el mismo canal que la configuración regular. El endpoint de depuración no
tenía separación limpia; simplemente volcó la config.
La remediación fue dolorosa pero directa: quitar secretos de env, montarlos como archivos, rediseñar la salida de depuración para excluir material secreto explícitamente
e introducir identidades por servicio para que una exposición accidental no se convierta en compromiso de toda la flota.
Mini-historia 3: Una práctica aburrida pero correcta que salvó el día
Una empresa del ámbito financiero (regulada lo suficiente para importarle, no tanto como para tener recursos infinitos) adoptó temprano una política:
“Todos los secretos son archivos, todos los archivos viven bajo un único directorio, y ese directorio nunca se incluye en bundles diagnósticos.”
Era el tipo de regla que la gente critica hasta que les salva.
Usaban secretos de Docker Swarm para un subconjunto de workloads y un gestor de secretos externo para el resto. De cualquier modo, la ruta en runtime era consistente:
/run/secrets/<name>. Las aplicaciones debían soportar leer desde esa ruta. No era opcional.
Durante una salida desordenada, un ingeniero senior pidió “todo”: salida de inspect de contenedores, logs y snapshots del sistema de archivos de un nodo con problemas.
El equipo los recopiló rápido. Seguridad revisó el bundle antes de compartirlo con un proveedor. La revisión no encontró credenciales.
No porque la gente fuera cuidadosa en el momento, sino porque el sistema estaba diseñado para hacer la “cuidado” el comportamiento por defecto.
La práctica aburrida hizo dos cosas: redujo los lugares donde los secretos podían aparecer y facilitó las auditorías. Cuando puedes decir,
“los secretos viven aquí y solo aquí”, puedes escanear ese límite, aplicar permisos y excluirlo de backups y diagnósticos.
Nadie escribió un post interno sobre esa política. Era demasiado sosa. Aun así previno un incidente secundario durante un incidente primario,
que es el tipo de victoria que no aparece en dashboards pero mantiene a las compañías en pie.
Listas de verificación / plan paso a paso
Plan paso a paso: migrar de .env a secretos basados en archivos (enfoque Docker)
-
Inventario de secretos actualmente en env: lista qué contenedores/servicios tienen variables de entorno tipo secreto
(contraseñas, tokens, claves privadas). -
Definir una ruta estándar de montaje de secretos: elige
/run/secretsy cúmplela. -
Actualizar aplicaciones: enseña a las apps a leer
DB_PASSWORD_FILE=/run/secrets/db_passwordo leer directamente el archivo.
Prefiere la convención “_FILE” si necesitas mantener la configuración basada en env sin incrustar valores secretos. -
Implementar secretos en tu plataforma:
- Swarm:
docker secret create+ montajes de secreto en servicios. - Compose (no-Swarm): usa
secrets:si está soportado; de lo contrario, bind mount desde un directorio protegido del host con permisos estrictos. - Gestor externo: obtener en runtime usando identidad; escribir en tmpfs; montar en el contenedor.
- Swarm:
- Rotar mientras migras: no reuses valores “solo para el corte”. Asume que las rutas env antiguas están comprometidas.
- Bloquear superficies de observabilidad: elimina endpoints de volcado de config; redacta logs; restringe quién puede inspeccionar contenedores.
-
Añadir guardrails en CI: que fallen los builds si se detecta
.envo patrones de secretos en el contexto de build o repo. - Practicar rotación: realiza ejercicios de rotación trimestrales. La primera vez que rotas no debe ser durante un incidente.
Checklist: cómo se ve “bien” en producción
- Los secretos no aparecen en la salida de
docker inspect. - Los secretos no están en imágenes (verificado con
docker historyy controles del contexto de build). - Los secretos no están en logs (verificados por muestreo y escaneos automáticos).
- La rotación es una operación rutinaria de despliegue, con runbooks y rollback probado.
- El acceso al socket Docker / metadatos del orquestador está auditado y minimizado.
- Los secretos son únicos por entorno y, idealmente, por identidad de servicio.
Preguntas frecuentes
1) ¿Son aceptables alguna vez las variables de entorno para secretos?
A veces, para apps legacy, tokens de corta duración o una fase de transición. Pero trátalo como deuda técnica con fecha límite.
Si es de larga duración y de alto impacto (contraseña admin de BD, clave de firma), no la pongas en env.
2) ¿Un archivo secreto montado no sigue siendo legible por la app, cuál es la diferencia?
La diferencia es la superficie de exposición. Las env se filtran hacia metadatos, salida de inspección y patrones de logging accidentales con más frecuencia.
Los archivos pueden tener permisos, excluirse de diagnósticos y rotarse por reemplazo sin reescribir configuraciones.
3) ¿Funcionan los secretos Docker sin Swarm?
No en el sentido completo administrado por Swarm. Compose soporta un concepto de secretos, pero dependiendo del modo puede convertirse en un bind mount.
Eso puede ser mejor que las env vars, pero debes entender dónde vive el texto plano en disco.
4) ¿Qué hay de los patrones DB_PASSWORD_FILE—son seguros?
Son un compromiso pragmático: la env contiene solo una ruta de archivo, no el valor secreto. El secreto aún necesita una fuente segura y montaje.
Este patrón también mantiene la configuración de la app consistente entre plataformas.
5) ¿Cómo evito que los secretos se horneen en imágenes?
Excluye archivos secretos con .dockerignore, evita pasar secretos vía ARG y usa montajes de secretos de BuildKit para necesidades de build.
Luego verifica con docker history --no-trunc y escaneo del registro.
6) ¿Cómo deberíamos rotar secretos con mínimo downtime?
Usa rotación en dos fases cuando sea posible: introduce una credencial nueva, despliega soporte para ambas, cambia el tráfico y luego retira la antigua.
Para secretos de Swarm, eso típicamente significa crear _v2 y actualizar el servicio, luego retirar _v1.
7) Si alguien vio el secreto en logs una vez, ¿realmente hace falta rotar?
Sí. Los logs se replican, persisten y se acceden por razones no relacionadas con seguridad. Si apareció una vez, no puedes garantizar que haya desaparecido por completo.
Rota y arregla la ruta de logging.
8) ¿Qué permisos deberían tener los archivos secretos dentro de los contenedores?
Solo lectura para el proceso que los necesita, idealmente legibles por grupo con un grupo dedicado. Evita lectura por todo el mundo.
No ejecutes toda la app como root solo para leer un archivo.
9) ¿Cómo convenzo a un equipo de que “repo privado” no es un almacén de secretos?
Pregunta quién tiene acceso hoy, quién lo tendrá el próximo trimestre y dónde viven los clones. Los repos están diseñados para replicarse.
Los almacenes de secretos están diseñados para controlar acceso y auditarlo.
10) ¿Los secretos como archivos lo solucionan todo?
No. Reducen rutas comunes de fuga. Aún necesitas mínimos privilegios, segmentación, credenciales de corta duración cuando sea posible y higiene de logging.
Pero es un buen valor predeterminado que previene muchas heridas autoinfligidas.
Conclusión: siguientes pasos que realmente reducen el riesgo
Deja de tratar a .env como una conveniencia inofensiva. En producción, es un imán de responsabilidad: se filtra a repos, logs, artefactos
y salida de inspección. Y lo peor es lo normal que se siente—hasta que deja de serlo.
Pasos prácticos siguientes:
- Audita servicios en ejecución por vars secretas con
docker inspect; elimínalas. - Cambia secretos a montajes de archivos (
/run/secrets) vía secretos Swarm, secretos de Compose o un gestor externo. - Rota cualquier cosa que haya vivido en
.env, logs de CI o bundles diagnósticos. - Restringe quién puede acceder al socket Docker y quién puede leer logs; audita ambos.
- Haz de la rotación una acción rutinaria de despliegue, no un ritual de emergencia.
No necesitas seguridad perfecta. Necesitas sistemas que no copien casualmente tus joyas de la corona en cada herramienta que esté cerca.
Eso es lo que “secretos sin filtraciones” significa en el mundo real.