openbao-pydantic-settings-adapter
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
secretsandsupersecretspaths with deep merge - SecretStr Validation - Enforces
SecretStrfor supersecrets to prevent log exposure - Graceful Degradation - Falls back to
.envwhen OpenBao is unavailable - Type Safety - Full type annotations with
py.typedmarker (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/{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 the {id} placeholder and applies all ApiPath rules to
the rest of the path. The placeholder is kept in the value, so it stays formattable:
routes = EmailApi(...)
routes.message.format(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_FILEBAO_APPROLE_SECRET_ID_FILEBAO_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:
- Caches tokens per
(namespace, role_id)combination - Renews at 75% of TTL (configurable via
BAO_TOKEN_RENEWAL_THRESHOLD) - Thread-safe for multi-threaded applications
- 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-settingsOpenBaoClient- Low-level HTTP client for OpenBao APITokenManager- Singleton for token lifecycle managementTokenEntry- Dataclass for cached token metadata (token, expires_at, ttl)ApiPath- Validated API path string for service endpoint settingsTemplateApiPath-ApiPathwith a required{id}placeholder for dynamic routes
Exceptions
OpenBaoError- Base exception for all OpenBao errorsInvalidPathError- Path not found (HTTP 404)InvalidRequestError- Invalid request/credentials (HTTP 400)ForbiddenError- Access denied (HTTP 403)InvalidResponseError- Unexpected API response structureSecurityMisconfigurationError- SecretStr validation failed
Response Models
AppRoleLoginResponse- AppRole authentication responseAuthInfo- Auth block with token, TTL, policies (part ofAppRoleLoginResponse.auth)KvV2ReadResponse- KV v2 read responseSecretDataWrapper- Inner data wrapper for KV v2 secrets (part ofKvV2ReadResponse.data)SourceInfo- Settings source diagnostic infoFieldSourceType- 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 sourcedeep_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:
-
auto_tag — on push to
main, reads__version__from__init__.pyand automatically creates a tag (if it doesn't exist) -
mirror_to_github — on tag creation, mirrors the repository to GitHub (removing
.gitlab-ci.yml)
Release flow
- Update
__version__insrc/openbao_settings/__init__.py - Push to main
- 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.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.8
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| openbao_pydantic_settings_adapter-1.0.8.tar.gz | 25.3 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| openbao_pydantic_settings_adapter-1.0.8-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 52.8 kB
Release files / openbao_pydantic_settings_adapter-1.0.8.tar.gz
| Download URL | openbao_pydantic_settings_adapter-1.0.8.tar.gz |
|---|---|
| Size | 25.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b3e1f20af2466f5f60cc34ef19408a64a45ad45ae734408a9517e6c6eea51a26
|
|
BLAKE2b-256 checksum How to use checksums |
8d492b10de24737ec125848c3917347c09016339e4cb64af8af719bd92e5b106
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Aug 4, 2026.
Transparency logRelease files / openbao_pydantic_settings_adapter-1.0.8-py3-none-any.whl
| Download URL | openbao_pydantic_settings_adapter-1.0.8-py3-none-any.whl |
|---|---|
| Size | 27.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
044dcf9c36dedd2d1d05aaf2b13759d432b6d4696478eabb30421c7176d8494b
|
|
BLAKE2b-256 checksum How to use checksums |
5478de80a5e595e3200adbf56ddfbd46c11e6a5ae5c2dffd9f48c8890282b0a1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Aug 4, 2026.
Transparency logSecurity for openbao-pydantic-settings-adapter 1.0.8
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.
Attestation for openbao_pydantic_settings_adapter-1.0.8.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 Aug 4, 2026.
Transparency logA 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.