Перейти к основному содержимому

Секреты

dagstack/config не хранит секреты — он корректно их транспортирует и маскирует в диагностике. Хранение самих секретов остаётся за внешними инструментами (env-переменные, Vault, Kubernetes Secret, облачные секрет-менеджеры).

Базовое правило

Секрет — это env-переменная, которая попадает в конфигурацию через ${VAR}. Никогда не пиши значение секрета в app-config.yaml или app-config.${ENV}.yaml:

НЕПРАВИЛЬНО — секрет уходит в git
database:
password: "s3cr3t-prod-pw"
ПРАВИЛЬНО — секрет приходит из окружения, не коммитится
database:
password: "${DB_PASSWORD}"

Автоматическое маскирование по имени поля

Когда конфигурация выводится в логи / диагностику / сообщения об ошибках, секретные поля автоматически заменяются на [MASKED]. Список паттернов нормативно зафиксирован в config-spec/_meta/secret_patterns.yaml:

Точное совпадениеСуффикс
api_key*_secret
secret_key*_token
access_token*_password
password*_key
client_secret

Примеры маскируемых полей:

  • database.password (точное совпадение)
  • cache.auth_token (суффикс _token)
  • auth.jwt_secret (суффикс _secret)
  • payment.stripe_api_key (суффикс _key)
  • webhook.signing_password (суффикс _password)

Не маскируется:

  • database.host — не подходит ни под один паттерн.
  • database.url — URL может содержать креды (postgresql://user:pass@host), но паттерн на это не рассчитан; передавай password отдельным полем.
  • Кастомное поле internal_key_id — содержит _key, НО: совпадение по суффиксу _keyпопадает под маскирование. Если это не секрет (просто id), переименуй: internal_id / key_name.

Диагностика маскируется

Маскирование автоматически применяется внутри сообщения ConfigError.details: если загрузчик или типизированный геттер бросают ошибку на значении из секретного поля, в тексте исключения вместо сырого значения видно [MASKED]. Подключать ничего не надо.

ConfigError(
reason=type_mismatch,
path=database.password,
details=expected string, got int with value [MASKED]
)

Для собственного диагностического вывода биндинги предоставляют три примитива:

from dagstack.config.secrets_mask import (
MASKED_PLACEHOLDER,
is_secret_field,
mask_value,
)

is_secret_field("password") # True
is_secret_field("host") # False
mask_value("api_key", "sk-live-...") # "[MASKED]"
mask_value("host", "localhost") # "localhost"
print(MASKED_PLACEHOLDER) # "[MASKED]"

Используй их, когда строишь собственный дамп дерева конфигурации, логируешь конкретные поля или хочешь стабильную проверку «секретное ли это имя поля».

.local.yaml — для некоммитящихся переопределений

Если нужно временно переопределить секрет локально для отладки, используй app-config.local.yaml:

app-config.local.yaml
database:
password: "local-dev-pw" # ок — этот файл в .gitignore

Файл обязан быть в .gitignore. Стандартный шаблон .gitignore для dagstack-проектов содержит:

# dagstack config — локальные переопределения разработчика, не коммитятся.
app-config.local.yaml

Альтернатива — .env-файл с переменной, на которую ссылается ${VAR}:

.env (gitignored)
DB_PASSWORD=local-dev-pw

Публичные секрет-менеджеры (Vault / K8s Secret)

В production секреты обычно приходят из централизованного секрет-менеджера:

HashiCorp Vault через envconsul / agent:

# envconsul запускает приложение с env-переменными, взятыми из Vault
envconsul -config=vault.hcl -- python main.py

Kubernetes Secret через env:

kubernetes/deployment.yaml
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
- name: PAYMENT_API_KEY
valueFrom:
secretKeyRef:
name: payment-secrets
key: stripe-api-key

dagstack/config видит ${DB_PASSWORD} в YAML — источник переменной (env / Vault / K8s) для него прозрачен.

В Phase 2+ появятся прямые адаптеры (VaultSource, KubernetesSecretSource) — компонент сможет читать секрет из Vault без env-посредника. Но для текущего релиза env — универсальный «наименьший общий знаменатель».

Что делать, если секрет утёк

Если обнаружил, что секрет попал в git (например, app-config.yaml с plaintext-паролем или API-ключом):

  1. Немедленно отзови секрет в соответствующем сервисе (в БД смени пароль пользователю; у API-провайдера — отзови ключ).
  2. Выпусти новый секрет и обнови env-переменные в CI / production.
  3. Перепиши YAML на использование подстановки ${VAR}.
  4. Перепиши историю git (git filter-branch / BFG Repo Cleaner) — если репозиторий ещё не публичный.
  5. Если репозиторий был публичным, считай секрет скомпрометированным независимо от перезаписи истории.

Добавление собственных паттернов секретов

Список паттернов зафиксирован в v0.1 / v0.2 — он отражает config-spec/_meta/secret_patterns.yaml, и биндинги пока не дают способа расширить его на этапе загрузки. Кастомизация в рантайме (Config.load(secret_patterns=...) или эквивалент) — в плане Phase 2+.

Если нужно замаскировать поле, чьё имя не подходит под стандартный список, делай это в собственном диагностическом дампе через mask_value / maskValue / MaskValue:

# Кастомный дамп, который дополнительно маскирует проектное 'connection_string'.
def custom_masked_string(name: str, value: object) -> object:
if name == "connection_string" or is_secret_field(name):
return MASKED_PLACEHOLDER
return value

Девять стандартных паттернов уже покрывают примерно 95% случаев; по возможности переименовывай проектные секретные поля так, чтобы они подходили под стандартный суффикс (*_secret, *_token, *_password, *_key) — тогда маскирование останется автоматическим.

См. также