Por qué no debes dar claves maestras a un agente de Node.js y cómo implementar tokens temporales de 60 segundos
Al crear agentes autónomos con LangChain o LlamaIndex, tarde o temprano llega el momento de conectarlos a bases de datos y API externas. Es aquí donde suelen ocurrir los accidentes. Con un solo prompt injection, la clave maestra de OpenAI o la contraseña de administración de la base de datos almacenadas en .env quedan expuestas. No importa qué tan minuciosos sean tus prompt guardrails, no servirán de nada. Mientras la capa de razonamiento y la capa de ejecución del LLM estén acopladas, una sola frase basta para vulnerar la seguridad.
Según el informe SAIF (Secure AI Framework) de Google de 2025, el 88% de las empresas que adoptaron agentes experimentaron intentos de prompt injection. La tasa de bloqueo de las técnicas tradicionales basadas en detección de patrones de texto fue de tan solo el 23%. Es más tranquilizador asumir que el propio proceso del agente no es de confianza. En lugar de otorgar permisos al agente, la estructura debe cambiarse para inyectar tokens temporales con una vida útil de solo 60 segundos en la capa de middleware.
Inyección de tokens dinámicos de Vault de 60 segundos en Express
Al utilizar la autenticación AppRole de HashiCorp Vault, se puede emitir un token válido por 60 segundos justo en el momento exacto en que se invoca una herramienta. Cuando el agente realiza una llamada a una API externa, un interceptor interviene en el medio e inserta el token de corta duración (short-lived) en el encabezado.
`typescript
import { Request, Response, NextFunction } from 'express';
import vault from 'node-vault';
interface VaultAppRoleAuth {
roleId: string;
secretId: string;
}
export class EphemeralTokenInjector {
private vaultClient: any;
private roleId: string;
private secretId: string;
constructor(endpoint: string, auth: VaultAppRoleAuth) {
this.vaultClient = vault({ endpoint });
this.roleId = auth.roleId;
this.secretId = auth.secretId;
}
private async getAppRoleToken(): Promise {
const result = await this.vaultClient.approleLogin({
role_id: this.roleId,
secret_id: this.secretId,
});
return result.auth.client_token;
}
public createToolInterceptor(targetServiceRole: string) {
return async (req: Request, res: Response, next: NextFunction) => {
let clientToken: string | null = null;
try {
clientToken = await this.getAppRoleToken();
const dynamicSecret = await this.vaultClient.write(
`sys/leases/generate/${targetServiceRole}`,
{ ttl: '60s' }
);
req.headers['authorization'] = `Bearer ${dynamicSecret.data.token}`;
req.body.ephemeralContext = {
leaseId: dynamicSecret.lease_id,
expiresAt: Date.now() + 60000,
};
next();
} catch (error) {
res.status(500).json({ error: 'Failed to inject ephemeral dynamic secret' });
} finally {
clientToken = null;
}
};
}
}
`
Escribir el código no es suficiente. Dado que el recolector de basura (garbage collector) del motor V8 de Node.js se ejecuta a su propio criterio, la cadena de texto del token permanece en la memoria heap durante un tiempo incluso después de vaciar la variable. Si sufres un ataque de heap dump, existe riesgo de filtración.
- Inmediatamente al recibir la respuesta de la API, guarda los valores sensibles en un objeto
Buffer.
- Una vez completada la ejecución, ejecuta
Buffer.fill(0) para purgar los bytes a 0 de forma forzada.
- Asigna
null a la variable de referencia para entregarla como alimento al GC.
Siguiendo únicamente estos tres pasos, la probabilidad de que un token sea robado mediante perfilado de memoria se reduce drásticamente.
| Método de gestión de credenciales |
Tiempo de vida promedio (TTL) |
Alcance del daño en caso de robo |
Trazabilidad de auditoría |
| Master API Key codificada de forma rígida |
Ilimitado |
Control total de toda la infraestructura |
Imposible identificar al sujeto por compartir una sola clave |
| Static Secret en variable de entorno (.env) |
Duración del proceso |
Abuso total de los permisos de ese proceso de Node |
Imposible rastrear flujos de trabajo por usuario |
| Token efímero AppRole de Vault |
60 segundos |
Limitado a 1 sola ejecución de esa Tool |
Rastreo al segundo mediante Vault Audit Log |
Transferencia de Auth0 OIDC Claims a políticas RLS de PostgreSQL
Aunque el agente ejecute una orden como "consulta la información de todos los usuarios" debido a un jailbreak en el prompt, basta con que el motor de la base de datos lo rechace. Si te encuentras en un entorno SaaS multitenant, debes configurar la Seguridad a Nivel de Fila (Row Level Security o RLS) de PostgreSQL.
Extrae el userId y tenantId del JWT emitido por Auth0 y pásalos al contexto de sesión RunnableConfig de LangChain. Este contexto se inyecta como variables de sesión dentro de la transacción de Prisma.
`typescript
import { RunnableConfig } from '@langchain/core/runnables';
import { PrismaClient } from '@prisma/client';
export interface AgentUserClaims {
userId: string;
tenantId: string;
}
export async function executeAgentToolWithRLS(
prisma: PrismaClient,
config: RunnableConfig,
dbOperation: (tx: any) => Promise
): Promise {
const claims = config.configurable?.userClaims as AgentUserClaims;
if (!claims || !claims.tenantId || !claims.userId) {
throw new Error('Unauthorized: Missing OIDC Claims in Agent Execution Context');
}
return await prisma.transaction(async (tx) => {
await tx.executeRawSELECT set_config('app.current_tenant_id', ${claims.tenantId}, true);
await tx.$executeRawSELECT set_config('app.current_user_id', ${claims.userId}, true);
return await dbOperation(tx);
});
}
`
Es fundamental pasar true como último argumento de set_config para que las variables solo se apliquen al alcance de la transacción actual (SET LOCAL). Esta es una configuración clave para evitar que los permisos de un usuario anterior se filtren a la siguiente solicitud en un entorno con connection pooling de base de datos.
Ahora, crea una política en el archivo SQL que lea estas variables de sesión.
`sql
ALTER TABLE tenant_documents ENABLE ROW LEVEL SECURITY;
ALTER TABLE tenant_documents FORCE ROW LEVEL SECURITY;
CREATE POLICY agent_tenant_isolation_policy ON tenant_documents
FOR ALL
TO authenticated_agent_role
USING (
tenant_id = current_setting('app.current_tenant_id', true)::uuid
)
WITH CHECK (
tenant_id = current_setting('app.current_tenant_id', true)::uuid
);
`
Con esta combinación, no importa qué consulta extraña genere el agente: no podrá ni siquiera consultar datos fuera del área de su propio tenant. Incluso si ocurre un incidente de filtración, el tiempo de recuperación de datos (MTTR) se puede reducir de varios días a solo unos minutos.
Pipeline de verificación CI/CD usando Promptfoo y PyRIT
No es viable probar manualmente cada vez que un desarrollador modifica el código de las herramientas del agente para comprobar si el aislamiento de permisos funciona correctamente. Integramos las herramientas de código abierto Promptfoo y PyRIT de Microsoft en GitHub Actions para realizar la verificación en cada PR.
`yaml
name: Agent Red Teaming Security Gate
on:
pull_request:
paths:
- 'src/agents/'
- 'src/tools/'
- 'prompts/**'
jobs:
security-eval:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install Dependencies
run: npm ci
- name: Run Promptfoo Scan
uses: promptfoo/promptfoo-action@v1
with:
config: 'promptfooconfig.yaml'
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Run PyRIT Multi-Turn Test
env:
AGENT_ENDPOINT: 'http://localhost:3000/api/agent'
run: |
python -m pip install pyrit
python scripts/run_pyrit_eval.py --endpoint $AGENT_ENDPOINT --pass-threshold 0.98
`
En el entorno de ejecución del servidor, adjuntamos un middleware de circuit breaker que detiene la operación de inmediato si detecta patrones de amenaza.
`typescript
import { Request, Response, NextFunction } from 'express';
export class AgentCircuitBreaker {
private failureCount: number = 0;
private readonly threshold: number = 3;
private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
private forbiddenSignatures: RegExp[] = [
/ignore\s+all\s+previous\s+instructions/i,
/system::override_privileges/i,
/grant\s+role\s+admin/i,
/concat\s*(\s*select/i
];
public middleware() {
return (req: Request, res: Response, next: NextFunction) => {
if (this.state === 'OPEN') {
return res.status(503).json({
error: 'CircuitBreaker:Open - Agent execution halted'
});
}
const promptInput = JSON.stringify(req.body);
const isPatternViolated = this.forbiddenSignatures.some(sig => sig.test(promptInput));
if (isPatternViolated) {
this.failureCount++;
this.dispatchSecurityAlert(req.body);
if (this.failureCount >= this.threshold) {
this.state = 'OPEN';
}
return res.status(403).json({
error: 'Security Policy Violation: Malicious prompt pattern'
});
}
next();
};
}
private dispatchSecurityAlert(payload: any): void {
const webhookUrl = process.env.SECURITY_WEBHOOK_URL;
if (!webhookUrl) return;
fetch(webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
event: 'AGENT_PRIVILEGE_ESCALATION_DETECTED',
timestamp: new Date().toISOString(),
payload
})
}).catch(() => {});
}
}
`
El orden para construir el pipeline de pruebas de seguridad es sencillo:
- Coloca
promptfooconfig.yaml en la raíz del proyecto y define los elementos de inspección de jailbreak y excessive-agency.
- Vincula un flujo de trabajo de detección de PR en GitHub Actions para ejecutar los escenarios de Promptfoo y PyRIT.
- Coloca el middleware
AgentCircuitBreaker al frente del endpoint de Express para detener la ejecución del agente al detectar patrones de inyección 3 veces consecutivas.
Al contar con esta configuración, puedes ahorrar más de 5 horas a la semana que antes dedicabas a la revisión manual de prompts.
| Métrica de evaluación |
Método de verificación manual |
Adopción de automatización Zero-Trust |
| Tiempo de preparación de auditoría |
5 a 8 horas por semana |
Menos de 1 hora por semana |
| MTTR en caso de filtración de permisos |
Varios días (investigación completa de registros/BD) |
Minutos (limitado al alcance de RLS) |
| Tasa de bloqueo de prompt injection |
Aprox. 23% |
99.9% |
| Riesgo de facturación en caso de fuga de claves |
Facturación ilimitada en la nube |
Facturación bloqueada mediante TTL de 60s |
La clave de la seguridad en agentes no radica en rezar para que el modelo se porte bien. Consiste en ponerle esposas a nivel de infraestructura para que, incluso si el modelo dice tonterías o cae en un ataque, no pueda causar daños al sistema.