El módulo 10 terminó con una constatación incómoda: Escena Viva funciona. Reparte carga entre trabajadores, cachea el catálogo en Redis, procesa las colas de emisión de entradas en un proceso aparte, mide su propio percentil 99 y expone una API REST y otra GraphQL. Y todo eso sigue corriendo en un portátil, con los secretos en un fichero .env que solo existe en tu disco, sin registros centralizados, sin supervisor, sin contenedor y sin despliegue automático. Este módulo cierra esa distancia. Y empieza por donde hay que empezar: por la configuración. No porque sea lo más vistoso, sino porque es lo primero que se rompe cuando una aplicación sale del portátil. La misma base de código tiene que arrancar en tu máquina apuntando a una base de datos local, en el servidor de integración continua apuntando a contenedores efímeros, en preproducción apuntando a una copia de los datos reales y en producción apuntando a la base de datos donde Lucia y Marc compran de verdad sus entradas para el Festival de Jazz de Primavera. Una sola base de código, cuatro comportamientos. La diferencia está entera en la configuración.
Contenido
- Los doce factores aplicados a la configuración
process.envde verdaddotenvy sus límites- Validar al arrancar y fallar rápido
- Configuración por entorno sin duplicar ficheros
- Qué nunca va en una variable de entorno
- Gestión de secretos en serio
- Rotación de secretos sin parada
- Cuando un secreto se filtra
- Configuración en caliente frente a reinicio
- Tabla de referencia de las variables de Escena Viva
- Los doce factores aplicados a la configuración
The Twelve-Factor App es un manifiesto de 2011 que describe cómo debería construirse una aplicación que se despliega en servicios en la nube. Su factor III dice, en una frase: guarda la configuración en el entorno. La idea de fondo es separar lo que cambia entre despliegues de lo que no. El código de Escena Viva es idéntico en tu portátil y en producción: el mismo crearAplicacion, los mismos repositorios, el mismo cálculo de aforo. Lo que cambia es a qué base de datos se conecta, con qué clave firma los JWT, en qué puerto escucha y cuántos trabajadores arranca. Eso es configuración. La prueba práctica para distinguir una cosa de otra es la que los doce factores llaman la prueba del repositorio público:
¿Podrías hacer público el repositorio de Escena Viva ahora mismo, sin cambiar nada, sin comprometer ninguna credencial?
Si la respuesta es no, tienes configuración dentro del código. No importa si está en un config.json, en un objeto JavaScript o en un comentario: si está versionado y es secreto, has fallado la prueba. Ojo con el matiz: la prueba habla de credenciales, no de cualquier valor que varíe. El aforo máximo de la Sala Bóveda no es configuración por entorno: es una regla de negocio, y vive en el dominio o en la base de datos. Un error muy común es convertir en variable de entorno todo lo que en algún momento podría cambiar, y acabar con cuarenta variables que nadie sabe para qué sirven.
| Es configuración | No es configuración |
|---|---|
| URL de PostgreSQL, MongoDB, Redis | Nombre de las tablas y colecciones |
JWT_SECRETO, claves de API de terceros |
Duración de negocio de un token de refresco decidida por producto |
| Puerto de escucha, número de trabajadores | Reglas de aforo, precios base, tipos de sala |
| Origen permitido por CORS | Cabeceras de seguridad que siempre son las mismas |
| Nivel de registro | Formato del identificador de entrada EV-<año>-<6 dígitos> |
La regla mental: si dos despliegues del mismo código necesitan valores distintos, es configuración. Si todos los despliegues necesitan el mismo valor, es código.
process.env de verdad
process.env de verdadNode expone las variables de entorno del proceso en process.env. Es un objeto normal, pero tiene tres características que causan la mayoría de los errores. Todo son cadenas. Siempre. No hay números, ni booleanos, ni nulos.
// Suponiendo: PUERTO=3000 DEPURAR=false TRABAJADORES=0
typeof process.env.PUERTO; // 'string'
process.env.PUERTO + 1; // '30001' (concatenacion, no suma)
Boolean(process.env.DEPURAR); // true (la cadena 'false' es veraz)
Number(process.env.TRABAJADORES) || 4; // 4 (el 0 se pierde)Las tres líneas son errores reales que se ven en producción. La última es especialmente traicionera: || 4 parece un valor por defecto razonable hasta que alguien pide explícitamente cero trabajadores y obtiene cuatro. Por eso más adelante convertiremos tipos con un esquema, no a mano. undefined no es lo mismo que cadena vacía. Una variable que no existe da undefined; una variable declarada sin valor da ''. Ambas son falsas en un if, pero significan cosas distintas: la primera es «no me lo has dicho», la segunda es «te digo explícitamente que está vacío».
node -e "console.log(process.env.ORIGEN_CORS)" # undefined
ORIGEN_CORS= node -e "console.log(JSON.stringify(process.env.ORIGEN_CORS))" # ""Mayúsculas por convención. No es una regla de Node, es una convención de Unix heredada: las variables de entorno van en MAYUSCULAS_CON_GUION_BAJO. Respétala aunque el resto del proyecto use camelCase; quien lea un docker-compose.yml o un panel de Heroku espera ese formato.
De dónde salen las variables
Esto es lo que suele quedar borroso. Las variables no vienen de un sitio: vienen del proceso padre, sea cual sea.
| Origen | Cuándo se usa | Ejemplo |
|---|---|---|
| Shell interactiva | Pruebas puntuales | PUERTO=4000 npm start |
Fichero .env + dotenv |
Desarrollo local | .env en la raíz del proyecto |
| Unidad de systemd | VPS o servidor propio | Environment= o EnvironmentFile= |
| Ecosistema de PM2 / motor de contenedores | Supervisor o Docker | env_production (11-03), ENV/-e (11-04) |
| Panel del proveedor PaaS | Heroku, Render, Fly | Variables de configuración (lección 11-05) |
| Secretos de CI | Tuberías | secrets de GitHub Actions (lección 11-06) |
Todos acaban en el mismo sitio: process.env. Por eso la aplicación no tiene que saber cuál de ellos la ha arrancado. Ese desacoplamiento es justo lo que hace que el mismo código funcione en los siete escenarios.
dotenv y sus límites
dotenv y sus límitesEn desarrollo, exportar quince variables a mano en cada terminal es insufrible. dotenv lee un fichero .env y vuelca su contenido en process.env. El fragmento inicial de src/config/index.js, ya existente desde el módulo 6, es simplemente require('dotenv').config();. Dos límites que hay que tener clarísimos:
dotenvno sobrescribe lo que ya existe enprocess.env. Si el entorno real ya definePUERTO, el.envse ignora para esa variable. Es el comportamiento correcto: el entorno real manda sobre el fichero de comodidad.dotenvno es un gestor de secretos. Es un fichero de texto plano en tu disco. En producción no debe existir. Lo que hay en producción son variables inyectadas por el orquestador, el supervisor o la plataforma.
Desde Node 20 existe además node --env-file=.env, que hace lo mismo sin dependencia. Mantenemos dotenv en Escena Viva porque el proyecto ya lo usa y porque su comportamiento es idéntico en todas las versiones que soportamos, pero conviene saber que la alternativa nativa existe.
.env fuera, .env.example dentro
El .gitignore de Escena Viva ignora .env y todas sus variantes locales. Lo que sí se versiona es .env.example: la lista completa de variables que la aplicación necesita, con valores falsos o vacíos. Es documentación ejecutable, y es lo primero que mira alguien que se incorpora al equipo.
# .env.example — copialo a .env y rellena los valores.
# NUNCA pongas valores reales aqui: este fichero SI esta versionado.
NODE_ENV=development
PUERTO=3000
NIVEL_REGISTRO=debug
# Bases de datos
URL_POSTGRES=postgres://escena:escena@localhost:5432/escena_viva
URL_MONGO=mongodb://localhost:27017/escena_viva
URL_REDIS=redis://localhost:6379
# Seguridad — genera valores con: openssl rand -hex 32
JWT_SECRETO=
JWT_SECRETO_ANTERIOR=
SESION_SECRETO=
CSRF_SECRETO=
# Red, procesos y observabilidad (11-02)
ORIGEN_CORS=http://localhost:5173
CONFIAR_EN_PROXY=false
NUMERO_TRABAJADORES=0
CONCURRENCIA_COLA=5
TOKEN_METRICAS=Regla operativa: cada vez que alguien añade una variable al código, la añade a .env.example en el mismo commit. Si no, el siguiente que clone el repositorio descubrirá la variable que falta cuando la aplicación reviente.
- Validar al arrancar y fallar rápido
Aquí está el corazón de la lección. El fallo clásico de configuración no es que una variable falte: es cuándo te enteras de que falta. Imagina que JWT_SECRETO no está definido en producción. Sin validación, la aplicación arranca perfectamente. Sirve el catálogo, muestra las siete sesiones del Festival de Jazz, deja navegar. Tres horas después, Lucia intenta iniciar sesión para comprar dos entradas y jsonwebtoken lanza secretOrPrivateKey must have a value. Un error 500, un cliente perdido y un registro que no dice nada útil. Con validación al arrancar, la aplicación no arranca. El despliegue falla en el primer segundo, la plataforma no envía tráfico a la instancia rota y el mensaje dice exactamente qué falta. Eso es fail fast. Ampliamos src/config/index.js con un esquema zod (la misma librería que ya usamos para validar peticiones en el módulo 6, así que no añadimos dependencias).
// src/config/index.js
'use strict';
require('dotenv').config();
const { z } = require('zod');
// Ayudas de conversion: recuerda que TODO llega como cadena.
const comoEntero = (porDefecto) =>
z.coerce.number().int().nonnegative().default(porDefecto);
const comoBooleano = (porDefecto) =>
z.enum(['true', 'false']).default(String(porDefecto)).transform((v) => v === 'true');
// Un secreto util tiene longitud suficiente: 32 caracteres hex como minimo.
const secretoFuerte = z.string().min(32, 'minimo 32 caracteres: openssl rand -hex 32');
const esquemaConfiguracion = z.object({
NODE_ENV: z.enum(['development', 'test', 'staging', 'production']).default('development'),
PUERTO: comoEntero(3000),
NIVEL_REGISTRO: z.enum(['trace', 'debug', 'info', 'warn', 'error', 'fatal']).default('info'),
URL_POSTGRES: z.string().url(),
URL_MONGO: z.string().url(),
URL_REDIS: z.string().url(),
JWT_SECRETO: secretoFuerte,
// Opcional: solo existe durante una rotacion (apartado 8).
JWT_SECRETO_ANTERIOR: secretoFuerte.optional(),
SESION_SECRETO: secretoFuerte,
CSRF_SECRETO: secretoFuerte,
ORIGEN_CORS: z.string().default('http://localhost:5173'),
CONFIAR_EN_PROXY: comoBooleano(false),
NUMERO_TRABAJADORES: comoEntero(0),
CONCURRENCIA_COLA: comoEntero(5),
TOKEN_METRICAS: z.string().min(16).optional(),
});
const resultado = esquemaConfiguracion.safeParse(process.env);
if (!resultado.success) {
// Ni logger ni nada elaborado: aqui todavia no hay aplicacion.
const problemas = resultado.error.issues
.map((i) => ` - ${i.path.join('.')}: ${i.message}`)
.join('\n');
process.stderr.write(
`\nConfiguracion invalida. Escena Viva no puede arrancar:\n${problemas}\n\n` +
'Revisa .env.example y las variables del entorno de despliegue.\n\n'
);
process.exit(1);
}
const bruta = resultado.data;
// La forma del objeto NO es la del entorno: el resto del codigo pide
// configuracion.seguridad.jwtSecreto, nunca process.env.JWT_SECRETO.
const configuracion = Object.freeze({
entorno: bruta.NODE_ENV,
esProduccion: bruta.NODE_ENV === 'production',
esPrueba: bruta.NODE_ENV === 'test',
puerto: bruta.PUERTO,
nivelRegistro: bruta.NIVEL_REGISTRO,
confiarEnProxy: bruta.CONFIAR_EN_PROXY,
origenesCors: bruta.ORIGEN_CORS.split(',').map((origen) => origen.trim()),
baseDatos: Object.freeze({
postgres: bruta.URL_POSTGRES, mongo: bruta.URL_MONGO, redis: bruta.URL_REDIS,
}),
seguridad: Object.freeze({
jwtSecreto: bruta.JWT_SECRETO,
jwtSecretoAnterior: bruta.JWT_SECRETO_ANTERIOR,
sesionSecreto: bruta.SESION_SECRETO,
csrfSecreto: bruta.CSRF_SECRETO,
}),
procesos: Object.freeze({
numeroTrabajadores: bruta.NUMERO_TRABAJADORES,
concurrenciaCola: bruta.CONCURRENCIA_COLA,
}),
observabilidad: Object.freeze({ tokenMetricas: bruta.TOKEN_METRICAS }),
});
module.exports = { configuracion };Qué está haciendo cada decisión:
z.coerce.number()convierte la cadena a número dentro del esquema, así que nunca vuelves a escribirNumber(process.env.ALGO)disperso por el código.comoBooleanoacepta solo'true'o'false': si alguien escribeCONFIAR_EN_PROXY=si, el arranque falla con un mensaje claro en lugar de interpretarlo como verdadero..default()centraliza los valores por defecto en un único sitio. El|| 4disperso desaparece, y con él el problema del cero.safeParse+process.exit(1)convierte cualquier problema en una muerte inmediata con código de salida distinto de cero, que es lo que PM2, Docker y las PaaS interpretan como «arranque fallido».Object.freezeen todos los niveles impide que un módulo modifique la configuración en caliente y deje al resto del proceso viendo otra cosa.- La forma del objeto exportado no es la del entorno. El resto del código pide
configuracion.seguridad.jwtSecreto, noprocess.env.JWT_SECRETO. Si mañana renombramos la variable, se toca un fichero.
Con JWT_SECRETO ausente, el arranque produce Configuracion invalida. Escena Viva no puede arrancar: - JWT_SECRETO: Required. Cuarenta caracteres de salida que ahorran una tarde entera.
Regla de oro:
process.envse lee exclusivamente ensrc/config/index.js. En cualquier otro fichero está prohibido. Puedes vigilarlo con una regla de ESLint (no-restricted-properties) o con ungrepen la tubería de CI de la lección 11-06.
- Configuración por entorno sin duplicar ficheros
La tentación es crear config.development.js, config.staging.js y config.production.js. Es un error, porque duplica estructura: cuando añades una variable tienes que tocar tres ficheros y siempre olvidas uno, y porque la única diferencia real entre entornos son los valores, no la forma. En Escena Viva hay un solo esquema y los valores llegan de fuera:
| Entorno | NODE_ENV |
De dónde salen los valores |
|---|---|---|
| Desarrollo | development |
.env local con dotenv |
| Prueba | test |
.env.test (módulo 9) y servicios de CI |
| Preproducción | staging |
Panel del proveedor o secretos del orquestador |
| Producción | production |
Gestor de secretos + variables de la plataforma |
Por qué NODE_ENV=production importa más de lo que parece
NODE_ENV no es una variable como las demás: media ecosistema la mira.
- Express desactiva la traza de error en las respuestas, activa el caché de vistas y reduce trabajo por petición. Ejecutar Express con
NODE_ENVsin definir en producción es más lento y más filtrón. - Muchas librerías (validadores, motores de plantillas, React en el frontal) desactivan comprobaciones de desarrollo y avisos.
- npm:
npm ci --omit=devinstala solo las dependencias de producción. Es lo que haremos en la imagen Docker de 11-04 y en la construcción de la PaaS de 11-05. Y por eso todo lo que la aplicación necesita en tiempo de ejecución tiene que estar endependencies, nunca endevDependencies. Unrequirede un paquete de desarrollo revienta en producción y en ningún otro sitio. - Nuestro propio código:
configuracion.esProducciondecide sipino-prettyestá activo (11-02), si las cookies llevansecure, si se exponen las trazas.
Solo valen los cuatro valores del enum. Un NODE_ENV=prod mal escrito ya no arranca la aplicación en lugar de dejarla en modo desarrollo silenciosamente.
- Qué nunca va en una variable de entorno
| Sí | No |
|---|---|
| Cadenas de conexión, claves, tokens | Ficheros grandes o binarios (certificados PEM completos) |
| Interruptores de comportamiento por entorno | Lógica de negocio disfrazada de configuración |
| Nombres de host, puertos, orígenes | Datos personales de usuarios |
| Nivel de registro, banderas de funcionalidad simples | Estructuras complejas en JSON codificado |
Sobre el último punto: CONFIG_SALAS={"teatro-almendra":{"aforo":800}} es una señal de alarma. Si la configuración necesita estructura, lo que necesitas es un fichero montado (que puede venir de un secreto del orquestador) o una tabla en la base de datos, no una variable de entorno con JSON dentro que nadie puede leer ni depurar.
- Gestión de secretos en serio
Las variables de entorno están mejor que el código versionado, pero no son seguras. Se filtran por sitios que no esperas:
- Volcados de proceso. Un core dump contiene el bloque de entorno completo.
- Registros y trazas. Un
console.log(process.env)en una depuración de urgencia, o una librería que vuelca el contexto en un informe de error, y tuJWT_SECRETOacaba en el servicio de registro de terceros. /proc/<pid>/environ. En Linux, cualquier proceso del mismo usuario puede leer el entorno de otro.- Imágenes de contenedor. Un
ENV JWT_SECRETO=...en elDockerfilequeda grabado en una capa de la imagen, y la imagen se publica en un registro (11-04). - La salida de un error. Algunos clientes de base de datos incluyen la URL completa —con usuario y contraseña— en el mensaje de excepción.
- Subprocesos. Todo hijo hereda el entorno del padre por defecto.
Por eso existen los gestores de secretos. El panorama honesto:
| Solución | Cómo funciona | A favor | En contra |
|---|---|---|---|
| Variables del proveedor (Heroku, Render, Fly) | Panel o CLI, inyectadas al arrancar | Cero infraestructura, inmediato | Sin rotación automática ni auditoría fina |
| HashiCorp Vault | Servicio dedicado; la app pide el secreto con un token | Rotación, auditoría, secretos dinámicos y de vida corta | Operar Vault es un proyecto en sí mismo |
| AWS Secrets Manager / GCP Secret Manager | Servicio gestionado del proveedor, con IAM | Integración nativa, rotación programada | Ata al proveedor, coste por secreto y acceso |
| Ficheros montados (secretos de Kubernetes/Swarm) | El secreto aparece como fichero en el contenedor | No aparece en environ ni en la imagen |
Requiere orquestador; hay que leer el fichero |
| Cifrado en el repositorio (SOPS, git-crypt) | Secretos cifrados versionados; se descifran al desplegar | Historial y revisión por PR de los cambios | La clave maestra sigue teniendo que vivir en algún sitio |
Escena Viva empieza con variables del proveedor (11-05) porque el tamaño del proyecto no justifica operar Vault, y deja el camino abierto: como toda la lectura pasa por src/config/index.js, migrar a ficheros montados es cambiar quince líneas en un único fichero. Un patrón intermedio muy útil es el sufijo _FILE: si existe JWT_SECRETO_FILE, se lee el contenido de ese fichero; si no, se usa JWT_SECRETO. Así la misma imagen sirve para una PaaS con variables y para un orquestador con secretos montados.
- Rotación de secretos sin parada
Los secretos caducan. Se rotan porque alguien deja el equipo, porque lo exige una auditoría o porque se han filtrado. El problema es que cambiar JWT_SECRETO de golpe invalida todos los tokens en circulación: Lucia, Marc y los organizadores de las tres salas quedan desconectados a mitad de compra. La solución es aceptar dos claves durante la transición. Se firma siempre con la nueva y se verifica contra ambas.
// src/servicios/tokens.js (fragmento adaptado para la rotacion)
'use strict';
const jwt = require('jsonwebtoken');
const { configuracion } = require('../config/index.js');
const { ErrorAutenticacion } = require('../errores.js');
const { jwtSecreto, jwtSecretoAnterior } = configuracion.seguridad;
// Se firma SIEMPRE con el secreto vigente.
const firmarAcceso = (carga) => jwt.sign(carga, jwtSecreto, { expiresIn: '15m' });
function verificarAcceso(token) {
// Orden importante: primero el vigente (caso mayoritario).
for (const clave of [jwtSecreto, jwtSecretoAnterior].filter(Boolean)) {
try {
return jwt.verify(token, clave);
} catch (error) {
// Token caducado o malformado: no lo tapamos.
if (error.name !== 'JsonWebTokenError') throw error;
}
}
throw new ErrorAutenticacion('Token no valido');
}
module.exports = { firmarAcceso, verificarAcceso };El procedimiento completo, con tokens de acceso de 15 minutos: (1) generar la clave nueva con openssl rand -hex 32; (2) poner el valor actual en JWT_SECRETO_ANTERIOR y el nuevo en JWT_SECRETO; (3) reiniciar con recarga sin cortes (lección 11-03), y desde ese instante se firma con la nueva y se acepta la vieja; (4) esperar más que la vida del token más largo —con acceso de 15 minutos y refresco de 7 días, hasta que caduquen los refrescos o se fuerce su rotación—; y (5) borrar JWT_SECRETO_ANTERIOR y reiniciar otra vez. Fíjate en que el esquema ya soporta esto: JWT_SECRETO_ANTERIOR es .optional(), así que la aplicación arranca igual con una clave o con dos. La rotación no requiere tocar código.
- Cuando un secreto se filtra
Ocurre. Alguien pega la URL de PostgreSQL en un chat, o hace commit del .env un viernes. El orden de las acciones importa:
- Revocar y rotar primero. El secreto filtrado es válido hasta que deja de serlo; todo lo demás puede esperar.
- Revisar accesos después, con los registros de auditoría del módulo 8: ¿hubo conexiones desde IP desconocidas?, ¿se emitieron tokens raros?
- Limpiar el historial al final, sabiendo que no basta. Reescribir el historial de Git (
git filter-repo) no borra el secreto de los clones que ya existen, ni de los forks, ni de la caché de la interfaz web, ni de los rastreadores automáticos que escanean GitHub buscando credenciales. Un secreto que ha estado en un repositorio remoto está comprometido para siempre. Borrar el commit es higiene, no remedio. - Evitar la reincidencia. Un gancho de pre-commit que detecte patrones de credenciales (el proyecto ya tiene husky desde el módulo 9) y escaneo de secretos en la tubería de CI (11-06).
- Configuración en caliente frente a reinicio
Hay dos formas de aplicar un cambio de configuración: recargarla en el proceso vivo, o reiniciar el proceso.
| En caliente | Reinicio | |
|---|---|---|
| Complejidad | Alta: cada módulo debe reaccionar al cambio | Nula |
| Estado | Riesgo de inconsistencia entre módulos | Todo coherente desde el segundo cero |
| Corte de servicio | Ninguno | Ninguno si hay recarga sin cortes |
| Auditoría | Difícil saber qué valores había en cada momento | El arranque deja constancia |
Escena Viva reinicia, y por eso configuracion está congelado. La razón es que ya tenemos apagado ordenado (módulo 6) y vamos a tener recarga secuencial de trabajadores (11-03): reiniciar cuesta segundos y no pierde ni una petición. La complejidad de recargar en caliente solo se justifica cuando el arranque es carísimo, que no es nuestro caso. Nota importante: una bandera de funcionalidad no es configuración de despliegue. Si quieres activar la venta anticipada del Festival de Jazz a las 10:00 sin reiniciar, eso va en base de datos o en un servicio de banderas, no en process.env.
- Tabla de referencia de las variables de Escena Viva
Esta tabla es la referencia que usaremos en el resto del módulo: el .env.example, el ecosystem.config.js de PM2, el docker-compose.yml, el panel de la PaaS y los secretos de CI derivan todos de aquí.
| Variable | Tipo | Obligatoria | Por defecto | Dónde se usa |
|---|---|---|---|---|
NODE_ENV |
enum | No | development |
Express, npm, configuracion.esProduccion |
PUERTO |
entero | No | 3000 |
src/servidor.js |
NIVEL_REGISTRO |
enum | No | info |
pino (11-02) |
URL_POSTGRES |
url | Sí | — | src/db/sequelize.js |
URL_MONGO |
url | Sí | — | src/db/conexion.js |
URL_REDIS |
url | Sí | — | src/db/redis.js, sesión, límites, colas |
JWT_SECRETO |
secreto ≥32 | Sí | — | src/servicios/tokens.js |
JWT_SECRETO_ANTERIOR |
secreto ≥32 | No | — | Rotación (apartado 8) |
SESION_SECRETO |
secreto ≥32 | Sí | — | src/middleware/sesion.js |
CSRF_SECRETO |
secreto ≥32 | Sí | — | src/middleware/csrf.js |
ORIGEN_CORS |
lista | No | http://localhost:5173 |
src/middleware/cors.js |
CONFIAR_EN_PROXY |
booleano | No | false |
trust proxy (11-05) |
NUMERO_TRABAJADORES |
entero | No | 0 (= núcleos) |
src/cluster.js, pool de Sequelize |
CONCURRENCIA_COLA |
entero | No | 5 |
src/procesos/consumidor-entradas.js |
TOKEN_METRICAS |
secreto ≥16 | No | — | /metricas protegido (11-02) |
Errores Comunes y Consejos
- Leer
process.envfuera desrc/config/index.js. Es el error que más daño hace a largo plazo: se pierde la validación, los valores por defecto se dispersan y nadie sabe qué variables usa la aplicación. Prohíbelo con lint. - Confundir «no está en el código» con «es seguro». Una variable de entorno es visible en
/proc, en volcados y en cualquier vuelco accidental. Trátala como un secreto en tránsito, no como una caja fuerte. - Consejo: genera todos los secretos con
openssl rand -hex 32y nunca reutilices el mismo valor paraJWT_SECRETO,SESION_SECRETOyCSRF_SECRETO: si uno se filtra, se filtran los tres. Y actualiza.env.exampley la tabla del apartado 11 en el mismo commit en que añadas una variable.
Ejercicios
Ejercicio 1 — Diagnóstico de configuración
Escribe un script scripts/comprobar-configuracion.js que cargue src/config/index.js e imprima una tabla con todas las variables del esquema indicando, para cada una, si viene del entorno o de un valor por defecto, censurando los secretos. Debe salir con código 1 si la configuración es inválida (aprovechando el process.exit(1) que ya hace el módulo).
Ejercicio 2 — Soporte de _FILE
Amplía src/config/index.js para que cualquier variable pueda venir de un fichero: si existe <NOMBRE>_FILE, se lee el contenido de esa ruta (recortando el salto de línea final) y se usa como valor de <NOMBRE>. Aplícalo antes de validar con zod.
Ejercicio 3 — Rotación simulada
Escribe una prueba de integración que verifique que un token firmado con JWT_SECRETO_ANTERIOR sigue siendo aceptado por verificarAcceso, y que uno firmado con una tercera clave desconocida es rechazado con ErrorAutenticacion.
Soluciones
Ejercicio 1. La clave es no volver a leer process.env en el script salvo para saber el origen, y censurar por nombre:
// scripts/comprobar-configuracion.js
'use strict';
// Si la configuracion fuese invalida, este require ya habria salido con codigo 1.
const { configuracion } = require('../src/config/index.js');
const SENSIBLES = /SECRETO|TOKEN|PASSWORD|URL_POSTGRES|URL_MONGO/;
const NOMBRES = [
'NODE_ENV', 'PUERTO', 'NIVEL_REGISTRO', 'URL_POSTGRES', 'URL_MONGO',
'URL_REDIS', 'JWT_SECRETO', 'JWT_SECRETO_ANTERIOR', 'SESION_SECRETO',
'CSRF_SECRETO', 'ORIGEN_CORS', 'CONFIAR_EN_PROXY', 'NUMERO_TRABAJADORES',
'CONCURRENCIA_COLA', 'TOKEN_METRICAS',
];
const censurar = (nombre, valor) =>
valor === undefined ? '(sin definir)' : SENSIBLES.test(nombre) ? '********' : String(valor);
console.table(NOMBRES.map((nombre) => ({
variable: nombre,
origen: process.env[nombre] === undefined ? 'por defecto' : 'entorno',
valor: censurar(nombre, process.env[nombre]),
})));Ejercicio 2. Se preprocesa process.env antes del safeParse:
const fs = require('node:fs');
function resolverFicheros(entorno) {
const resuelto = { ...entorno };
for (const [clave, valor] of Object.entries(entorno)) {
if (!clave.endsWith('_FILE') || !valor) continue;
try {
// Lectura sincrona a proposito: estamos en el arranque.
resuelto[clave.slice(0, -'_FILE'.length)] = fs.readFileSync(valor, 'utf8').trimEnd();
} catch (error) {
process.stderr.write(`No se pudo leer ${clave}=${valor}: ${error.message}\n`);
process.exit(1);
}
}
return resuelto;
}
const resultado = esquemaConfiguracion.safeParse(resolverFicheros(process.env));Se lee de forma síncrona a propósito: estamos en el arranque, antes de que exista bucle de eventos con trabajo, y queremos fallar antes de continuar.
Ejercicio 3. Con mocha y chai, firmando a mano con cada clave:
const { expect } = require('chai');
const jwt = require('jsonwebtoken');
const { configuracion } = require('../../src/config/index.js');
const { verificarAcceso } = require('../../src/servicios/tokens.js');
describe('rotacion de JWT_SECRETO', () => {
const carga = { sub: 'usr-lucia', rol: 'asistente' };
it('acepta un token firmado con el secreto anterior', () => {
const anterior = configuracion.seguridad.jwtSecretoAnterior;
expect(anterior, 'define JWT_SECRETO_ANTERIOR en .env.test').to.be.a('string');
const token = jwt.sign(carga, anterior, { expiresIn: '15m' });
expect(verificarAcceso(token)).to.include({ sub: 'usr-lucia' });
});
it('rechaza un token firmado con una clave desconocida', () => {
const token = jwt.sign(carga, 'x'.repeat(32), { expiresIn: '15m' });
expect(() => verificarAcceso(token)).to.throw(/no valido/i);
});
});Conclusión
La configuración de Escena Viva ha dejado de ser un .env en tu portátil para convertirse en un contrato explícito: un esquema zod que enumera cada variable, convierte su tipo, aplica valores por defecto y mata el proceso con un mensaje comprensible si falta un secreto. Un único punto de lectura de process.env, un .env.example que documenta el contrato, una tabla de referencia que servirá de base para el fichero de PM2, el Dockerfile, el docker-compose.yml, el panel de la PaaS y los secretos de CI. Y, además, una estrategia de rotación que permite cambiar la clave de firma de los JWT sin desconectar a nadie. Con la configuración resuelta, aparece la siguiente pregunta incómoda: cuando algo falle en producción a las tres de la madrugada —y fallará—, ¿cómo te enteras? En la próxima lección, Registro y Monitorización en Producción, sustituimos los console.log por registro estructurado en JSON con pino, colgamos el registrador hijo del idPeticion que llevamos arrastrando desde el módulo 6 para poder seguir una compra fallida por todos sus rastros, exponemos métricas con prom-client —incluido el retraso del bucle de eventos que ya sabemos medir— e implementamos las sondas /salud/vivo y /salud/listo que el resto del módulo dará por supuestas.
Curso de Node.js: De Principiante a Avanzado
Módulo 1: Introducción a Node.js
- ¿Qué es Node.js?
- Instalación y Configuración del Entorno
- Tu Primer Programa en Node.js
- El REPL de Node.js
- JavaScript Moderno para Node.js
- El Proyecto del Curso: la Plataforma Escena Viva
Módulo 2: Conceptos Básicos
- Arquitectura de Node.js
- El Bucle de Eventos (Event Loop)
- Callbacks y Programación Asíncrona
- Promesas y async/await
- Eventos y EventEmitter
- Módulos CommonJS y require()
- Módulos ES e Interoperabilidad
Módulo 3: Sistema de Archivos y E/S
- Lectura y Escritura de Archivos
- El Módulo fs a Fondo
- Rutas Multiplataforma con el Módulo path
- Trabajando con Streams
- Streams de Transformación y pipeline
- Buffers y Datos Binarios
Módulo 4: HTTP y Servidores Web
- Creando un Servidor HTTP Simple
- Manejo de Solicitudes y Respuestas
- Enrutamiento Manual
- Sirviendo Archivos Estáticos
- Recibiendo Datos: Cuerpos de Petición y JSON
- Consumiendo APIs Externas desde Node.js
Módulo 5: NPM y Gestión de Paquetes
- Introducción a NPM y package.json
- Instalación y Uso de Paquetes
- Versionado Semántico y package-lock
- Scripts de npm y Automatización del Proyecto
- Creación y Publicación de Paquetes
- Seguridad y Mantenimiento de Dependencias
Módulo 6: Framework Express.js
- Introducción a Express.js
- Configuración de una Aplicación Express
- Enrutamiento en Express
- Middleware
- Middleware de Terceros Esenciales
- Validación de Datos de Entrada
- Manejo de Errores
Módulo 7: Bases de Datos y ORMs
- Introducción a las Bases de Datos
- Usando MongoDB con Mongoose
- Operaciones CRUD
- Relaciones, Poblado y Consultas Avanzadas
- Usando Bases de Datos SQL con Sequelize
- Migraciones, Transacciones y Datos de Prueba
Módulo 8: Autenticación y Autorización
- Introducción a la Autenticación
- Registro de Usuarios y Hash de Contraseñas
- Sesiones y Cookies con Passport.js
- Autenticación con JWT
- Control de Acceso Basado en Roles
- Buenas Prácticas de Seguridad en APIs
Módulo 9: Pruebas y Depuración
- Introducción a las Pruebas
- Pruebas Unitarias con Mocha y Chai
- Dobles de Prueba con Sinon
- Pruebas de Integración
- Cobertura y Automatización de las Pruebas
- Depuración de Aplicaciones Node.js
Módulo 10: Temas Avanzados
- El Módulo Cluster
- Hilos de Trabajo (Worker Threads)
- Caché y Colas de Trabajo con Redis
- Optimización del Rendimiento
- Construcción de APIs RESTful
- GraphQL con Node.js
Módulo 11: Despliegue y DevOps
- Configuración y Variables de Entorno
- Registro y Monitorización en Producción
- Usando PM2 para la Gestión de Procesos
- Empaquetado con Docker
- Desplegando en Heroku y Otras PaaS
- Integración y Despliegue Continuos
