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

Секреты

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

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


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

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

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

В мультиарендном развёртывании храните учётные данные платформы в сервисе секретов, а не в переменных окружения. Администратор арендатора может записать ${env:...} в собственное поле учётных данных, поэтому любое значение в окружении процесса доступно каждому арендатору. Оставьте в окружении только собственные учётные данные бэкенда секретов.

Разрешение везде работает по принципу 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.


Секреты организации в центральном сервисе

Развёртывание, которое обслуживает много организаций и читает центральный сервис, читает его для каждой организации внутри собственного префикса этой организации. Ссылка остаётся коротким именем (${secret:db#password}); префикс добавляется при чтении. (REQ-1557) [tool-verified: provisa/core/secrets_providers.py, org_namespace]

Сервис Где хранится секрет db организации acme
HashiCorp Vault (KV v2) orgs/acme/db
AWS Secrets Manager orgs/acme/db
GCP Secret Manager org-acme--db
Azure Key Vault o4-acme-db (o, длина id организации, id с _, записанным как -, затем имя)

Ссылка отклоняется по имени, когда:

  • её имя вышло бы за пределы префикса: начинается с /, содержит сегмент .. или обратную косую черту либо содержит / в GCP или Azure, где имена плоские;
  • её имя слишком длинное для сервиса после добавления префикса (GCP 255 символов, Azure 127); это отклоняется при сохранении ссылки с указанием предела;
  • к операции, которая её читает, не привязана никакая организация.

Развёртывание с одной организацией читает ссылку ровно так, как она записана, без префикса: сервис, к которому оно подключено, принадлежит этой организации, и ничего переносить не нужно.

Переход на префикс. В развёртывании с многими организациями каждый секрет, на который ссылается организация, должен находиться под префиксом этой организации. Скопируйте каждый туда собственными средствами сервиса, затем сократите ссылку до имени внутри префикса (${secret:teams/data/db}, хранящийся в orgs/acme/db, становится ${secret:db}). Ссылка, чей секрет не перенесён, завершается ошибкой «не найдено»; она не читается из прежнего места.

Встроенное хранилище 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]


Собственные учётные данные хранилищ развёртывания

Пароли хранилищ, которые запускает Provisa (PostgreSQL, Redis, MinIO, административная учётная запись Grafana и общий секрет кластера Trino), не являются секретами ни в одном из хранилищ секретов. Они генерируются для каждого развёртывания и хранятся самим развёртыванием: в ~/.provisa/store-credentials.env (режим 600), в состоянии Terraform или в Kubernetes Secret, имя которого задано в credentials.existingSecret Helm-чарта. См. Укрепление развёртывания.


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

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


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