Почему нельзя давать мастер-ключ агентам 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 неизбежно наступает момент, когда их нужно связать с базой данных и внешними API. И именно здесь чаще всего происходят инциденты. Из-за одной инъекции промпта (Prompt Injection) мастер-ключ OpenAI или пароль администратора БД, прописанные в .env, тут же утекают в сеть. И сколько бы гардрайлов (guardrails) для промптов вы ни выстраивали, это не поможет. Пока область рассуждений LLM жестко связана с областью выполнения, безопасность рушится буквально от одной фразы.
Согласно отчету Google SAIF (Secure AI Framework) за 2025 год, 88% компаний, внедряющих агентов, сталкивались с попытками инъекции промпта. При этом уровень блокировки традиционными методами обнаружения текстовых шаблонов составил всего 23%. Гораздо проще сразу не доверять самому процессу агента. Лучше вообще не давать агенту постоянных прав, а изменить архитектуру так, чтобы на уровне промежуточного ПО (middleware) внедрялся временный токен со сроком жизни 60 секунд.
Используя аутентификацию AppRole в HashiCorp Vault, можно выпускать токен на 60 секунд ровно в момент вызова инструмента. Когда агент обращается к внешнему API, перехватчик (interceptor) вмешивается в процесс и подставляет 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;
}
};
}
}
`
Написанием одного лишь кода дело не ограничивается. Куча памяти (heap) движка Node.js V8 очищается сборщиком мусора непредсказуемо, поэтому даже после обнуления переменной строка токена некоторое время остается в памяти. При атаке с дампом кучи возникает риск утечки.
Buffer.Buffer.fill(0), чтобы принудительно очистить байты в 0.null, передавая её «на съедение» GC.Соблюдение этих трех шагов существенно снижает вероятность утечки токена при профилировании памяти.
| Метод управления учетными данными | Средний срок жизни (TTL) | Область ущерба при компрометации | Аудит и трассировка |
|---|---|---|---|
| Жестко закодированный Master API Key | Неограничен | Полный захват всей инфраструктуры | Идентификация субъекта невозможна из-за общего ключа |
| Static Secret в переменных окружения (.env) | Срок жизни процесса | Злоупотребление всеми правами данного процесса Node | Невозможно отследить рабочие процессы отдельных пользователей |
| Vault AppRole Ephemeral Token | 60 секунд | Ограничено 1 однократным запуском инструмента | Посекундная трассировка через Vault Audit Log |
Даже если из-за побега из промпта (jailbreak) агент выполнит команду “выведи информацию обо всех пользователях”, достаточно, чтобы сам движок БД отклонил этот запрос. В мультиарендной (Multi-tenant) SaaS-среде необходимо настроить Row Level Security (RLS) в PostgreSQL.
Извлекаем userId и tenantId из JWT, выданного Auth0, и передаем их в контекст сессии RunnableConfig в LangChain. Затем внедряем этот контекст в качестве переменных сессии внутри транзакции 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). Это ключевая настройка, предотвращающая утечку прав предыдущего пользователя следующему запросу в условиях пула соединений БД.
Теперь создаем политику в 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) сократится с нескольких дней до считанных минут.
Каждый раз, когда разработчик меняет код инструментов агента, тестировать изоляцию прав вручную невозможно. Мы объединяем open-source инструменты 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
`
На сервере в рантайме подключаем middleware предохранителя (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+roles+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 перед эндпоинтами Express, чтобы останавливать выполнение агента при обнаружении 3 подряд паттернов инъекции.С такой конфигурацией вы сэкономите более 5 часов, которые раньше еженедельно тратились на ручной анализ промптов.
| Метрика оценки | Ручной способ проверки | Внедрение автоматизации Zero-Trust |
|---|---|---|
| Время подготовки к аудиту | 5–8 часов в неделю | Менее 1 часа в неделю |
| MTTR при утечке прав | Несколько дней (полный анализ логов/БД) | Считанные минуты (ограничено рамками RLS) |
| Уровень блокировки инъекций промпта | Около 23% | 99.9% |
| Риск расходов при утечке ключа | Неограниченные счета за облако | Счета заблокированы благодаря TTL 60 сек |
Суть безопасности агентов заключается не в том, чтобы молиться на послушание модели. Она в том, чтобы на уровне инфраструктуры надеть на неё наручники, которые не позволят нанести вред системе, даже если модель начнет галлюцинировать или поддастся атаке.