Управление секретами агентов (Secrets Management)
#1. Цель
Ввести секрет как first-class ресурс Temporality и закрыть класс проблем «агенты теряют / утекают / переизобретают свои токены доступа к внешним системам».
Секреты нужны агентам для работы с GitHub/GitLab, Jira, Slack, облачными API, базами данных и MCP-серверами. Сегодня эти креденшелы живут в контексте агента: в файлах эфемерного воркспейса, в переменных окружения песочницы, в аргументах команд. Это источник сразу нескольких проблем, и выбор хранилища сам по себе ни одну из них не решает.
Ключевой принцип документа: агент не должен держать сырые значения секретов. Агент оперирует ссылкой, значение разрешается ядром на границе исполнения инструмента и не проходит через модель, события и файлы.
#2. Проблематика текущего состояния
- Потеря между запусками. Воркспейс эфемерен: токен, записанный агентом в файл, исчезает вместе с воркспейсом. Агент создаёт новый токен или просит человека — поэтому выдача секретов идёт через ядро с журналом, а не через файлы в воркспейсе.
- Утечка в журнал.
run_command, выводы инструментов и аргументы попадают в event journal, и утечка может «запомниться» как знание — поэтому redaction-фильтр журнала вырезает секреты при записи (observation/redact.go,<redacted:kind>). - Нет идентичности доступа. Общие статические токены не отвечают на вопрос «какой агент каким секретом пользовался и когда».
- Нет ротации и отзыва. Долгоживущие статические токены с широкими правами; компрометация означает ручную замену везде.
- Размазанность. Часть ключей — в
.envядра, часть — в провайдерах workspace, часть — в конфигах MCP. Единого места «какие секреты есть у этого агента/департамента» нет.
#3. Основные понятия
#3.1. Secret (секрет)
Org-scoped ресурс (как агенты, навыки и MCP-серверы):
workspace_secret
id TEXT PRIMARY KEY
org_unit_id TEXT NULL REFERENCES org_unit(id) -- NULL = вся инсталляция
name TEXT -- человекочитаемое имя, напр. github-repo-write
provider TEXT -- где лежит значение: builtin | env (внешние хранилища — фаза 2)
external_ref TEXT NULL -- ссылка на запись во внешнем хранилище (для не-builtin)
description TEXT
created_at / updated_at
Секрет — это указатель + права, а не обязательно само значение. Для
builtin-провайдера значение хранится в зашифрованной таблице Postgres, для
внешних провайдеров — в них, а у нас только ссылка.
#3.2. Secret Reference (ссылка на секрет)
Строка вида secret://<name> (опционально secret://<name>#<key> для
многозначных записей). Ссылки используются везде, где сегодня пишутся сырые
ключи: в конфигурации агента, MCP-серверов, провайдеров моделей, триггеров.
Ссылка — это и есть интерфейс для модели: агент видит secret://github,
но никогда — значение.
#3.3. Secret Provider (провайдер хранилища)
Интерфейс ядра, скрывающий конкретный бэкенд:
Get(reference, executionContext) -> (value, lease)
Issue(request, executionContext) -> (reference, lease) -- для dynamic secrets
Revoke(lease)
Реализации: builtin (Postgres + envelope-шифрование AES-256-GCM) и env.
Заявлено на фазу 2 (интерфейс бэкендов и lease-модель ниже — план):
infisical, vault/openbao, aws-sm, gcp-sm, azure-kv. Выбор
бэкенда — конфигурация инсталляции, не код.
#3.4. Credential Lease (аренда креденшела)
Выдача значения на ограниченный срок конкретному запуску. Lease несёт TTL, идентичность получателя (execution identity) и подлежит отзыву. Для статических секретов lease — это разрешение на кэширование в памяти ядра; для динамических — короткоживущий токен, сгенерированный провайдером.
#4. Принцип: агент не держит секрет
#4.1. Разрешение на границе исполнения
Единственная точка, где ссылка превращается в значение, — ядро в момент исполнения инструмента:
- HTTP-инструменты (MCP, fetch-подобные): значение подставляется в заголовок авторизации запроса, не в аргументы инструмента.
- Git-операции: credential helper на стороне ядра.
run_commandв песочнице: переменная окружения конкретного процесса (только память, без записи на диск; префиксный неймингSECRET_*).- Файловые случаи, где без секрет-файла нельзя (legacy-тулы): tmpfs внутри песочницы, уничтожаемая вместе с контейнером.
#4.2. Куда значение не попадает никогда
- в промпт и контекст модели (в т.ч. субагентов при делегировании);
- в аргументы инструментов, которые агент формирует сам;
- в event journal, trajectories, knowledge;
- в персистентные файлы воркспейса и кэш-артефакты;
- в логи ядра и песочницы.
#4.3. Redaction как вторая линия
Даже при брокер-схеме журнал получает фильтр маскирования на ingest:
известные форматы токенов (ghp_*, gho_*, AKIA*, xoxb-*, Bearer-заголовки,
приватные ключи) заменяются на <redacted:kind>. Фильтр настроен на
гарanteed-детектируемые паттерны и не пытается угадать «похожее на ключ» —
чтобы не калечить легитимный контент. Это защита от человеческих ошибок
вне брокер-пути, а не основной механизм.
#5. Границы доверия
┌──────────┐ ссылка secret:// ┌─────────────┐ lease + value ┌────────────┐
│ Агент │ ─────────────────────▶│ Ядро │ ────────────────▶ │ Provider │
└──────────┘ │ (execution │ │ (vault/ │
▲ │ identity) │ │ builtin) │
│ значение не проходит └─────────────┘ └────────────┘
- Песочница остаётся без сети по умолчанию. Агенты не ходят в vault сами SDK — это открывало бы egress и размазывало точку интеграции. Единственный клиент провайдера — ядро.
- Аутентификация ядра в провайдере — по execution identity инсталляции:
AppRole/OIDC/mTLS (Vault), machine identity (Infisical), IAM-роль
(облачные SM). Креденшел аутентификации ядра — единственный секрет,
который существует вне провайдера, и он подчиняется конвенции
<VAR>_FILE. - Видимость секретов наследуется по оргдереву тем же правилом, что у
остальных ресурсов:
org_unit_id IS NULLилиorg_unit_id IN path(U). Мультиорговые инсталляции не видят чужие секреты. - Привязка «агент → секрет» отдельным ACL: ресурс виден департаменту не значит, что каждый агент департамента его получает. Явный grant в конфигурации агента (или команды), проверяемый политиками.
#6. Динамические секреты и ротация
- Где целевая система умеет короткоживущие креденшалы — выдаём их, а не статические: динамические БД-аккаунты, PKI-сертификаты, облачные STS.
- Статические секреты провайдер ротирует по расписанию; ядро узнаёт новое
значение при следующем
Get(кэш с TTL не даёт протухнуть надолго). - Завершение рана →
Revoke(lease)для динамических; для статических — сброс кэша. - Отзыв по инциденту — операция UI/CLI: отзыв lease'ов + (для динамических) инвалидация на стороне целевой системы.
Федерация без статических токенов — отдельный приоритетный кейс:
- GitHub Apps: installation-токен на 1 час вместо PAT — закрывает главный сценарий «агент пушит код» без долгоживущих ключей в принципе.
- OIDC/workload identity federation для облачных API.
#7. Аудит
Каждое обращение к секрету — событие journal:
secret.issued (run_id, secret_id, lease_id, ttl) -- выдан lease
secret.denied (run_id, secret_id, policy_violation) -- отказ с причиной
Заявлено на фазу 2: secret.accessed (использован в операции),
secret.revoked (отзыв lease'ов).
Это даёт ответы на вопросы «кто пользовался скомпрометированным ключом», «какие секреты фактически не используются» (кандидаты на отзыв) — и ложится в существующую модель наблюдаемости: события видны в трейсе рана, агрегируются в таймлайне, экспортируются в SIEM.
#8. Доступ агента к секретам
Агенту доступны два инструмента:
secret_list— имена и описания секретов, доступных его execution identity (без значений и без external_ref).secret_request— запрос привязки нового секрета к агенту/рану (фаза 2, не реализован). Слабые запросы (секрет существует и доступен по политикам) удовлетворяются автоматически; остальное уходит в approvals — существующий HITL-контур.
В системном промпте агента фиксируется контракт: «секреты передаются ссылками secret://, значения подставляются системой; не пытайся читать, записывать или пересылать значения токенов».
#9. Отказоустойчивость
- Провайдер не должен лежать на горячем пути каждого вызова инструмента: значения кэшируются в памяти ядра на время TTL lease.
- Недоступность провайдера: раны, не требующие секретов, не замечают ничего; требующие — по умолчанию fail-closed (ошибка инструмента с внятным текстом), политика может разрешить fail-open для конкретных некритичных секретов.
builtin-провайдер живёт в том же Postgres, что и журнал — не добавляет новой точки отказа за пределами уже существующих.
#10. Оценка бэкендов для больших компаний
| Критерий | Infisical | Vault / OpenBao | CyberArk | Облачные SM | builtin (Postgres) |
|---|---|---|---|---|---|
| Self-hosted, данные у себя | ✅ open source | ✅ (OpenBao — MPL; Vault — BUSL) | ✅ (платный) | ❌ | ✅ |
| Зрелость на enterprise-масштабе | растёт | ✅ доказан | ✅ инкамбент | ✅ в своём облаке | локальный масштаб |
| Динамические секреты, TTL | ✅ | ✅ широчайшие | ✅ | частично | ❌ |
| HSM / FIPS | ограниченно | ✅ (Vault ent) | ✅ | зависит от облака | ❌ |
| Аудит → SIEM | API, допиливать | ✅ | ✅ | CloudTrail-стайл | journal-события |
| Усилие внедрения | недели | месяцы | месяцы | низкое (если облако своё) | дни |
| Конфликт с корпстандартом | вероятен | обычно уже одобрен | обычно уже одобрен | обычно уже одобрен | нет |
Выводы:
- Infisical Agent Vault — лучший выбор для greenfield и среднего масштаба: быстро, современно, панель наблюдения за агентскими креденшелами, self-hosted. Главный барьер в большой компании — почти всегда уже есть одобренный ИБ vault, и второй системы не дадут завести.
- Vault/OpenBao — дефолт, если он уже стоит в компании; интегрируемся, а не спорим. OpenBao — если BUSL-лицензия Vault неприемлема.
- Облачные SM — минимальное трение, если компания вся в одном облаке.
- builtin — достаточен для одиночных установок и дева; не претендует на HSM-грейд и автоматическую ротацию.
Архитектурный вывод: благодаря интерфейсу провайдера выбор бэкенда — конфигурация инсталляции. Начинаем с builtin, добавляем Infisical и Vault/OpenBao адаптеры, enterprise-клиент подключает то, что одобрено.
#11. API
GET/POST/PUT/DELETE /v1/secrets— CRUD метаданных (значения только через провайдер; в API не возвращаются никогда).POST /v1/secrets/{id}/value— запись значения (туда), без чтения обратно для внешних провайдеров.- Биндинг к org unit — общий механизм ресурсов:
PUT /v1/org/resources/secret/{id}/binding. - Grants агенту — поле
secretsв определении агента (Agent.Definition.Secrets []string). - Заявлено на фазу 2:
POST /v1/secrets/{id}/rotate(принудительная ротация),POST /v1/secrets/{id}/revoke(отзыв lease'ов), отдельные эндпоинты грантов.
#12. UI
- Раздел «Секреты»: список с фильтрацией по оргединице, провайдеру, привязке к агентам; создание/редактирование метаданных; запись значения (maskeд-input, значение не возвращается на клиент после сохранения).
- В карточке агента и команды — гранты секретов с тумблерами.
- В конфигурации MCP-серверов и провайдеров — поля типов
secretсо выбором из доступных секретов вместо сырых ключей. - В трейсе рана — события
secret.*компактной строкой («Секрет выдан github-repo-write · TTL 1h»), без значений.
#13. Фазы реализации
#Фаза 1 — брокер и builtin (закрывает основную боль)
Ресурс workspace_secret + миграция; ссылки secret:// в конфигах
агентов/MCP/провайдеров; разрешение в ядре на границе инструментов
(HTTP-заголовки, env песочницы, tmpfs); события secret.*;
redaction-фильтр journal; тул secret_list; UI-раздел.
Внешних зависимостей нет.
#Фаза 2 — внешние провайдеры
Интерфейс SecretProvider как публичный контракт; адаптеры Infisical и Vault/OpenBao; аутентификация ядра по execution identity; динамические секреты и ротация; revoke-операции; grants + policies-гейтинг.
#Фаза 3 — федерация и enterprise
GitHub Apps-брокер (installation-токены); OIDC/STS-федерация для облаков; экспорт аудита в SIEM; fail-open политики; синхронизация видимости с оргструктурой при её изменении (события org unit уже есть).
#14. Критерии готовности
#Фаза 1
- Агент выполняет git push по
secret://-ссылке; значение не появляется в журнале, трейсе, воркспейсе и промпте (проверяется grep по дампу рана). - Токен в выводе команды маскируется при ingest.
- Секрет с
org_unit_idне виден агентам чужого поддерева. - Перезапуск рана не требует перевыпуска токена.
#Фаза 2
- Инсталляция с Infisical и с Vault проходит один и тот же сценарий без изменений в коде агентов.
- Динамический секрет истекает по TTL и продлевается без участия человека.
- Отзыв секрета прекращает работающие lease'ы в течение минуты.
#Фаза 3
- Агент пушит код через GitHub App без какого-либо долгоживущего токена.
- Аудит доступов восстанавливает картину «кто чем пользовался» за период.
#15. Открытые вопросы
- Хранение мастер-ключа builtin-провайдера — решено:
KERNEL_SECRET_MASTER_KEY/KERNEL_SECRET_MASTER_KEY_FILE, при отсутствии — автогенерация с сохранением вworkspace_settingи предупреждением; облачный KMS — на будущее. - Нужен ли аудит «кто выдал grant» отдельным журналом или достаточно существующих событий изменения ресурсов.
- Мультиоблачность внутри одной инсталляции (секреты в AWS SM при стеке в GCP) — вероятно нет, зафиксировать как ограничение.
- Срок жизни redaction-паттернов: ведение списка форматов токенов как данные, а не код.
#16. Архитектурные ограничения
- Значение секрета не пересекает границу ядра в сторону модели — это инвариант, который тестируется на уровне CI-сценариев, а не надежд.
- Провайдер секретов не становится зависимостью ранов, не использующих секреты.
- Схема ссылок
secret://едина для всех конфигов; сырые значения в конфигах допускаются только в режиме локальной разработки (env/fileпровайдеры) и помечаются предупреждением в UI. - Секреты не привязываются к проектам: это сквозной ресурс оргструктуры (тот же принцип, что агенты, навыки и MCP-серверы).