Skip to main content

openbao-pydantic-settings-adapter

PyPI version Python 3.11+ License: PolyForm Noncommercial

OpenBao/Vault secrets source for pydantic-settings with AppRole authentication, automatic token renewal, and graceful fallback to environment variables.

Features

  • AppRole Authentication - Secure machine-to-machine authentication
  • Automatic Token Renewal - TokenManager handles TTL-based token lifecycle
  • Namespace Support - Multi-tenant isolation (team/project level)
  • Two-Path Architecture - Separate secrets and supersecrets paths with deep merge
  • SecretStr Validation - Enforces SecretStr for supersecrets to prevent log exposure
  • Graceful Degradation - Falls back to .env when OpenBao is unavailable
  • Type Safety - Full type annotations with py.typed marker (PEP 561)

Installation

pip install openbao-pydantic-settings-adapter

Note: The import name is openbao_settings:

from openbao_settings import OpenBaoSettingsSource

Quick Start

from pydantic import SecretStr
from pydantic_settings import BaseSettings, PydanticBaseSettingsSource

from openbao_settings import OpenBaoSettingsSource


class Settings(BaseSettings):
    database_url: str
    api_key: SecretStr  # Fields from supersecrets MUST use SecretStr

    @classmethod
    def settings_customise_sources(
        cls,
        settings_cls: type[BaseSettings],
        init_settings: PydanticBaseSettingsSource,
        env_settings: PydanticBaseSettingsSource,
        dotenv_settings: PydanticBaseSettingsSource,
        file_secret_settings: PydanticBaseSettingsSource,
    ) -> tuple[PydanticBaseSettingsSource, ...]:
        return (
            init_settings,
            OpenBaoSettingsSource(settings_cls),  # OpenBao has priority
            env_settings,
            dotenv_settings,
        )


settings = Settings()

Validated API Paths

Use ApiPath for API endpoint settings stored in OpenBao or environment variables:

from pydantic import BaseModel, EmailStr, HttpUrl

from openbao_settings import ApiPath, TemplateApiPath


class EmailApi(BaseModel):
    url: HttpUrl
    send_email: ApiPath
    health: ApiPath
    log: EmailStr
    settings: ApiPath
    message: TemplateApiPath  # e.g. "messages/{message_id}/"

ApiPath is a strict string type: it requires a trailing slash, forbids a leading one, rejects double slashes, and only allows characters from [a-zA-Z0-9/_-]. The value must already be normalized (e.g. api/health/, not /api/health or api/health).

TemplateApiPath covers routes with a dynamic entity identifier. Since ApiPath rejects { and }, this subtype requires at least one {name} placeholder and applies all ApiPath rules to the rest of the path. Placeholder names use Starlette's path-parameter charset ([a-zA-Z_][a-zA-Z0-9_]*), so {bucket}, {task_id} and {mailId} are all valid. The value is a str.format template for outgoing calls, not a route to register, so a name may repeat (a/{id}/{id}/) even though Starlette rejects that. Placeholders are kept in the value, so it stays formattable:

routes = EmailApi(...)
routes.message.format(message_id=42)  # "messages/42/"

Configuration

Configure via environment variables:

Variable Description Required
BAO_ADDR OpenBao server URL (e.g., http://localhost:8200) Yes
BAO_SECRET_PATH Base path to secrets (e.g., myapp) Yes
BAO_APPROLE_ROLE_ID AppRole Role ID Yes
BAO_APPROLE_SECRET_ID AppRole Secret ID Yes
BAO_NAMESPACE OpenBao namespace for multi-tenant isolation No
BAO_MOUNT_POINT KV engine mount point (default: kv) No
BAO_TIMEOUT Connection timeout in seconds (default: 30) No
BAO_TOKEN_RENEWAL_THRESHOLD When to renew token (default: 0.75 = at 75% of TTL) No

For Docker/Kubernetes, you can use file-based credentials:

  • BAO_APPROLE_ROLE_ID_FILE
  • BAO_APPROLE_SECRET_ID_FILE
  • BAO_NAMESPACE_FILE

Secrets Architecture

The library reads from two paths and merges them:

kv/{BAO_SECRET_PATH}/secrets      - Regular secrets (developers can view/edit)
kv/{BAO_SECRET_PATH}/supersecrets - Sensitive secrets (admin only)

Important: Fields loaded from supersecrets MUST use SecretStr type annotation. The library validates this at runtime and raises SecurityMisconfigurationError if violated.

# Wrong - will raise SecurityMisconfigurationError
class Settings(BaseSettings):
    api_key: str  # Loaded from supersecrets but not SecretStr!

# Correct
class Settings(BaseSettings):
    api_key: SecretStr  # Properly protected

Token Lifecycle

TokenManager automatically handles token renewal:

  1. Caches tokens per (namespace, role_id) combination
  2. Renews at 75% of TTL (configurable via BAO_TOKEN_RENEWAL_THRESHOLD)
  3. Thread-safe for multi-threaded applications
  4. Graceful degradation if OpenBao becomes unavailable
from openbao_settings import TokenManager

# Manual token invalidation (e.g., for credential rotation)
TokenManager().invalidate()

# Check token health
TokenManager().is_healthy(namespace="myapp", role_id="...")

Diagnostics

Track where settings were loaded from:

from openbao_settings import get_last_source_info, SourceInfo

# Basic info (only OpenBao fields in field_sources)
info: SourceInfo = get_last_source_info()
print(info.status)              # True if loaded from OpenBao
print(info.source)              # "openbao" or "env"
print(info.details)             # Human-readable description
print(info.openbao_keys_loaded) # Number of keys from OpenBao
print(info.timestamp)           # When the check was performed

# Full field tracking (pass settings instance)
settings = Settings()
info = get_last_source_info(settings)
print(info.field_sources)
# {
#   "db.host": "openbao/secrets",
#   "db.port": "openbao/secrets",
#   "db.password": "openbao/supersecrets",
#   "services.otlp.domain": "openbao/secrets",
#   "services.otlp.password": "openbao/supersecrets",
#   "log_level": "env",
#   "debug": "default"
# }

Field source values (FieldSourceType) with dot notation for nested fields:

  • "openbao/secrets" — loaded from secrets/ path (e.g., "db.host")
  • "openbao/supersecrets" — loaded from supersecrets/ path (e.g., "db.password")
  • "openbao/merged" — same leaf field in BOTH paths (supersecrets wins)
  • "env" — loaded from environment variables or .env file
  • "default" — uses field's default value

info.unmapped_keys lists OpenBao keys with no matching model field, in the same {path: source} format (e.g. a typo "smtp.providers.backup.logni": "openbao/secrets"). Nested models, field aliases, list[Model], dict[str, Any] and dict[str, Model] are understood: for dict[str, Model] any key (provider id) is valid and each value is checked against Model.

API Reference

Classes

  • OpenBaoSettingsSource - Main settings source for pydantic-settings
  • OpenBaoClient - Low-level HTTP client for OpenBao API
  • TokenManager - Singleton for token lifecycle management
  • TokenEntry - Dataclass for cached token metadata (token, expires_at, ttl)
  • ApiPath - Validated API path string for service endpoint settings
  • TemplateApiPath - ApiPath with at least one required {name} placeholder for dynamic routes

Exceptions

  • OpenBaoError - Base exception for all OpenBao errors
  • InvalidPathError - Path not found (HTTP 404)
  • InvalidRequestError - Invalid request/credentials (HTTP 400)
  • ForbiddenError - Access denied (HTTP 403)
  • InvalidResponseError - Unexpected API response structure
  • SecurityMisconfigurationError - SecretStr validation failed

Response Models

  • AppRoleLoginResponse - AppRole authentication response
  • AuthInfo - Auth block with token, TTL, policies (part of AppRoleLoginResponse.auth)
  • KvV2ReadResponse - KV v2 read response
  • SecretDataWrapper - Inner data wrapper for KV v2 secrets (part of KvV2ReadResponse.data)
  • SourceInfo - Settings source diagnostic info
  • FieldSourceType - Type alias for field source values (Literal["openbao/secrets", "openbao/supersecrets", "openbao/merged", "env", "default"])

Utilities

  • get_last_source_info(settings=None) - Get info about last settings load source
  • deep_merge(base, override) - Recursively merge dictionaries (override wins)

Development

Syncing with Remote

# Fetch commits and tags
git pull origin main --tags

# If local branch is behind remote
git reset --hard origin/main

Verify sync status

git log --oneline origin/main -5
git diff origin/main

Checks

uv run pytest        # unit tests, no OpenBao needed
uv run ruff check .
uv run pyright

CI/CD

The pipeline consists of two stages:

  1. auto_tag — on push to main, reads __version__ from __init__.py and automatically creates a tag (if it doesn't exist)

  2. mirror_to_github — on tag creation, mirrors the repository to GitHub (removing .gitlab-ci.yml)

Release flow

  1. Update __version__ in src/openbao_settings/__init__.py
  2. Push to main
  3. CI creates tag → mirrors to GitHub → publishes to PyPI

Versioning

This project follows Semantic Versioning (MAJOR.MINOR.PATCH):

  • PATCH — bug fixes, documentation, metadata
  • MINOR — new features (backwards compatible)
  • MAJOR — breaking changes

Changelog

1.0.10

Fixed false unmapped_keys for dict[str, Model] fields (e.g. smtp.providers.backup.login): any key is accepted as an item id and each value is checked against Model, so a real typo is still reported with its full path (smtp.providers.backup.logni). Unmapped keys are now found by walking the data alongside the model, which also removes false positives for list[Model] fields and for nested models behind a field alias. Diagnostics only: loading, validation and the supersecrets SecretStr checks are unchanged. First regression tests added (uv run pytest).

1.0.9

TemplateApiPath accepts any named placeholder instead of the hardcoded {id}. A value must carry at least one {name} placeholder; names use Starlette's path-parameter charset ([a-zA-Z_][a-zA-Z0-9_]*), so {bucket}, {task_id} and {mailId} all work and a path may carry several of them. Strictly more permissive: every value valid in 1.0.8 stays valid.

1.0.8

TemplateApiPath — ApiPath subtype for routes with a dynamic entity identifier. Requires the {id} placeholder (which ApiPath itself rejects, since { and } are not allowed characters) and applies all other ApiPath rules to the substituted path. The placeholder is preserved in the value, so path.format(id=42) yields a valid ApiPath.

1.0.7

ApiPath — validated str subtype for service endpoint settings stored in OpenBao or env. Requires a trailing slash, forbids a leading one, rejects //, and only allows characters from [a-zA-Z0-9/_-]. Use as a Pydantic field type (e.g. health: ApiPath) to surface misconfigured paths as validation errors at startup instead of silently letting them through.

1.0.6

unmapped_keys field added to SourceInfo — detects OpenBao keys (after merge of secrets and supersecrets) that have no corresponding field in the Pydantic Settings model. Logged as a warning at startup and exposed via get_last_source_info(settings). Supports nested models, field aliases, and dict[str, Any] fields. Uses the same {path: source} format as field_sources.

1.0.5

SourceInfo model for diagnostics — standardized format for reporting where settings came from, with per-field tracking using dot notation (e.g. db.password → openbao/supersecrets). FieldSourceType literal — "openbao/secrets" | "openbao/supersecrets" | "openbao/merged" | "env" | "default". get_last_source_info(settings=None) — when called with a settings instance, returns full per-field source tracking including env and default values; without an instance, returns only OpenBao fields. README API Reference expanded to cover all exported symbols.

1.0.4

Initial SourceInfo model (extended in 1.0.5 with field_sources tracking).

1.0.3

Minimum supported Python lowered from 3.13 to 3.11. requires-python in pyproject.toml changed to >=3.11, Python 3.11 and 3.12 added to the trove classifiers. Note: the corresponding release commit is mistakenly titled Release 1.02, but the published PyPI version is 1.0.3.

1.0.2

Maintenance release: __version__ bump only, no API-visible changes.

1.0.1

Initial public release on PyPI. Provides OpenBaoSettingsSource for pydantic-settings with AppRole authentication, automatic token renewal via TokenManager, namespace support, two-path architecture (secrets / supersecrets) with deep merge, SecretStr enforcement for supersecrets, and graceful .env fallback when OpenBao is unavailable.

License

This project is licensed under the PolyForm Noncommercial License 1.0.0.

You may use this software for noncommercial purposes only.

See LICENSE for the full license text, or visit polyformproject.org.

Metadata

Release files for openbao-pydantic-settings-adapter 1.0.10

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for openbao-pydantic-settings-adapter 1.0.10
File Size Uploaded Provenance
openbao_pydantic_settings_adapter-1.0.10.tar.gz 72.8 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for openbao-pydantic-settings-adapter 1.0.10
File Interpreter ABI Platform
openbao_pydantic_settings_adapter-1.0.10-py3-none-any.whl Python 3 none any Details

Total release size: 101.2 kB

Release files / openbao_pydantic_settings_adapter-1.0.10.tar.gz

Download URL openbao_pydantic_settings_adapter-1.0.10.tar.gz
Size 72.8 kB
Tags Source
SHA-256 checksum
How to use checksums
6b53da7ecfb7b21c48bdd447a1d5f26687c9ef34df4fcac4bfa83e7586055a4b
BLAKE2b-256 checksum
How to use checksums
6e5f25d123e939c60a2701d87b6cbd0a95a8cbeaeb1b93e97d86feff5dfe0aaf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.

Transparency log

Release files / openbao_pydantic_settings_adapter-1.0.10-py3-none-any.whl

Download URL openbao_pydantic_settings_adapter-1.0.10-py3-none-any.whl
Size 28.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
16907bd022a293bec144f5aee40a9c81703bc0bef809fa29db69db9b0a00fe2a
BLAKE2b-256 checksum
How to use checksums
97040363ab7c42db716626c566ec1ccfa9ac13d8e41e45a17533de2de00f116a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.

Transparency log

Security for openbao-pydantic-settings-adapter 1.0.10

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for openbao_pydantic_settings_adapter-1.0.10.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page