Агент, который умеет вызывать внешние 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-инфраструктуры — вот с чем ещё стоит разобраться: