Секреты
dagstack/config не хранит секреты — он правильно их передаёт и маскирует в диагностике. Ответственность за хранение секретов остаётся за внешними инструментами (env-переменные, Vault, Kubernetes Secret, облачные менеджеры секретов).
:::caution Planned API
Методы маскирования (to_masked_dict / toMaskedDict / ToMaskedMap),
константа SECRET_PATTERNS и параметр secret_patterns=/WithSecretPatterns
из примеров ниже — запланированный API (Phase 2+). В текущих v0.1/v0.2
биндингах (config-python v0.2.0, config-go v0.1.0, @dagstack/config
v0.1.0) он ещё не реализован; нормативный контракт фиксируется в
config-spec/_meta/secret_patterns.yaml и ADR-поправке к §4. До реализации
выводите в логи только те ключи, которые пометили secret-ами сами.
:::
Базовое правило
Секрет — это env-переменная, попадающая в конфиг через ${VAR}. Никогда не пишите значение секрета в app-config.yaml или app-config.${ENV}.yaml:
database:
password: "s3cr3t-prod-pw"
database:
password: "${DB_PASSWORD}"
Auto-маскирование по именам полей
При выводе конфига в логи / диагностику / error-messages — секретные поля автоматически заменяются на [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отдельным полем.- Custom-поле
internal_key_id— содержит_key, НО: проверяется суффикс_key→ совпадает. Если это не секрет (просто id), переименуйте:internal_id/key_name.
Диагностика маскируется
Когда приложение логирует конфиг на старте для self-check:
- Python
- TypeScript
- Go
import json
print(json.dumps(config.to_masked_dict(), indent=2))
console.log(JSON.stringify(config.toMaskedDict(), null, 2));
masked, _ := json.MarshalIndent(cfg.ToMaskedMap(), "", " ")
fmt.Println(string(masked))
{
"database": {
"host": "localhost",
"user": "app",
"password": "[MASKED]",
"pool_size": 20
},
"payment": {
"provider": "stripe",
"stripe_api_key": "[MASKED]",
"webhook_secret": "[MASKED]"
}
}
В исключениях и ошибках маскирование применяется автоматически — ConfigError в сообщении показывает [MASKED] вместо значения.
.local.yaml — для некоммитаемых переопределений
Если нужно временно переопределить секрет локально для отладки, используйте app-config.local.yaml:
database:
password: "local-dev-pw" # ok — этот файл в .gitignore
Файл должен быть в .gitignore. Стандартная структура .gitignore в dagstack-проектах содержит:
# dagstack config — локальные переопределения разработчика, не коммитятся.
app-config.local.yaml
Альтернатива — .env файл с переменной, читаемой через ${VAR}:
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:
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 с паролем в открытом виде или с API-ключом):
- Немедленно отозвать секрет в соответствующем сервисе (в БД — сменить пароль пользователя; в API-провайдере — revoke key).
- Выпустить новый секрет, обновить env-переменные в CI / production.
- Переписать YAML на
${VAR}-подстановку. - Очистить git-историю (git filter-branch / BFG Repo Cleaner) — если репозиторий ещё не публичен.
- Если репозиторий был публичный — считайте секрет скомпрометированным независимо от очистки истории.
Добавление своих secret-паттернов
Если приложение использует кастомные поля-секреты, не покрытые стандартным списком, расширьте:
- Python
- TypeScript
- Go
from dagstack.config import Config, SECRET_PATTERNS
custom_patterns = SECRET_PATTERNS + ["connection_string", "*_pin"]
config = Config.load("app-config.yaml", secret_patterns=custom_patterns)
import { Config, SECRET_PATTERNS } from "@dagstack/config";
const customPatterns = [...SECRET_PATTERNS, "connection_string", "*_pin"];
const config = await Config.load("app-config.yaml", { secretPatterns: customPatterns });
// Defensive-copy — SecretPatterns() может возвращать shared slice
// из package-константы; append без копии мог бы мутировать backing
// array. Канонический pattern для всех "extend default list" кейсов.
base := config.SecretPatterns()
patterns := append(append([]string(nil), base...), "connection_string", "*_pin")
cfg, err := config.Load(ctx, "app-config.yaml", config.WithSecretPatterns(patterns))
Стандартные 9 паттернов покрывают 95% кейсов; кастомизация — для специфичных терминов вашего домена.
См. также
- Подстановка env-переменных — как секрет из env попадает в config.
- Слои конфигурации — почему
.local.yamlподходит для локальной отладки. - ADR-0001 — нормативные паттерны masked-полей.
- Нормативный список —
config-spec/_meta/secret_patterns.yaml.