Node.jsエージェントにマスターキーを渡してはいけない理由と60秒時限トークンの実装法
TuBrief 편집팀
2026년 7월 23일
0
Computing/Software원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.
커뮤니티의 다른 글
댓글 (0)
Log in to leave a comment
아직 작성된 글이 없습니다
원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.
Log in to leave a comment
아직 작성된 글이 없습니다
LangChainやLlamaIndexで自律型エージェントを開発していると、最終的にDBや外部APIと連携する段階を迎えます。大抵の場合、事故はここで起こります。プロンプトインジェクション一発で.envに書かれたOpenAIのマスターキーやDBの管理者パスワードが流出してしまうのです。プロンプトガードレールをいくら緻密に設計しても意味がありません。LLMの推論領域と実行領域が結合している限り、たった1行の文章でセキュリティはあっけなく突破されます。
Googleの2025年版SAIF(Secure AI Framework)レポートによると、エージェントを導入した企業の88%がプロンプトインジェクション攻撃を経験しています。従来のテキストパターン検知手法による遮断率はわずか23%に過ぎませんでした。エージェントプロセスそのものを信頼しない設計にする方が懸念を減らせます。エージェントには権限を一切与えず、ミドルウェアレイヤーで60秒間のみ有効な一時トークンを注入する構造へ切り替えるべきです。
HashiCorp VaultのAppRole認証を使用すると、ツールを呼び出すまさにその瞬間だけ、60秒間有効なトークンを発行できます。エージェントが外部APIを呼び出す際、インターセプターが間に介入してShort-livedトークンをヘッダーに挿入する方式です。
`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エンジンのヒープメモリはガベージコレクターが独自のタイミングで動作するため、変数をクリアしてもトークン文字列がしばらくヒープ内に残ってしまいます。ヒープダンプ攻撃を受けると流出のリスクが存在します。
Bufferオブジェクトに格納します。Buffer.fill(0)を実行し、バイト配列を0で強制的に破棄(クリア)します。nullを代入し、GCの回収対象として渡します。この3つのステップを徹底するだけで、メモリプロファイリングによるトークン奪取の確率を大幅に低減できます。
| 資格情報管理方式 | 平均寿命 (TTL) | 奪取時の被害範囲 | 監査トラッキング |
|---|---|---|---|
| ハードコードされた Master API Key | 無制限 | インフラ全体の権限掌握 | 単一キーの共有により主体の特定不可 |
| 環境変数 (.env) Static Secret | プロセス寿命 | 対象Nodeプロセスの権限全体を悪用 | ユーザーごとのワークフロー追跡不可 |
| Vault AppRole Ephemeral Token | 60秒 | 対象Toolの1回実行に限定 | Vault Audit Logにより秒単位で追跡可能 |
プロンプト脱獄(Jailbreak)によってエージェントが「全会員情報を取得して」という命令を実行しようとしても、DBエンジンレベルで拒否できれば問題ありません。マルチテナントSaaS環境であれば、PostgreSQLのRow Level Security(RLS)を設定する必要があります。
Auth0が発行したJWTのuserIdとtenantIdを取得し、LangChainのRunnableConfigセッションコンテキストに伝達します。このコンテキストを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);
});
}
`
set_configの最後の引数にtrueを渡すことで、現在のトランザクション範囲(SET LOCAL)にのみ変数が適用されます。DBコネクションプーリング環境において、前のユーザー権限が次のリクエストに漏れ出す事態を防ぐための重要な設定です。
次に、SQLファイル側でこのセッション変数を読み取るポリシーを作成します。
`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
);
`
この組み合わせを用意しておけば、エージェントがどれほど想定外のクエリを生成したとしても、自身のテナント領域外のデータは照会すらできません。漏洩事故が発生した場合でも、データ復旧時間(MTTR)を数日からわずか数分単位に短縮できます。
開発者がエージェントツールのコードを変更するたびに、権限分離が正しく機能しているか手動でテストし続けるわけにはいきません。オープンソースツールのPromptfooとMicrosoftの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
`
サーバーのランタイムには、脅威パターンを検知した際に即座に動作を停止させるサーキットブレーカーミミドルウェアを組み込んでおきます。
`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回連続でインジェクションパターンを検知した場合はエージェントの実行を遮断します。この構成を整えることで、毎週の手動プロンプトレビューに費やしていた5時間以上の時間を削減できます。
| 評価指標 | 手동検証方式 | Zero-Trust自動化の導入 |
|---|---|---|
| 監査準備時間 | 週あたり 5〜8時間 | 週あたり 1時間未満 |
| 権限流出時の MTTR | 数日(ログ・DB全体の全数調査) | 数分(RLSの範囲内に制限) |
| プロンプトインジェクション遮断率 | 約 23% | 99.9% |
| キー流出時の課金リスク | 無制限のクラウド課金 | 60秒のTTLにより高額課金を防止 |
エージェントセキュリティの本質は、モデルが指示通りに動くことを祈ることではありません。モデルが暴走したり攻撃に屈したりした状態であっても、システムに害を及ぼせないよう、インフラレベルで「手枷」を嵌めておくことなのです。