Skip to main content

OpenBao/Vault secrets source for pydantic-settings with AppRole authentication

Project description

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


class EmailApi(BaseModel):
    url: HttpUrl
    send_email: ApiPath
    health: ApiPath
    log: EmailStr
    settings: ApiPath

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).

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

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

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

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.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.passwordopenbao/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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

openbao_pydantic_settings_adapter-1.0.7.tar.gz (24.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

File details

Details for the file openbao_pydantic_settings_adapter-1.0.7.tar.gz.

File metadata

File hashes

Hashes for openbao_pydantic_settings_adapter-1.0.7.tar.gz
Algorithm Hash digest
SHA256 d8a7119b76057cf237362afa6d4d5142c362e846079a4003f5d400c74504bfdc
MD5 07c6b99f47ad948236763c6b5b10f0aa
BLAKE2b-256 a3356e0665784883ff7cd83da2c10619052ac88d49bc98171eca8b49479da704

See more details on using hashes here.

Provenance

The following attestation bundles were made for openbao_pydantic_settings_adapter-1.0.7.tar.gz:

Publisher: publish.yml on itstandart/openbao-pydantic-settings-adapter

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file openbao_pydantic_settings_adapter-1.0.7-py3-none-any.whl.

File metadata

File hashes

Hashes for openbao_pydantic_settings_adapter-1.0.7-py3-none-any.whl
Algorithm Hash digest
SHA256 10841245fad42e7735a80bc660af071ec4eda799ad640b8cd6867e597ed38daa
MD5 3c7c2f8f60306bdd9658fe46f387518c
BLAKE2b-256 f24346e2c14062c4d4fcfb6df9c6272ad6a9d3da6161c8f81aab1ea241234718

See more details on using hashes here.

Provenance

The following attestation bundles were made for openbao_pydantic_settings_adapter-1.0.7-py3-none-any.whl:

Publisher: publish.yml on itstandart/openbao-pydantic-settings-adapter

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page