Изоляция секретов и API-ключей при работе агентов
Агент, который умеет вызывать внешние API, читать почту или обращаться к базе данных, обязательно держит в своём окружении секреты: ключи API, пароли к базам, токены доступа к внутренним сервисам. Проблема в том, что LLM-агенты по своей природе устроены так, что им регулярно «подсовывают» произвольный контекст — от пользовательского ввода до содержимого внешних документов, — и если секреты лежат небрежно, они рано или поздно окажутся в этом контексте, а оттуда рискуют попасть в лог, в ответ модели или даже в промпт, отправленный третьей стороне.
Разберём, как организовать изоляцию секретов для агентов, работающих на srv-ai01 в сети 192.168.10.0/24, — от простого варианта с env-файлами до полноценного HashiCorp Vault.
Почему секреты в коде и промптах — это плохо вдвойне
У обычного бэкенд-приложения секрет, зашитый в код, — это уже проблема. У AI-агента таких проблем две: во-первых, классическая (утечка через git-репозиторий, докер-образ, случайный лог), во-вторых, специфическая — модель может процитировать секрет в своём ответе, если он оказался в системном промпте или в контексте выполнения инструмента. Известны случаи, когда агенты с доступом к переменным окружения при отладке буквально выводили содержимое .env файла в чат, потому что пользователь попросил «покажи свою конфигурацию» или инъекция заставила это сделать.
Уровень 1: env-файлы с ограниченным доступом
Для небольшой инфраструктуры базовый, но рабочий подход — хранить секреты в отдельных env-файлах вне репозитория, с правами доступа только для сервисного пользователя, от имени которого запускается агент.
# на srv-ai01
sudo useradd -r -s /usr/sbin/nologin svc-agent
sudo mkdir -p /etc/itpages-agent
sudo touch /etc/itpages-agent/secrets.env
sudo chown svc-agent:svc-agent /etc/itpages-agent/secrets.env
sudo chmod 600 /etc/itpages-agent/secrets.env
# /etc/itpages-agent/secrets.env
CRM_API_KEY=xxxxxxxxxxxxxxxx
SMTP_PASSWORD=xxxxxxxxxxxxxxxx
INTERNAL_DB_DSN=postgresql://agent_ro:xxxxxx@db01.itpages.lab.local:5432/support
Ключевые правила для этого уровня:
- файл секретов никогда не попадает в git — добавь его путь в глобальный
.gitignoreи проверь черезgit log --all --full-history -- "*secrets*", что ничего не утекло раньше; - процесс агента запускается строго от сервисного пользователя, а не от root и не от твоей личной учётки;
- секреты подгружаются в переменные окружения процесса при старте, а не читаются из файла на каждый запрос — так меньше шансов, что путь к файлу утечёт через инструмент чтения файлов, доступный модели.
Уровень 2: HashiCorp Vault для динамических секретов
Когда агентов и интеграций становится больше одной-двух, env-файлы перестают масштабироваться: непонятно, кто и когда обращался к секрету, ротация требует ручного передеплоя, а компрометация одного файла означает компрометацию всех ключей сразу. Vault решает это через централизованное хранение, аудит доступа и, что особенно важно для агентов, через динамические секреты — Vault может выдавать временные, недолго живущие креды к базе данных вместо статичного пароля.
# включаем secrets engine для БД на Vault-сервере
vault secrets enable database
vault write database/config/support-db \
plugin_name=postgresql-database-plugin \
connection_url="postgresql://{{username}}:{{password}}@db01.itpages.lab.local:5432/support" \
allowed_roles="agent-readonly" \
username="vault_admin" \
password="$(cat /run/secrets/vault_db_admin_pw)"
vault write database/roles/agent-readonly \
db_name=support-db \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' \
VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
default_ttl="1h" \
max_ttl="4h"
Агент на srv-ai01 при старте запрашивает у Vault временный креденшл, который живёт максимум час, вместо того чтобы держать постоянный пароль в конфиге:
import hvac
client = hvac.Client(url="https://vault.itpages.lab.local:8200", token=vault_token)
creds = client.secrets.database.generate_credentials(name="agent-readonly")
db_dsn = f"postgresql://{creds['data']['username']}:{creds['data']['password']}@db01.itpages.lab.local:5432/support"
Даже если такой креденшл каким-то образом попадёт в контекст модели и утечёт через ответ, у атакующего будет максимум час на его использование, а не бессрочный доступ.
Изоляция секретов от контекста модели
Отдельная, специфичная для AI-агентов практика — архитектурно разделять код, который знает секрет, и код, который формирует промпт для модели. Ключ API от CRM должен использоваться внутри инструмента (tool/function), который агент вызывает, но сам агент — то есть LLM — никогда не должен видеть значение ключа в своём контексте.
def call_crm_tool(query: str) -> str:
# ключ читается из окружения процесса-исполнителя инструмента,
# а не передаётся модели и не фигурирует в промпте
api_key = os.environ["CRM_API_KEY"]
response = requests.get(
"https://crm.itpages.lab.local/api/search",
headers={"Authorization": f"Bearer {api_key}"},
params={"q": query},
)
return response.json()
# модель видит только результат вызова, не сам ключ
result = call_crm_tool(model_generated_query)
Такая архитектура (tool calling с секретами внутри исполнителя инструмента, а не в промпте) — это, пожалуй, самая важная практика в этой статье: она делает утечку секрета через саму модель технически невозможной, независимо от того, насколько хитрой была инъекция.
Vault Agent для автоматического внедрения секретов
Ручной вызов Vault API из кода агента — не единственный вариант интеграции. Vault Agent умеет автоматически подставлять секреты в файл конфигурации перед запуском сервиса, что удобно, если ты не хочешь встраивать логику работы с Vault прямо в код агента.
# vault-agent-config.hcl на srv-ai01
vault {
address = "https://vault.itpages.lab.local:8200"
}
auto_auth {
method "approle" {
config = {
role_id_file_path = "/etc/vault/role_id"
secret_id_file_path = "/etc/vault/secret_id"
}
}
}
template {
source = "/etc/itpages-agent/secrets.env.tpl"
destination = "/run/itpages-agent/secrets.env"
perms = "0600"
}
Шаблон secrets.env.tpl содержит плейсхолдеры вида {{ with secret "database/creds/agent-readonly" }}{{ .Data.username }}{{ end }}, а Vault Agent сам следит за истечением TTL и обновляет файл с новыми временными кредами, перезапуская сервис при необходимости. Это снимает с разработчика агента ответственность за корректную работу с Vault API и снижает риск ошибки в коде, которая случайно залогирует секрет.
Секреты в мультиагентных системах
Если агенты вызывают друг друга (оркестратор делегирует задачу специализированному саб-агенту), важно не передавать секреты через межагентные сообщения. Оркестратор не должен пересылать API-ключ саб-агенту текстом — вместо этого каждый агент должен получать доступ к нужным ему секретам напрямую от Vault или из собственного изолированного окружения, по своей роли.
# неправильно: секрет передаётся через сообщение между агентами
orchestrator_message = f"Вызови CRM API с ключом {crm_api_key} и найди клиента"
# правильно: саб-агент сам запрашивает секрет по своей роли
orchestrator_message = "Найди клиента в CRM по имени Иванов"
# саб-агент внутри своего tool-исполнителя сам достаёт ключ из Vault
Это особенно важно, если разные саб-агенты имеют разный уровень доверия — например, один работает только с внутренними данными, а другой обращается к внешним источникам и потенциально подвержен prompt injection. Секрет, попавший в межагентное сообщение, может утечь через любого из агентов цепочки, даже если сам он никогда не должен был иметь к нему доступ.
Экстренная процедура при подозрении на утечку
Даже при хорошей изоляции стоит держать наготове быструю процедуру реагирования на подозрение об утечке секрета — например, если сотрудник заметил, что агент в ответе процитировал похожий на ключ фрагмент. Скорость реакции здесь важнее полноты расследования на первом этапе.
# быстрый отзыв ключа через Vault
vault lease revoke -prefix database/creds/agent-readonly
# быстрый отзыв виртуального ключа LiteLLM
curl -X POST http://srv-ai01.itpages.lab.local:4000/key/delete \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
-d '{"keys": ["sk-compromised-key-id"]}'
Пропиши эту процедуру заранее в виде runbook, доступного дежурному инженеру, а не полагайся на то, что в момент паники кто-то вспомнит правильные команды. Runbook должен включать не только отзыв ключа, но и следующий шаг — выпуск нового ключа и обновление конфигурации зависимых сервисов, чтобы отзыв скомпрометированного секрета не превратился в новый простой из-за забытой реконфигурации.
Частые ошибки и как их избежать
- Секреты передаются модели как часть системного промпта («вот твой API-ключ, используй его для вызова…»). Модель может процитировать его в ответе. Всегда держи секрет на стороне кода инструмента, а не в тексте, который видит LLM.
- Один и тот же ключ используется для нескольких агентов и сред (dev, staging, prod). Компрометация тестового окружения автоматически компрометирует продакшен. Выпускай отдельные ключи под каждую среду и каждого агента.
- Секреты не ротируются годами. Даже если утечки не было, статичный пароль, который не менялся три года, — это вопрос времени. Настрой Vault на автоматическую ротацию или хотя бы календарное напоминание для env-based секретов.
Аудит доступа к секретам
Помимо самого хранения, важно вести журнал того, кто и когда обращался к каждому секрету. Vault ведёт такой аудит из коробки, и его стоит включать сразу при развёртывании, а не добавлять постфактум после инцидента:
# включение audit-логирования в Vault
vault audit enable file file_path=/var/log/vault/audit.log
# пример строки из audit-лога — кто, когда и какой секрет запросил
{
"time": "2026-08-28T09:14:22Z",
"auth": {"display_name": "approle-agent-support"},
"request": {"path": "database/creds/agent-readonly", "operation": "read"},
"response": {"data": {"username": "hmac-sha256:xxxxx"}}
}
Обрати внимание, что значения в audit-логе Vault по умолчанию хэшируются (HMAC), а не хранятся в открытом виде — это осознанное решение, чтобы сам аудит-лог не стал новой точкой утечки секретов. Регулярный обзор такого лога (хотя бы раз в месяц) быстро выявляет аномалии — например, сервисную учётку, которая внезапно начала запрашивать секрет, к которому раньше не обращалась.
Секреты в контейнеризированном окружении
Если агенты на srv-ai01 работают в Docker или Kubernetes, отдельное внимание стоит уделить тому, чтобы секреты не попадали в переменные окружения контейнера через docker-compose.yml или Kubernetes manifests в открытом виде — такие файлы часто случайно коммитятся в репозиторий целиком.
# плохо: секрет в открытом виде в docker-compose.yml
environment:
- CRM_API_KEY=sk-live-abc123xyz
# лучше: секрет подтягивается из внешнего секрет-хранилища
environment:
- CRM_API_KEY_FILE=/run/secrets/crm_api_key
secrets:
crm_api_key:
external: true
Docker Secrets или Kubernetes Secrets (в связке с внешним провайдером вроде Vault через CSI-driver) держат значение вне файлов конфигурации, которые обычно попадают под версионный контроль, и монтируют его в контейнер только во время выполнения, что заметно снижает риск случайного коммита секрета в git.
Что дальше
Безопасность и эксплуатация AI-инфраструктуры — вот с чем ещё стоит разобраться: