Masking#
dature automatically masks secret values in error messages, debug logs, and LoadReport to prevent accidental leakage of sensitive data.
Why Masking Matters#
Without masking, a validation error or debug log could expose:
Config loading errors (1)
[password] Expected str, got int
└── FILE 'config.yaml', line 2
password: my_super_secret_password
With masking enabled (default):
Config loading errors (1)
[password] Expected str, got int
└── FILE 'config.yaml', line 2
password: <REDACTED>
Detection Methods#
dature uses three methods to identify secrets:
| Method | Description | Always active |
|---|---|---|
| By type | Fields typed as SecretStr or PaymentCardNumber | Yes |
| By name | Field name contains a known pattern (case-insensitive) | Yes |
| Heuristic | String values that look like random tokens | Requires dature[secure] |
Default Name Patterns#
password, passwd, secret, token, api_key, apikey, api_secret, access_key, private_key, auth, credential
Examples#
SecretStr and PaymentCardNumber mask values in str(), repr(), and debug logs:
"""SecretStr & PaymentCardNumber — masked values in debug logs."""
from dataclasses import dataclass
from pathlib import Path
import dature
from dature.fields.payment_card import PaymentCardNumber
from dature.fields.secret_str import SecretStr
SOURCES_DIR = Path(__file__).parent / "sources"
@dataclass
class Config:
api_key: SecretStr
card_number: PaymentCardNumber
host: str
dature.load(
dature.Yaml12Source(file=SOURCES_DIR / "masking_secret_str.yaml"),
schema=Config,
)
api_key: "sk-proj-abc123def456"
card_number: "not_valid_card_number"
host: "production"
| dature.errors.exceptions.DatureConfigError: Config loading errors (1)
+-+---------------- 1 ----------------
| dature.errors.exceptions.FieldLoadError: [card_number] Card number must contain only digits
| ├── card_number: "<REDACTED>"
| │ ^^^^^^^^^^
| └── FILE '{SOURCES_DIR}masking_secret_str.yaml', line 2
+------------------------------------
Fields whose names contain known patterns are automatically masked in error messages:
"""Masking by name — secrets are masked in error messages."""
from dataclasses import dataclass
from pathlib import Path
from typing import Literal
import dature
SOURCES_DIR = Path(__file__).parent / "sources"
@dataclass
class Config:
password: Literal["admin", "root"]
host: str
dature.load(
dature.Yaml12Source(file=SOURCES_DIR / "masking_by_name.yaml"),
schema=Config,
)
| dature.errors.exceptions.DatureConfigError: Config loading errors (1)
+-+---------------- 1 ----------------
| dature.errors.exceptions.FieldLoadError: [password] Invalid variant: '<REDACTED>'
| ├── password: "<REDACTED>"
| │ ^^^^^^^^^^
| └── FILE '{SOURCES_DIR}masking_by_name.yaml', line 1
+------------------------------------
With dature[secure], values that look like random tokens are masked in error messages even if the field name is not a known secret pattern:
"""Heuristic masking — random tokens are masked in error messages."""
from dataclasses import dataclass
from pathlib import Path
from typing import Literal
import dature
SOURCES_DIR = Path(__file__).parent / "sources"
@dataclass
class Config:
connection_id: Literal["conn-1", "conn-2"]
host: str
dature.load(
dature.Yaml12Source(file=SOURCES_DIR / "masking_heuristic.yaml"),
schema=Config,
)
| dature.errors.exceptions.DatureConfigError: Config loading errors (1)
+-+---------------- 1 ----------------
| dature.errors.exceptions.FieldLoadError: [connection_id] Invalid variant: '<REDACTED>'
| ├── connection_id: "<REDACTED>"
| │ ^^^^^^^^^^
| └── FILE '{SOURCES_DIR}masking_heuristic.yaml', line 1
+------------------------------------
Mask Format#
By default, the entire value is replaced with <REDACTED>:
"my_secret_password"→"<REDACTED>""1234"→"<REDACTED>"
Configure visible_prefix / visible_suffix to keep characters visible at the start/end:
If visible_prefix + visible_suffix >= len(value), the value is shown as-is.
Classic ab*****cd style:
dature.configure(
masking={"mask": "*****", "visible_prefix": 2, "visible_suffix": 2},
)
# "my_secret_password" → "my*****rd"
# "ab" → "ab" (too short — shown as-is)
Configuration#
Per-load#
mask_secrets and secret_field_names are passed directly to dature.load(). They apply to both single-source and multi-source modes.
"""Disable masking — mask_secrets=False exposes values in errors."""
from dataclasses import dataclass
from pathlib import Path
from typing import Annotated
import dature
from dature import V
SOURCES_DIR = Path(__file__).parent / "sources"
@dataclass
class Config:
api_key: Annotated[str, V.len() >= 20]
host: str
dature.load(
dature.Yaml12Source(file=SOURCES_DIR / "masking_per_source.yaml"),
schema=Config,
mask_secrets=False,
)
| dature.errors.exceptions.DatureConfigError: Config loading errors (1)
+-+---------------- 1 ----------------
| dature.errors.exceptions.FieldLoadError: [api_key] Value length must be greater than or equal to 20
| ├── api_key: "short"
| │ ^^^^^
| └── FILE '{SOURCES_DIR}masking_per_source.yaml', line 1
+------------------------------------
In merge mode#
"""Merge mode masking — secret_field_names applied across all sources."""
from dataclasses import dataclass
from pathlib import Path
from typing import Annotated
import dature
from dature import V
SOURCES_DIR = Path(__file__).parent / "sources"
@dataclass
class Config:
host: str
port: int
api_key: Annotated[str, V.len() >= 20] = ""
dature.load(
dature.Yaml12Source(file=SOURCES_DIR / "masking_merge_mode_defaults.yaml"),
dature.Yaml12Source(file=SOURCES_DIR / "masking_merge_mode_secrets.yaml"),
schema=Config,
secret_field_names=("api_key",),
)
| dature.errors.exceptions.DatureConfigError: Config loading errors (1)
+-+---------------- 1 ----------------
| dature.errors.exceptions.FieldLoadError: [api_key] Value length must be greater than or equal to 20
| ├── api_key: "<REDACTED>"
| │ ^^^^^^^^^^
| └── FILE '{SOURCES_DIR}masking_merge_mode_secrets.yaml', line 1
+------------------------------------
Global#
See Configure for global masking defaults and all available config options.