{*}SecureCodeHQ
5 errores con secretos en tu servidor (y como arreglarlos)
·por Juan Isidoro·5 min de lectura

5 errores con secretos en tu servidor (y como arreglarlos)

JWT secrets hardcodeados, credenciales en texto plano, fallbacks silenciosos. Los fallos de seguridad mas comunes en Node.js en produccion y como solucionarlos.

securitysecretsnodejsbest-practices

Tu app funciona. Los usuarios hacen login. Todo va bien.

Pero en algun lugar de tu servidor hay un JWT_SECRET que no ha cambiado desde el primer dia. Un DATABASE_URL en texto plano. Un valor por defecto que dice "change-me" y nadie lo cambio nunca.

No son casos hipoteticos. Son los problemas de seguridad que me encuentro una y otra vez en servidores Node.js en produccion. Te cuento cuales son y como arreglarlos.


1. JWT Secret metido en el codigo

El problema:

// "Ya lo arreglo luego"
const secret = process.env.JWT_SECRET || 'super-secret-key';

Este fallback parece inofensivo. Pero si la variable de entorno no se carga (un deploy mal configurado, un .env que falta, PM2 que se lia) tu app usa 'super-secret-key' sin avisarte. Cualquiera que lea tu codigo o adivine un fallback tipico puede fabricar tokens de autenticacion.

Como se arregla:

Que la app reviente si el secreto no existe.

const secret = process.env.JWT_SECRET;

if (!secret) {
  throw new Error('JWT_SECRET is required');
}

Sin fallback. Sin "ya lo pillaremos en testing". Si no esta, la app no arranca. Punto.


2. Credenciales de base de datos en texto plano

El problema:

# .env (commiteado "sin querer")
DATABASE_URL=postgres://admin:password_real_123@db.ejemplo.com:5432/produccion

O peor: que se cuelen por los logs de CI/CD, por argumentos de build de Docker, o por las variables de entorno de PM2 que cualquiera con acceso al servidor puede ver con pm2 env.

Como se arregla:

Las credenciales de base de datos no deberian existir como ficheros en tu servidor. Pidelas en tiempo de ejecucion desde un gestor de secretos:

async function bootstrap() {
  const secrets = await secretsManager.getAll();
  process.env.DATABASE_URL = secrets.DATABASE_URL;

  // Ahora si, arranca la app
  const prisma = new PrismaClient({
    datasourceUrl: process.env.DATABASE_URL
  });
}

Las credenciales solo existen en memoria mientras la app esta corriendo. Nada en disco que pueda filtrarse.


3. Valores por defecto que "funcionan"

El problema:

const config = {
  jwtSecret: process.env.JWT_SECRET || 'change-me',
  encryptionKey: process.env.ENCRYPTION_KEY || 'default-key',
  apiKey: process.env.API_KEY || 'dev-key'
};

Esto es programacion defensiva mal entendida. Tu app arranca sin problemas con valores inseguros. Sin errores. Sin avisos. Solo una falsa sensacion de seguridad.

Como se arregla:

Cambia cada fallback por una validacion:

const required = ['JWT_SECRET', 'ENCRYPTION_KEY', 'API_KEY'];

for (const key of required) {
  if (!process.env[key]) {
    throw new Error(`Falta variable de entorno requerida: ${key}`);
  }
}

Que falle fuerte. Que falle pronto. Nunca dejes que una configuracion insegura llegue a produccion en silencio.


4. Secretos que se filtran por CI/CD

El problema:

# Tu Dockerfile
ARG DATABASE_URL
RUN npx prisma generate

Los argumentos de build se guardan en las capas de la imagen Docker. Cualquiera con acceso a tu registry puede sacarlos:

docker history tu-imagen --no-trunc
# Ahi tienes tu DATABASE_URL en texto plano

Lo mismo pasa con los logs de GitHub Actions si haces echo de un secreto sin querer, o con builds de Nixpacks que exponen variables de entorno.

Como se arregla:

Usa secretos de BuildKit que nunca se guardan en la imagen:

RUN --mount=type=secret,id=database_url \
    DATABASE_URL=$(cat /run/secrets/database_url) \
    npx prisma generate

O mejor todavia: no pases secretos en tiempo de build. Pidelos en tiempo de ejecucion.


5. Secretos que nunca se rotan

El problema:

Tu JWT_SECRET tiene el mismo valor que generaste hace dos anos. A lo mejor estuvo en un commit en algun momento. A lo mejor un exempleado lo vio. A lo mejor esta en las notas de alguien.

Si ese secreto se filtra, todos los tokens emitidos estan comprometidos. Y no tienes forma de invalidarlos sin echar a todos los usuarios de su sesion.

Como se arregla:

Disena pensando en rotacion desde el primer dia:

const secrets = {
  current: process.env.JWT_SECRET_CURRENT,
  previous: process.env.JWT_SECRET_PREVIOUS
};

function verifyToken(token: string) {
  try {
    return jwt.verify(token, secrets.current);
  } catch {
    // Periodo de gracia: acepta tokens firmados con el secreto anterior
    return jwt.verify(token, secrets.previous);
  }
}

Cuando rotas, el secreto antiguo pasa a previous. Los tokens que ya existen siguen funcionando durante la ventana de transicion. Los nuevos tokens usan el secreto nuevo.


Lo que tienen en comun los 5

No son cinco problemas separados. Son sintomas de lo mismo: secretos viviendo donde no deberian.

  • En tu codigo: valores hardcodeados
  • En tu repo: ficheros .env commiteados
  • En tu CI/CD: argumentos de build expuestos
  • En tu servidor: ficheros estaticos en disco
  • En tu historial: valores que nunca cambian

La solucion es tratar los secretos como algo externo, que se obtiene en tiempo de ejecucion, y que se puede rotar en cualquier momento.


Como lo resuelve SecureCodeHQ

SecureCodeHQ es un vault de secretos pensado exactamente para este flujo:

  1. Cero secretos en disco. Tu servidor solo guarda un token de dispositivo. Todos los secretos se piden en tiempo de ejecucion.

  2. Si falla, falla rapido. Si SecureCodeHQ no puede cargar tus secretos, tu app no arranca. Nada de fallbacks silenciosos.

  3. Rotacion integrada. Un click para rotar cualquier secreto, con periodos de gracia automaticos para los tokens que ya estan en circulacion.

  4. Auditoria completa. Sabes exactamente cuando se accedio a cada secreto y desde que servidor.

import { SecureCode } from '@securecode/sdk';

async function bootstrap() {
  await SecureCode.loadEnv(); // Obtiene todos los secretos

  // DATABASE_URL, JWT_SECRET, etc. ya estan en process.env
  // Nada en disco. Nada en tu codigo.

  startApp();
}

Tus secretos se quedan en el vault. Tus servidores se quedan limpios. Y tu duermes tranquilo.


Listo para limpiar tus secretos? Empieza con SecureCodeHQ


Lectura adicional