Skip to content

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,
)
masking_secret_str.yaml
api_key: "sk-proj-abc123def456"
card_number: "not_valid_card_number"
host: "production"
Error
  | 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,
)
masking_by_name.yaml
password: "my_secret_password"
host: "production"
Error
  | 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,
)
masking_heuristic.yaml
connection_id: "aK9$mP2xL5vQ8wR3nJ7yB4zT6"
host: "production"
Error
  | 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,
)
Error
  | 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",),
)
Error
  | 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.