Mover una contraseña desde el código hacia process.env evita dejarla escrita en ese archivo. Es una mejora, pero todavía quedan preguntas: ¿de dónde viene el valor?, ¿quién puede leerlo?, ¿termina en los logs?, ¿cómo se reemplaza si se filtra?
Una variable de entorno entrega un valor a un proceso. Un gestor de secretos administra quién puede obtenerlo y cómo se mantiene durante su vida útil. Pueden trabajar juntos. El problema aparece cuando confundimos cambiar la ubicación de una credencial con protegerla.
Vamos a seguir el recorrido de una credencial desde su almacenamiento hasta una aplicación Node.js. Veremos dónde puede quedar expuesta, cómo cargarla sin imprimirla y qué cambia al ejecutar contenedores o pipelines. Todos los valores de los ejemplos son ficticios y no permiten acceder a ningún servicio.
Configuración y secretos: importa lo que permite hacer el valor
La configuración determina cómo se comporta una aplicación. Un secreto es información cuyo conocimiento permite realizar operaciones restringidas o acceder a datos protegidos. Parte de la configuración puede contener secretos.
| Valor | Tratamiento habitual | Motivo |
|---|---|---|
| Puerto HTTP o nivel de log | Configuración | Ajusta el funcionamiento; no concede acceso por sí solo. |
| URL pública de una API | Configuración | El cliente necesita conocerla para conectarse. |
| Token privado de un proveedor | Secreto | Permite actuar con los permisos de su titular. |
| Contraseña o clave privada | Secreto | Permite autenticarse, firmar o descifrar, según su uso. |
| URL de base de datos con contraseña | Secreto completo | Mezcla dirección de conexión y credenciales. |
No clasifiques solo por el nombre: DATABASE_URL puede ser sensible aunque no incluya la palabra SECRET. A su vez, un identificador público no se vuelve secreto por llamarlo API_KEY; revisa el contrato del proveedor y sus restricciones.
Separar configuración ordinaria de credenciales también facilita asignar permisos distintos. Por ejemplo, Microsoft recomienda no utilizar Key Vault como almacén general de configuración.
Qué es realmente una variable de entorno
Un proceso se inicia con un conjunto de nombres y valores proporcionados por su entorno de ejecución. En Node.js accedemos a ellos mediante process.env. Los valores presentes son cadenas: "false" no es el booleano false, y un puerto necesita conversión y validación. Documentación de variables de entorno de Node.js.
Un archivo .env contiene pares de texto que una herramienta puede cargar en ese entorno. El archivo no se cifra por tener esa extensión. Tampoco Node carga cualquier .env automáticamente: hay que usar un mecanismo como --env-file, una biblioteca o la integración del framework.
Conviene distinguir tres momentos:
No se pudo mostrar el diagrama. Puedes consultar su definición.
---
config:
flowchart:
wrappingWidth: 220
---
flowchart TB
A["`**Almacenamiento**
Gestor o archivo protegido`"]
B["`**Entrega al proceso**
Entorno o archivo montado`"]
C["`**Uso en la aplicación**
Cliente del servicio con la credencial`"]
A --> B --> C
Son decisiones diferentes. Un gestor puede entregar un secreto mediante una variable de entorno; una aplicación también puede recuperarlo con un SDK y pasarlo directamente al componente que lo necesita.
El entorno de un proceso en ejecución no se actualiza automáticamente porque alguien cambie el valor en un panel. Hay que definir un reinicio, una nueva inyección o una recarga explícita. Lo mismo ocurre si el código lee un archivo una sola vez y conserva su contenido.
Qué protege una variable y qué no
Separar un valor del código permite cambiarlo entre despliegues sin editar la aplicación. Sin embargo, el código que corre dentro del proceso puede acceder a los secretos que ese proceso recibe. Un gestor no impide que una dependencia comprometida lea una credencial después de entregarla a la aplicación.
También hay propagación accidental. Los procesos hijos de Node heredan process.env por defecto si no se proporciona otro env. Lanzar una herramienta auxiliar puede entregarle credenciales que no necesita. API de procesos hijos de Node.js.
La inspección desde otros procesos depende del sistema operativo, el usuario, los privilegios y el aislamiento. No significa que cualquier visitante de una web pueda leer las variables del servidor. Sí exige revisar depuradores, diagnósticos y acceso administrativo al runtime.
OWASP recomienda preferir alternativas como archivos montados o recuperación desde el gestor cuando sea posible, y advierte de la exposición del entorno en logs y volcados. OWASP: inyección de secretos en contenedores.
Usar variables de entorno no es incorrecto por sí mismo. Mi criterio práctico es evaluar los controles de la plataforma, las identidades que pueden inspeccionar el proceso y las alternativas disponibles. Cambiar una variable por un archivo legible por todo el contenedor tampoco representa una mejora automática.
Las filtraciones suelen ocurrir antes de la conexión
Archivos .env y repositorios
Un .env puede terminar dentro del directorio público del servidor, en un ZIP de soporte, en una imagen Docker o en un commit. Guárdalo fuera de las rutas servidas y limita quién puede leerlo. En desarrollo, usa credenciales de desarrollo con permisos acotados.
Esta base para .gitignore permite versionar una plantilla sin valores sensibles:
.env
.env.*
!.env.example
secrets/La excepción solo es segura si .env.example contiene nombres, valores públicos y ejemplos ficticios. Git aclara que las reglas de exclusión no afectan a archivos ya versionados. Añadir la regla después de una filtración no retira las copias existentes.
Si se publicó una credencial, revócala o rótala en el servicio que la acepta y revisa su uso. Después coordina la limpieza de historial y copias cuando corresponda; un git revert no elimina la credencial del historial ni invalida su acceso. GitHub: retirar datos sensibles de un repositorio.
Logs, errores y herramientas de diagnóstico
Evita volcar process.env, el objeto completo de configuración o las cabeceras de una petición. Los errores de un SDK también pueden incluir URL, parámetros o datos de conexión: no los serialices íntegros sin revisar su contenido.
Registra el evento, no el valor: «falló la carga del secreto de pagos» resulta útil; la credencial no. OWASP identifica contraseñas, access tokens, cadenas de conexión y claves entre los datos que deben excluirse o tratarse antes de registrar. Aplica la misma decisión a logs, métricas y trazas, incluidos los atributos que envías a servicios externos.
Código y artefactos producidos durante el build
Un valor disponible durante la compilación puede acabar en JavaScript, HTML estático, mapas de código fuente o artefactos. Si se entrega al navegador, debes tratarlo como público, aunque esté minificado.
En Astro, las variables con prefijo PUBLIC_ pueden usarse en el cliente; no coloques allí una credencial privada. Además, un valor exclusivo del servidor puede filtrarse si tú lo escribes en el HTML prerenderizado. El prefijo controla disponibilidad, no evita toda serialización accidental. Astro: variables de entorno.
La consecuencia práctica es separar permisos: construir un frontend no debería requerir la contraseña de producción de su backend. Si el build necesita leer un CMS privado, dale una credencial de lectura específica y comprueba qué datos se publican.
Qué aporta un gestor de secretos
Un gestor centraliza almacenamiento y distribución, autentica a los consumidores y aplica permisos. Según el producto, también ofrece versiones, auditoría y mecanismos de renovación o revocación. Guardar una contraseña en un gestor no significa que el servicio externo ya la haya cambiado. La rotación necesita integración y coordinación.
OWASP recomienda administrar ese ciclo de vida, limitar acceso y auditar operaciones. Son recomendaciones de seguridad, no una obligación legal universal. OWASP Secrets Management Cheat Sheet.
Al comparar alternativas, mira cómo se autentica tu aplicación y qué trabajo operativo asume el equipo:
| Alternativa | Qué considerar |
|---|---|
| Azure Key Vault | Integración con Microsoft Entra ID, identidades administradas y permisos de acceso a secretos. |
| HashiCorp Vault | Motores de secretos y credenciales dinámicas con concesiones temporales; requiere diseñar autenticación y operación del servicio. |
| AWS Secrets Manager | Integración con IAM, cifrado, caché e integraciones de rotación según el secreto. |
| Google Cloud Secret Manager | Permisos IAM y versiones explícitas; la aplicación debe decidir cómo consume y actualiza cada versión. |
Las capacidades y sus condiciones están descritas por Azure, Vault, AWS y Google Cloud. No presupongas que todos los almacenes tienen el mismo comportamiento: Vault, por ejemplo, distingue las concesiones de secretos dinámicos de los valores arbitrarios guardados en su motor key/value.
Si buscas una implementación con CLI y SDK, la guía de gestión de secretos con Infisical desarrolla ese camino. Aquí nos interesa el diseño que debe mantenerse con cualquiera de estas herramientas.
Identidad y mínimo privilegio antes de elegir el canal
La aplicación necesita demostrar quién es para pedir un secreto. Si la plataforma lo admite, utiliza una identidad administrada o federación de identidades en lugar de distribuir otra contraseña permanente para abrir el gestor.
Azure recomienda identidades administradas para acceder a Key Vault y distingue permisos del plano de control de los del plano de datos. Poder administrar la configuración de un recurso no debe confundirse con la decisión de quién puede leer sus secretos. Seguridad de Key Vault.
Para una API de pedidos, un diseño sencillo podría ser:
- La identidad de producción lee únicamente la credencial del proveedor que utiliza.
- La identidad de desarrollo no tiene acceso a secretos de producción.
- El pipeline despliega la aplicación, pero no necesita obtener su contraseña de base de datos.
- La identidad de rotación puede actualizar la credencial; la aplicación no necesita ese permiso.
Son decisiones de diseño para este ejemplo. Compruébalas con pruebas de acceso permitido y denegado, no solo mirando nombres de carpetas. El principio es el mismo que aplicamos al control de acceso con RBAC y ABAC.
Node.js: cargar y validar sin revelar el secreto
Este ejemplo funciona con Node.js 22 o superior, sin dependencias. Usa una terminal compatible con Bash en una carpeta nueva. El programa comprueba configuración y termina; no envía credenciales ni llama a servicios externos.
Guarda lo siguiente como config.mjs:
import { readFile } from 'node:fs/promises';
export async function loadConfig(env = process.env) {
const rawPort = env.APP_PORT ?? '3000';
const port = Number(rawPort);
if (!/^\d+$/.test(rawPort) || port < 1 || port > 65535) {
throw new Error('APP_PORT debe ser un puerto válido');
}
const hasToken = env.API_TOKEN !== undefined;
const hasFile = env.API_TOKEN_FILE !== undefined;
if (hasToken === hasFile) {
throw new Error('Define solo API_TOKEN o API_TOKEN_FILE');
}
let apiToken;
if (hasFile) {
if (!env.API_TOKEN_FILE) {
throw new Error('API_TOKEN_FILE no puede estar vacío');
}
try {
apiToken = await readFile(env.API_TOKEN_FILE, 'utf8');
} catch {
throw new Error('No se pudo leer el archivo del secreto');
}
} else {
apiToken = env.API_TOKEN;
}
// Contrato de este ejemplo: token opaco, no vacío y de una línea.
if (!apiToken || /[\r\n]/.test(apiToken)) {
throw new Error('API_TOKEN debe ser no vacío y de una línea');
}
return { port, apiToken };
}Y este punto de entrada como app.mjs:
import { loadConfig } from './config.mjs';
try {
const config = await loadConfig();
// Entrega config.apiToken solo al cliente del servicio que lo necesite.
console.info(`Configuración válida; puerto ${config.port}.`);
} catch {
console.error('No se pudo iniciar: revisa la configuración requerida.');
process.exitCode = 1;
}El cargador rechaza fuentes ambiguas y no devuelve un secreto de respaldo cuando falta el esperado. No modifica el valor con trim(): un salto de línea produce un error según el contrato elegido. Para certificados o secretos multilínea necesitarías otro contrato. La ruta del archivo procede de configuración confiable del despliegue, nunca de un parámetro enviado por un usuario.
Entrega mediante un .env de desarrollo
En la carpeta del ejemplo, crea un archivo que solo contiene un valor ficticio:
umask 077
cat > .env <<'ENV'
APP_PORT=3000
API_TOKEN=credencial-ficticia-sin-acceso
ENV
node --env-file=.env app.mjsLa salida esperada es Configuración válida; puerto 3000.. Node documenta que una variable ya presente en el entorno tiene prioridad sobre el mismo nombre cargado con --env-file. Ejecuta el ejemplo sin API_TOKEN ni API_TOKEN_FILE heredados para evitar interferencias.
umask 077 restringe los permisos iniciales de archivos nuevos en esa sesión Unix; no cambia los permisos de archivos preexistentes ni sustituye los controles del equipo. No pegues un token real en un comando que pueda guardarse en el historial de la terminal.
Entrega mediante un archivo montado
La misma aplicación acepta un archivo. En el laboratorio, créalo con datos ficticios y sin un salto de línea final:
mkdir -p secrets
printf '%s' 'credencial-ficticia-sin-acceso' > secrets/api-token.txt
API_TOKEN_FILE=./secrets/api-token.txt node app.mjsAquí no usamos --env-file, por lo que el token del archivo .env no se carga. La variable contiene una ruta, no la credencial. _FILE es una convención implementada por nuestro código; Node no la interpreta automáticamente.
El secreto sigue existiendo en memoria después de leerlo. Pasarlo por archivo reduce ciertos riesgos de propagación del entorno, pero no lo oculta al proceso autorizado. Tampoco esta validación demuestra que el token esté vigente: eso lo determina el servicio que lo acepta.
Contenedores: montar al ejecutar, no incrustar al construir
Docker Compose permite conceder un secreto a servicios concretos y montarlo bajo /run/secrets/. Para ejecutar el ejemplo anterior, guarda compose.yaml junto a los dos archivos JavaScript:
services:
config-check:
image: node:22-bookworm-slim
working_dir: /app
command: ["node", "app.mjs"]
environment:
APP_PORT: "3000"
API_TOKEN_FILE: /run/secrets/api_token
volumes:
- ./app.mjs:/app/app.mjs:ro
- ./config.mjs:/app/config.mjs:ro
secrets:
- api_token
network_mode: none
read_only: true
cap_drop: ["ALL"]
security_opt:
- no-new-privileges:true
secrets:
api_token:
file: ./secrets/api-token.txtCon Docker y Compose disponibles, ejecuta docker compose run --rm config-check. Es un comprobador local que termina, no una plantilla completa de despliegue. En producción también define un usuario no privilegiado y permisos de lectura compatibles para el archivo montado; prueba esa combinación en tu runtime.
Compose utiliza aquí un archivo del host: no hemos creado una bóveda cifrada ni configurado rotación automática. Protege el archivo de origen y limita el acceso al daemon de Docker. Docker: uso de secretos en Compose.
Para construir una imagen, excluye secretos del contexto con .dockerignore; .gitignore no cumple esa función:
.git
.env
.env.*
secrets/
node_modules/No copies el secreto y lo borres en una instrucción posterior: puede permanecer en una capa anterior. Tampoco uses ARG o ENV del Dockerfile para transportar credenciales privadas de compilación. Para una instalación npm que necesite un registro privado, BuildKit permite un montaje temporal:
# Fragmento: package.json y package-lock.json ya están copiados.
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc,required=true \
npm ci --ignore-scriptsEl constructor recibe el archivo con docker build --secret id=npmrc,src=/ruta/protegida/npmrc .; la ruta debe apuntar a un archivo provisionado de forma segura, fuera del contexto. El montaje no se incorpora por sí mismo a la imagen, pero un comando podría copiarlo o imprimirlo. --ignore-scripts evita ejecutar scripts de instalación en ese paso; si tu proyecto los necesita, evalúalos y ejecútalos sin acceso al secreto cuando sea posible. Docker: secretos de build.
CI/CD: dar credenciales solo al trabajo que las necesita
Un pipeline ejecuta código con permisos. Si puede leer un secreto, los scripts y acciones ejecutados en ese contexto también podrían hacerlo. El enmascaramiento de logs ayuda, pero no es una barrera de acceso ni garantiza cubrir valores transformados. GitHub recomienda revisar el código que se ejecuta, reducir permisos y proteger el uso de secretos en workflows. Uso seguro de GitHub Actions.
Separa pruebas de código no confiable de despliegues con credenciales, controla quién modifica los workflows y restringe los entornos de producción. No entregues secretos de producción a una ejecución solo porque procede de un pull request.
Cuando el proveedor lo soporte, OIDC permite al job obtener credenciales temporales mediante una relación de confianza, en lugar de guardar una clave cloud permanente en el repositorio. El proveedor debe validar emisor, audiencia y condiciones que identifiquen el repositorio y entorno o rama autorizados. GitHub Actions y OpenID Connect.
Esto no elimina la autorización: una relación de confianza demasiado amplia permite que otros trabajos soliciten el mismo acceso. Concede a la identidad resultante solo las operaciones necesarias. Si todavía necesitas un secreto estático, limítalo al job y paso que lo consumen, y contempla su revocación.
Rotar y revocar sin dejar copias antiguas funcionando
Rotar sustituye una credencial por otra. Revocar impide seguir utilizando una credencial. Eliminar un valor del gestor puede impedir nuevas lecturas, pero no necesariamente invalida las copias que ya entregaste: la revocación debe alcanzar al sistema que acepta esa credencial.
En una rotación planificada de un token de integración, propongo esta secuencia:
- Crear la nueva credencial con los permisos previstos.
- Guardarla como nueva versión y actualizar los consumidores.
- Reiniciar o recargar todas las instancias; comprobar conexiones con la nueva versión.
- Revocar la anterior y confirmar que ya no funciona.
- Registrar quién realizó el cambio y su resultado, sin registrar los valores.
El solapamiento solo es posible si el proveedor admite credenciales simultáneas y el riesgo lo permite. Ante una filtración, puede ser necesario revocar inmediatamente y aceptar una interrupción controlada. No hay una periodicidad universal: depende de exposición, caducidad, capacidades del proveedor y consecuencias de un compromiso.
Las credenciales dinámicas pueden simplificar esa retirada. Vault entrega ciertos secretos con una concesión temporal —lease— que permite renovación y revocación; no todos sus motores se comportan igual. Vault: leases, renovación y revocación.
También hay que decidir qué ocurre si el gestor no responde. Una aplicación que carga al iniciar puede seguir usando su valor en memoria, mientras otra instancia nueva no podrá arrancar. Define caché, vigencia y respuesta a fallos conscientemente; no recurras a una credencial hardcodeada. Google recomienda consumir versiones concretas y coordinar su despliegue para controlar los cambios.
No confundas esta transición con destruir claves de cifrado: podrías necesitarlas para recuperar datos antiguos. Ese caso requiere una estrategia distinta, como la que vimos en rotación de claves criptográficas.
Diseñar el recorrido completo del secreto
La pregunta útil no es solamente «¿archivo o variable?». Es quién emite la credencial, dónde permanece, quién puede obtenerla, qué proceso la recibe y cómo deja de servir cuando ya no debería existir acceso.
Para una aplicación pequeña, el almacén de secretos de la plataforma y una inyección al iniciar pueden ser suficientes si los permisos, el aislamiento y la renovación están bien definidos. Cuando hay más consumidores o necesidades de auditoría y rotación, centralizar la gestión evita mantener copias independientes sin dueño. Evalúa el mecanismo completo y prueba sus fallos, no solo el camino exitoso.
Para limpiar el laboratorio, elimina únicamente la carpeta nueva que creaste con estos ejemplos. No se han utilizado credenciales reales. Se comprobaron la carga por entorno y archivo, la prioridad del entorno y el rechazo de valores ausentes, ambiguos o inválidos, sin imprimir el token. La configuración de Compose se validó, pero no se ejecutó el contenedor porque el daemon de Docker no estaba disponible. El fragmento de BuildKit es ilustrativo y requiere un proyecto con su registro configurado.
Referencias
- OWASP Secrets Management Cheat Sheet.
- OWASP Logging Cheat Sheet: datos que excluir.
- Node.js: variables de entorno,
--env-filey procesos hijos. - Git: gitignore y GitHub: retirada de datos sensibles.
- Astro: variables de entorno.
- Docker: secretos en Compose y secretos de build.
- Microsoft: proteger Azure Key Vault.
- HashiCorp Vault: leases, renovación y revocación.
- AWS Secrets Manager: buenas prácticas.
- Google Cloud Secret Manager: buenas prácticas.
- GitHub Actions: uso seguro y OpenID Connect.
