Перейти к содержанию

Секреты

Имена входят. Значения никогда не выходят обратно.

Ни один API-эндпоинт не возвращает сохранённое значение секрета. Ни один экран UI не предлагает кнопку «показать». Тот, кто потерял значение, заменяет его — это тот же вызов, что и создание, через ту же форму. Это не решение уровня политики: путь чтения попросту отсутствует в коде. (REQ-1558)


Синтаксис ссылок

Три формы ссылок допустимы везде, где Provisa разрешает учётные данные:

Форма Откуда разрешается Кто может её использовать
${env:VAR_NAME} Окружение серверного процесса Только конфигурация развёртывания
${secret:NAME} Хранилище секретов организации — общее для всех участников Любое поле, принимающее ссылку на учётные данные
${user:NAME} Личное хранилище секретов действующего лица Любое поле, принимающее ссылку на учётные данные

Разрешение везде работает по принципу fail-closed (отказ в доступе по умолчанию). Неизвестное имя провайдера, незаданное имя и недоступный бэкенд — каждый случай вызывает ошибку. Ссылка, которую не удалось разрешить, никогда не подменяется молча пустой строкой. (REQ-1557) [tool-verified: provisa/core/secrets.py:92-117]

Формат имени

Имена секретов должны соответствовать [A-Za-z_][A-Za-z0-9_]* — буквы, цифры и подчёркивания, начиная с буквы или подчёркивания. Ограничение продиктовано практикой: ${secret:NAME} разбирается грамматикой ссылок, которая читает всё до закрывающей }. Имя, содержащее фигурную скобку, пробел или двоеточие, дало бы ссылку, которая разберётся как что-то другое. [tool-verified: provisa/core/secrets_store.py:61]


Два хранилища, одна служба

У каждой организации два хранилища секретов. Оба живут внутри одной службы секретов. (REQ-1560)

Хранилище организации — учётные данные, которые администратор организации кладёт сюда, общие. Каждый участник, ссылающийся на ${secret:DATABASE_TOKEN}, получает одно и то же значение. Это для учётных данных, которыми владеет организация: общий пароль базы данных, ключ сервисного аккаунта, токен развёртывания. Для чтения или записи в хранилище организации требуется возможность org_settings.

Личное хранилище — учётные данные, положенные сюда, принадлежат ровно одному человеку. Когда GIT_TOKEN есть у двоих, ${user:GIT_TOKEN} разрешается в тот, чей владелец действует сейчас. Один и тот же текст ссылки выдаёт каждому его собственные учётные данные. Тот, кто ничего не сохранил, получает ошибку, а не чужое значение. Личное хранилище не закрыто никакой возможностью — держать собственные учётные данные не привилегия, которую выдаёт администратор. И нет синтаксиса запроса, чтобы назвать чужое хранилище. [tool-verified: provisa/api/admin/secrets_router.py:86-103]

Область — часть самой ссылки, а не разрешение вокруг неё. ${secret:NAME} и ${user:NAME} никогда не отвечают друг за друга.


Выбор службы секретов

Admin → Security → Secrets service. Панель видна всем, кто обладает возможностью platform_settings. Перечислен каждый бэкенд, известный сборке, независимо от того, установлен ли SDK. Затенённая строка сообщает, какого пакета Python не хватает — панель называет его, а не прячет вариант целиком.

В поставке пять бэкендов:

Ключ Подпись Требуется
provisa Provisa (built-in, encrypted) Ничего; это значение по умолчанию
hashicorp_vault HashiCorp Vault (KV v2) hvac
aws_secrets_manager AWS Secrets Manager boto3
gcp_secret_manager Google Secret Manager google-cloud-secret-manager
azure_key_vault Azure Key Vault (secrets) azure-keyvault-secrets

[tool-verified: provisa/core/secrets_registry.py:161-299]

Выбор работает по принципу fail-closed: неизвестный или недоступный бэкенд вызывает ошибку при старте, а не молча откатывается на другой. (REQ-1557)

Собственные учётные данные бэкенда

Учётные данные подключения к центральному бэкенду — это конфигурация процесса. Они берутся только из ${env:...} — никогда из ${secret:...}. Служба секретов, чьи собственные учётные данные лежат внутри неё самой, не может быть открыта, поэтому цепочка доверия по замыслу оканчивается в окружении хоста. Реестр обеспечивает это: любое значение конфигурации в спецификации бэкенда разрешается с providers=("env",) до того, как бэкенд будет сконструирован. [tool-verified: provisa/core/secrets_registry.py:128-141]

Пример — конфигурация Vault в provisa.yaml:

secrets:
  provider: hashicorp_vault
  hashicorp_vault:
    url: https://vault.internal:8200
    token: ${env:VAULT_TOKEN}   # process env only — never ${secret:...}
    mount: secret

Центральная служба против встроенной

Когда настроена центральная служба, Provisa читает из неё, но не пишет в неё. Создание и удаление записей принадлежит центральной службе — эти операции выполняет её собственный инструментарий. Страница Secrets так и сообщает и не предлагает кнопку создания. (REQ-1557)

Когда активен встроенный бэкенд provisa, страница Secrets полностью доступна на запись: создание, замена и удаление из UI или через API.


Встроенное хранилище Provisa

Значение по умолчанию, когда центральная служба не настроена. Каждая строка в secrets_store содержит зашифрованный конверт-блоб — колонка value двоичная, а не текстовая, и ключ расшифровки живёт в окружении процесса, а не в базе данных. Копия плоскости управления без мастер-ключа развёртывания содержит шифротекст и ничего больше. (REQ-1558)

Шифрование никогда не опционально. Когда ключ шифрования на уровне процесса не настроен, хранилище откатывается на локальную связку ключей. Если у хоста нет связки ключей, чтобы хранить ключ, хранилище отказывается писать, а не сохраняет значение в открытом виде. [tool-verified: provisa/core/secrets_store.py:130-159]

Форма хранения [tool-verified: provisa/core/schema_admin.py:493-505]:

Колонка Тип Назначение
org_id Text Организация, владеющая этим секретом
owner_id Text "*" для хранилища организации; идентификатор пользователя для личного хранилища
name Text Имя ссылки
value LargeBinary Зашифрованный конверт-блоб
description Text Для чего этот секрет — никогда не выводится из значения
updated_by Text Кто задал его последним

Колонка value не выбирается ни в одном запросе на перечисление. [tool-verified: provisa/core/secrets_store.py:214-235]


Эндпоинты API

Все маршруты находятся под /admin/orgs/{org_id}. Хранилище организации требует org_settings в этой организации. Личное хранилище не требует никакой возможности — владелец считывается из аутентифицированной личности; параметра запроса, чтобы назвать чужое хранилище, не существует.

Метод Путь Что делает
GET /secrets Перечислить имена и ссылки хранилища организации
PUT /secrets/{name} Создать или заменить один секрет организации
DELETE /secrets/{name} Удалить один секрет организации
GET /my-secrets Перечислить личные имена и ссылки вызывающей стороны
PUT /my-secrets/{name} Создать или заменить один секрет вызывающей стороны
DELETE /my-secrets/{name} Удалить один секрет вызывающей стороны

Каждый ответ возвращает метаданные — имя, описание, updated_at, updated_by и строку reference, готовую к вставке, — но никогда значение. Тело PUT несёт value (обязательно) и description (необязательно). Замена — тот же вызов, что и создание: идентичностью служит имя, а не отдельный ID.

Каждая запись фиксируется в журнале аудита. Запись журнала называет действующее лицо и имя секрета. Значение не записывается, даже его длина. [tool-verified: provisa/api/admin/secrets_router.py:106-117]


Где разрешается ${secret:NAME}

Разрешение происходит внутри операции, привязанной к контексту, а не во время импорта или старта. Хранилище читает и расшифровывает секреты организации один раз в начале этой операции и держит карту в ContextVar всё время её выполнения. Вне привязанной операции ${secret:NAME} вызывает ошибку. (REQ-1557) [tool-verified: provisa/core/secrets_store.py:269-290]

Привязку устанавливают два места вызова:

Операции с удалённым git. Когда URL удалённого репозитория организации содержит ссылку ${secret:...} или ${user:...} — например, токен push, встроенный в URL, — роутер окружений привязывает и хранилище организации, и личное хранилище действующего пользователя вокруг вызова git. Форма ${user:GIT_TOKEN} означает, что коммит ложится под учётными данными того, кто его отправил, а не общего сервисного аккаунта. [tool-verified: provisa/api/admin/environments_router.py:1263]

Чтение API-ключей AI-вендора. Когда Provisa читает ключ LLM-вендора организации и этот ключ хранится как ссылка ${secret:NAME}, bound_to_request_org устанавливает хранилище организации для этого запроса. Ссылка разрешается на выходе; сам текст ссылки вендору никогда не отправляется. (REQ-1580) [tool-verified: provisa/core/org_secrets.py:76-79]


Ключи AI-вендоров организации как ссылки на секреты

Ключ AI-вендора организации (Anthropic, OpenAI и другие) можно хранить как ссылку ${secret:NAME} вместо литерального ключа. (REQ-1580)

Сначала положите ключ в хранилище организации:

PUT /admin/orgs/{org_id}/secrets/OPENAI_KEY
{ "value": "sk-...", "description": "OpenAI production key" }

Затем настройте конфигурацию AI организации так, чтобы она ссылалась на него:

vendor key field → ${secret:OPENAI_KEY}

Ссылка хранится зашифрованной в org_secrets. Во время запроса Provisa разрешает ${secret:OPENAI_KEY} по хранилищу организации и передаёт литеральный ключ в SDK вендора. Ротация записи в хранилище вступает в силу немедленно — изменять конфигурацию на стороне настроек организации не нужно. [tool-verified: provisa/core/org_secrets.py:64-79]


Доступ администратора платформы

Администратор платформы, управляющий плоскостью управления, не имеет доступа на чтение к значениям секретов ни одной организации. Гард org_settings явно отказывает cross_org и обходу платформы: администрирование жизненного цикла организации — не чтение учётных данных, которые эта организация хранит. Сервер обеспечивает это независимо от UI. (REQ-1361) [tool-verified: provisa/api/admin/secrets_router.py:53-83]


Смотрите также