为什么不能给 Node.js Agent 主密钥,以及 60 秒临时 Token 的实现方法
TuBrief 편집팀
2026년 7월 23일
0
Computing/Software원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.
커뮤니티의 다른 글
댓글 (0)
Log in to leave a comment
아직 작성된 글이 없습니다
원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.
Log in to leave a comment
아직 작성된 글이 없습니다
在使用 LangChain 或 LlamaIndex 构建自主型 Agent(智能体)时,终究会迎来连接数据库和外部 API 的那一刻。而事故通常就在此时发生。一次 Prompt Injection(提示词注入)攻击,就可能让写在 .env 里的 OpenAI 主密钥或数据库管理员密码瞬间泄露。无论把 Prompt Guardrails(提示词 Guardrails)设计得多么严密也都无济于事。只要 LLM 的推理层与执行层绑在一起,仅凭一句话,安全性就会脆弱地被击穿。
根据谷歌 2025 年 SAIF(Secure AI Framework)报告显示,引入 Agent 的企业中有 88% 遭遇过提示词注入尝试。而传统的文本模式检测技术拦截率仅为 23%。与其抱有侥幸心理,倒不如干脆不信任 Agent 进程本身。应该完全不向 Agent 授予权限,而是改为在中间件层注入仅存活 60 秒的临时 Token 的架构。
使用 HashiCorp Vault 的 AppRole 认证,即可实现在调用工具的极短暂瞬间颁发一个有效期仅 60 秒的 Token。当 Agent 请求外部 API 时,拦截器在中间介入,并将 Short-lived Token 塞入 Header 中。
`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;
}
};
}
}
`
光写好代码还不够。由于 Node.js V8 引擎的堆内存(Heap Memory)是由垃圾回收器(GC)按自己的机制运行的,因此即便清空了变量,Token 字符串仍会在堆中残留一段时间。如果遭遇 Heap Dump 攻击,就会有泄露风险。
Buffer 对象。Buffer.fill(0) 强制将字节清理为 0。null,交给 GC 处理。只要做好这三个步骤,通过内存剖析(Memory Profiling)导致 Token 泄露的概率就会大幅降低。
| 凭据管理方式 | 平均生存时间 (TTL) | 被窃取时的受害范围 | 审计追踪 |
|---|---|---|---|
| 硬编码 Master API Key | 无限制 | 掌控全部基础设施权限 | 共享单一 Key,无法识别主体 |
| 环境变量 (.env) Static Secret | 进程生命周期 | 滥用该 Node 进程的所有权限 | 无法追踪具体用户的工作流 |
| Vault AppRole Ephemeral Token | 60秒 | 仅限该 Tool 执行 1 次 | 通过 Vault Audit Log 进行秒级追踪 |
即使 Agent 因提示词越狱而执行“查询所有会员信息”的指令,只要在数据库引擎层面予以拒绝即可。如果是 Multi-tenant(多租户)SaaS 环境,则需要设置 PostgreSQL 的 Row Level Security (RLS)。
获取 Auth0 签发的 JWT 中的 userId 和 tenantId,并将其传递给 LangChain 的 RunnableConfig 会话上下文。在 Prisma 事务中,将该上下文作为 Session 变量注入。
`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);
});
}
`
set_config 的最后一个参数必须传入 true,变量才会仅应用于当前事务范围(SET LOCAL)。这是在数据库连接池(Connection Pooling)环境中,防止上一用户的权限泄漏到下一请求的关键配置。
现在,在 SQL 文件中创建读取该 Session 变量的策略。
`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
);
`
配置好这套组合拳后,无论 Agent 生成多么离谱的查询,都无法查到自己租户范围之外的数据。即便发生泄露事故,也能将数据恢复时间(MTTR)从几天缩短至短短几分钟。
开发者每次修改 Agent 工具代码时,不可能每次都手动测试权限隔离是否正常运行。我们可以将开源工具 Promptfoo 和微软的 PyRIT 整合到 GitHub Actions 中,以 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
`
在服务器运行时(Server Runtime)中,挂载一个熔断器(Circuit Breaker)中间件,以便在检测到威胁模式时立即停止运行。
`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(() => {});
}
}
`
构建安全测试流水线的步骤非常简单:
promptfooconfig.yaml,并定义 jailbreak 和 excessive-agency 检查项。AgentCircuitBreaker 中间件,当连续 3 次检测到注入模式时,暂停 Agent 的执行。建立这套架构后,每周可以节省下原本花在手动审查提示词上的 5 小时以上的时间。
| 评估指标 | 手动验证方式 | 引入 Zero-Trust 自动化 |
|---|---|---|
| 审计准备时间 | 每周 5~8 小时 | 每周不足 1 小时 |
| 权限泄露时的 MTTR | 数天(全量日志/数据库全面排查) | 数分钟(限制在 RLS 范围内) |
| 提示词注入拦截率 | 约 23% | 99.9% |
| Key 泄露时的扣费风险 | 无限制的云服务扣费 | 60 秒 TTL 阻断扣费 |
Agent 安全的核心不在于祈祷模型听话,而是在基础设施层面戴上“手铐”,即使模型胡言乱语或被攻击控制,也无法对系统造成实质伤害。