
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.
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
.envcommiteados - 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:
-
Cero secretos en disco. Tu servidor solo guarda un token de dispositivo. Todos los secretos se piden en tiempo de ejecucion.
-
Si falla, falla rapido. Si SecureCodeHQ no puede cargar tus secretos, tu app no arranca. Nada de fallbacks silenciosos.
-
Rotacion integrada. Un click para rotar cualquier secreto, con periodos de gracia automaticos para los tokens que ya estan en circulacion.
-
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
- Por que los archivos .env son peligrosos con agentes IA si quieres entender que pasa cuando tus secretos estan en disco
- SSH para desarrolladores: claves, servidores y agentes IA si gestionas claves SSH en varios entornos
- Como evitar que tus secretos acaben en git para pre-commit hooks y herramientas de escaneo
- Seguridad de agentes IA: guia practica cubre el modelo de amenazas completo para agentes de programacion
- Gestionar secretos con Claude Code para montar un vault que funcione con agentes de IA
- Criptografia asimetrica: la idea que cambio internet para las matematicas detras del cifrado y los pares de claves
- Prueba SecureCode gratis. Secretos zero-knowledge para developers que usan agentes de IA.