ADR-0018: Master key для шифрования credentials и политика ротации DEK
Status: accepted Date: 2026-04-17 Deciders: команда проекта
Контекст
ADR-0010 ввёл envelope encryption для секретных полей SupplierCredential:
master key + per-customer DEK. Сам выбор бэкенда master key и регламент
ротации DEK были оставлены как “deployment decision (TBD)“.
Эта недоопределённость блокирует prod-готовность:
- Без выбранного бэкенда невозможно задать процедуру bootstrap’а окружения.
- Без регламента ротации DEK мы не имеем compliance-приемлемой системы хранения чувствительных данных клиентов (ИНН, токены доступа к API поставщиков).
- Break-glass и аудит расшифровки не привязаны к конкретному источнику ключа.
Threat model (явно):
- T1: компрометация одной реплики backend’а — атакующий не должен прочитать секреты других клиентов.
- T2: компрометация снапшота PostgreSQL — атакующий не должен расшифровать секреты без master key.
- T3: компрометация одного DEK — должна затронуть только одного customer’а и быть восстановимой через ротацию.
- T4: insider abuse (разработчик / SRE) — расшифровка должна быть аудируемой.
Не в scope: защита от полной компрометации платформы (root + KMS access одновременно).
Решение
1. Абстракция MasterKeyProvider
Master key используется только через интерфейс:
MasterKeyProvider
├── Encrypt(plaintext) → ciphertext // оборачивает DEK в master-key envelope
├── Decrypt(ciphertext) → plaintext
└── ActiveKeyId() → string // для metadata записи "каким master key завёрнут DEK"
Каждая зашифрованная DEK-запись хранит master_key_id — позволяет вращать master key без single-shot миграции.
2. Реализации провайдера (по убыванию приоритета для prod)
| Провайдер | Где допустим | Замечание |
|---|---|---|
| HashiCorp Vault Transit | prod, staging | дефолт для prod; даёт audit, ротацию ключа без выгрузки материала наружу |
| AWS KMS / Yandex Cloud KMS | prod (cloud-native deployments) | альтернатива Vault для cloud-only установок |
| Local file (sealed by passphrase) | dev, CI | passphrase из MASTER_KEY_PASSPHRASE env, файл монтируется в контейнер только в dev |
Plaintext env (MASTER_KEY_RAW) | только локальная разработка одного разработчика | flag TRACIUM_ALLOW_INSECURE_MASTER_KEY=1 обязателен; в prod backend паникует на старте |
Bootstrap процедура per env описана в 40-operations/deployment.md.
3. Ротация master key
- Внешний trigger (Vault rotation policy / KMS rotate-key).
- При смене ActiveKeyId — все новые DEK заворачиваются новым master key.
- Существующие DEK перезаворачиваются фоновым джобом
rewrap-deks(idempotent, batch по customer_id). - Активной может быть >1 master key (старый — для расшифровки legacy DEK, новый — для записи).
- Полное удаление старого master key — только после
rewrap-dekscomplete + 7 дней grace.
4. Per-customer DEK
- Один DEK на
customer_id(для customer-scope credentials) и один DEK наsystem(для system credentials). - DEK хранится в таблице
credential_dek (customer_id, dek_ciphertext, master_key_id, created_at, retired_at). - Кэш расшифрованных DEK в памяти процесса — TTL 5 минут, max 1000 entries, явная инвалидация при ротации.
5. Регламент ротации DEK
| Триггер | SLA на ротацию |
|---|---|
| Плановая (compliance) | каждые 180 дней на customer_id |
| Реактивная: подозрение компрометации credential | в течение 1 часа |
| Реактивная: компрометация master key | в течение 24 часов на все DEK |
| Удаление аккаунта customer’а | немедленно, DEK помечается retired_at и через 30 дней удаляется (после анонимизации credentials) |
Ротация:
- Сгенерировать новый DEK.
- Расшифровать все credentials customer’а старым DEK, зашифровать новым в одной транзакции.
- Старый DEK помечается
retired_at, удаляется через 30 дней. - Событие
CredentialDekRotated{customer_id, reason}вevent_store.
Owner регламента — security lead (или CTO в его отсутствие).
6. Audit и break-glass
- Каждая операция
Decryptлогируется вcredential_decrypt_audit (event_id, actor, credential_id, customer_id, master_key_id, reason, ts). - Поле
reasonобязательно:pricing_pipeline | manual_break_glass | rewrap_job | validation. - Break-glass (расшифровка вне обычного pipeline) требует:
- двух approver’ов в admin UI (4-eyes);
- указания
reasontext; - открытого incident ticket (ID указывается);
- автоматической нотификации в security channel.
- Все break-glass операции попадают в отдельный отчёт раз в неделю.
Последствия
Плюсы
- Bootstrap окружения детерминирован —
MasterKeyProviderединая точка контракта. - Ротация master key и DEK не требует downtime.
- Compromise blast radius ограничен: один DEK = один customer.
- Audit полный — расшифровка не выполняется неатрибутируемо.
Минусы
- Vault или KMS становится hard dependency для prod (но именно это даёт security gain).
- Сложность реализации rewrap job’а: должен быть idempotent + транзакционным per customer.
- Дополнительная latency на pricing pipeline при cache miss DEK (mitigate: prefetch при первом запросе клиента).
Нейтральные последствия
- Возможен переход с Vault Transit на KMS (или наоборот) без миграции данных credentials —
достаточно сменить backend
MasterKeyProviderи однократно прогнатьrewrap-deksпод новым master key.
Рассмотренные альтернативы
Hardcoded в env только
Не масштабируется. Не даёт audit, ротации, защиты от insider’а. Допустим только для dev.
Per-credential ключ (без promotion DEK на customer)
Слишком много объектов ключей, дороже ротация при компрометации одного credential.
Внешнее хранение секретов целиком (Vault как KV)
Vault KV становится критическим runtime-компонентом для каждого pricing-запроса. Transit-режим достаточен — он шифрует/расшифровывает, не хранит сами credentials.
Ссылки
- ADR-0009 (multi-tenant supplier credentials).
- ADR-0010 (pluggable credential schemas).
../../10-business/contexts/credentials.md— секция “Безопасность”.../../50-processes/adding-customer-credentials.md— секция “Безопасность”.