Секреты
dagstack/config не хранит секреты — он корректно их транспортирует и маскирует в диагностике. Хранение самих секретов остаётся за внешними инструментами (env-переменные, Vault, Kubernetes Secret, облачные секрет-менеджеры).
Базовое правило
Секрет — это env-переменная, которая попадает в конфигурацию через ${VAR}. Никогда не пиши значение секрета в app-config.yaml или app-config.${ENV}.yaml:
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]
)
Для собственного диагностического вывода биндинги предоставляют три примитива:
- Python
- TypeScript
- Go
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]"
import {
MASKED_PLACEHOLDER,
isSecretField,
maskValue,
} from "@dagstack/config";
isSecretField("password"); // true
isSecretField("host"); // false
maskValue("api_key", "sk-live-..."); // "[MASKED]"
maskValue("host", "localhost"); // "localhost"
console.log(MASKED_PLACEHOLDER); // "[MASKED]"
import "go.dagstack.dev/config"
config.IsSecretField("password") // true
config.IsSecretField("host") // false
config.MaskValue("api_key", "sk-live-...") // "[MASKED]"
config.MaskValue("host", "localhost") // "localhost"
fmt.Println(config.MaskedPlaceholder) // "[MASKED]"
Используй их, когда строишь собственный дамп дерева конфигурации, логируешь конкретные поля или хочешь стабильную проверку «секретное ли это имя поля».
.local.yaml — для некоммитящихся переопределений
Если нужно временно переопределить секрет локально для отладки, используй app-config.local.yaml:
database:
password: "local-dev-pw" # ок — этот файл в .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 с plaintext-паролем или API-ключом):
- Немедленно отзови секрет в соответствующем сервисе (в БД смени пароль пользователю; у API-провайдера — отзови ключ).
- Выпусти новый секрет и обнови env-переменные в CI / production.
- Перепиши YAML на использование подстановки
${VAR}. - Перепиши историю git (git filter-branch / BFG Repo Cleaner) — если репозиторий ещё не публичный.
- Если репозиторий был публичным, считай секрет скомпрометированным независимо от перезаписи истории.
Добавление собственных паттернов секретов
Список паттернов зафиксирован в 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) — тогда маскирование останется автоматическим.
См. также
- Подстановка переменных окружения — как секрет попадает из env в конфигурацию.
- Слои конфигурации — почему
.local.yaml— правильное место для локальной отладки. - ADR-0001 — нормативные паттерны маскируемых полей.
- Нормативный список —
config-spec/_meta/secret_patterns.yaml.