---
doc_id: ARC-CK8S-SECRET-RESOLUTION-0001
type: design-doc
title: kodex — безопасное разрешение секретов
status: active
owner_role: SA
created_at: 2026-05-11
updated_at: 2026-05-12
related_issues: [711, 718, 719, 723, 726]
related_prs: []
related_adrs: []
approvals:
  required: ["Owner"]
  status: approved
  request_id: "owner-2026-05-11-secret-resolution"
  approved_by: "ai-da-stas"
  approved_at: 2026-05-11
---

# Безопасное разрешение секретов

## Кратко

Сырые значения секретов не являются частью публичных gRPC-ответов, доменных событий, аудита, БД и операторских моделей чтения. `access-manager` подтверждает право использования внешнего аккаунта и возвращает только `secret_store_type + secret_store_ref`. Общий Go-пакет `libs/go/secretresolver` получает значение по этой ссылке внутри процесса вызывающего сервиса только на время конкретной операции.

## Границы ответственности

| Компонент | Ответственность |
|---|---|
| `access-manager` | Владеет внешними аккаунтами, политикой использования, привязками аккаунтов к областям и ссылками на секреты. |
| `libs/go/secretresolver` | Даёт общий контракт `Resolver` и `Checker`, безопасный тип `SecretValue`, выбор реализации хранилища по `store_type`, поддержку смонтированных Kubernetes Secret, env и Vault KV v2. |
| `provider-hub` | После успешного `ResolveExternalAccountUsage` вызывает `Resolver.Resolve` для пакетной GitHub-сверки в режиме только чтения, выполняет внешний API-вызов и сразу очищает значение. Операции записи в провайдера используют тот же контур в отдельном срезе. |
| `package-hub` | Для статуса заполненности установки использует только `Checker.Check`; получение сырого значения секрета в пакетном домене запрещено. |
| `fleet-manager` | Для проверки связности кластера вызывает `Resolver.Resolve` по `secret_store_type + secret_store_ref`, поддерживает env, смонтированные Kubernetes Secret и Vault KV v2, строит Kubernetes client только в памяти процесса, выполняет короткий безопасный probe и сразу очищает значение. |
| Runtime/deploy-контур | Подготавливает mount, переменные конфигурации resolver-клиента и доступ к выбранному хранилищу секретов. |

## Запрещено

- Возвращать значение секрета через proto, OpenAPI, AsyncAPI или публичные модели чтения.
- Хранить значение секрета в PostgreSQL, outbox, `platform-event-log`, теле аудита, журнале операций, трассировке, метках метрик или тексте ошибки.
- Логировать значение секрета или путь, который раскрывает чувствительную структуру хранилища.
- Передавать секрет в доменные события или модели операторского интерфейса.
- Добавлять локальный обход вокруг `access-manager`, чтобы сервис сам выбирал внешний аккаунт или хранил токен.

## Контракт использования

1. Вызывающий домен выбирает внешний аккаунт по своей политике или получает его из ранее сохранённого курсора.
2. Домен вызывает `ResolveExternalAccountUsage` в `access-manager` с выбранным аккаунтом, действием и областью.
3. `access-manager` проверяет статус аккаунта, поставщика, привязку области, разрешённые действия и наличие ссылки на секрет.
4. В ответ возвращаются `provider_slug`, разрешённые действия, `secret_store_type` и `secret_store_ref`; значение секрета не возвращается.
5. Если операции действительно нужен токен, домен вызывает `secretresolver.Resolver.Resolve`.
6. Полученный `SecretValue` используется только в памяти процесса и только для конкретного внешнего вызова; вызывающий код сразу ставит `defer value.Clear()`.
7. Для проверки наличия секрета без возврата значения вызывающему коду используется `secretresolver.Checker.Check`.

## Пакетные установки

Для установок пакетов `access-manager` хранит отдельные привязки логических ключей из схемы пакета к `SecretBindingRef`. `package-hub` читает их через `ListPackageInstallationSecretRefs` и получает только:

- логический ключ секрета;
- статус настройки: `configured`, `missing`, `invalid`, `disabled`;
- `secret_store_type`;
- `secret_store_ref`;
- безопасные строковые метаданные без secret/value/token/key-полей и вложенных структур.

`package-hub` не получает значение секрета и не хранит ссылку как свою каноническую модель. Если нужно пересчитать заполненность установки, `package-hub` берёт список ожидаемых ключей из своей `PackageSecretSchema`, запрашивает безопасные ссылки в `access-manager` и проверяет доступность только через `secretresolver.Checker`. Статус `missing` может быть вычислен на стороне `access-manager`, когда ожидаемый ключ передан в запросе, но привязки нет.

По результату проверки `package-hub` сохраняет только итоговый `secret_binding_status` установки:

- `not_required`, если у версии пакета нет схемы секретов.
- `missing`, если отсутствует обязательный секрет или привязка отключена.
- `partial`, если обязательные секреты доступны, но часть необязательных секретов не готова.
- `complete`, если все ожидаемые секреты доступны.
- `invalid`, если ссылка или статус привязки невалидны.
- `check_failed`, если хранилище не удалось проверить без чтения значения.

## Типы хранилищ первой версии

### `kubernetes_mounted_secret`

Реализация читает Kubernetes Secret, которые уже смонтированы в workload как файлы. Это нативный механизм Kubernetes volume projection: платформа не обращается к Kubernetes API во время разрешения секрета. Формат ссылки:

```text
namespace/secret-name#key
```

Resolver-клиент отображает ссылку на файл:

```text
<configured-root>/<namespace>/<secret-name>/<key>
```

Ограничения:

- workload видит только те Secret, которые runtime/deploy-контур заранее смонтировал в Pod;
- новые внешние аккаунты или ротации, требующие нового Secret volume, могут требовать изменения манифеста и rollout workload;
- `subPath` для такого mount не использовать, потому что Kubernetes не обновляет такие mount автоматически;
- это не реализация через Kubernetes API и не даёт сервису права `get/list/watch` на Secret.

### `env`

Реализация читает значение из переменной окружения процесса. Формат ссылки:

```text
ENV_VARIABLE_NAME
```

Этот тип нужен для совместимости с внешними пакетами и Helm-чартами, где Vault injector, External Secrets Operator или другой runtime-контур уже положил секрет в env контейнера. Ограничения:

- переменные окружения не обновляются в уже запущенном контейнере без restart/rollout;
- значение присутствует в окружении процесса весь срок жизни контейнера, поэтому этот тип не является предпочтительным для динамических provider-токенов;
- `Checker.Check` может проверить только наличие переменной в процессе, а не свежесть внешнего хранилища.

### `vault`

Реализация читает Vault KV v2 через официальный Go SDK `github.com/hashicorp/vault/api`. Формат ссылки:

```text
mount/path/to/secret#key
```

Например, `secret/provider/github#token` означает mount `secret`, путь `provider/github`, поле `token`. Настройка адреса Vault, namespace, TLS, auth method и token остаётся деталью runtime/deploy-контура и конфигурации resolver-клиента. Домены получают только `secret_store_type + secret_store_ref`; значение не возвращается через `access-manager` и не сохраняется в доменных БД.

`Checker.Check` для Vault использует запрос только метаданных KV v2 через `GetMetadata`: он подтверждает наличие пути и текущей неудалённой версии без чтения `data`. Это безопасный путь для `package-hub`, но он не доказывает наличие конкретного поля `#key`; проверка на уровне конкретного ключа требует чтения значения через `Resolver.Resolve` и разрешена только контурам, которым можно выполнять внешнюю операцию с секретом.

## Безопасный тип значения

`secretresolver.SecretValue`:

- хранит bytes в неэкспортируемом поле;
- возвращает копию через `Bytes()`;
- поддерживает `Clear()` для best-effort зануления внутреннего буфера после операции;
- маскирует вывод `fmt.Stringer` и `%#v`;
- запрещает JSON и text serialization;
- не должен попадать в структуры, которые сериализуются, логируются или сохраняются.

## Наблюдаемость

Допустимо фиксировать:

- факт успешного или неуспешного разрешения ссылки;
- `store_type`;
- безопасный код ошибки: неизвестный тип хранилища, ссылка не найдена, хранилище недоступно;
- correlation id, command id и идентификатор внешнего аккаунта.

Запрещено фиксировать:

- сырое значение;
- частичное значение;
- токен в замаскированном виде;
- полный путь хранилища, если он раскрывает внутреннюю структуру секретов;
- `secret_store_ref` в логах общего назначения.

## Последующие расширения

- реализация через Kubernetes API, если mounted/env/Vault окажутся недостаточными для отдельных workload и будет согласована строгая RBAC-модель без широкого `list/watch` Secret.
- Подключение resolver-клиента к provider-операциям записи `provider-hub` после согласования MCP/agent-manager инструментов.

## Апрув

- request_id: `owner-2026-05-11-secret-resolution`
- Решение: approved
- Комментарий: контракт безопасного разрешения секретов согласован как общий междоменный механизм для внешних аккаунтов и пакетных установок.
