In-process Canonical Secret URI client with PEP enforcement and Vault providers
Project description
Secrets SDK (Python)
Overview
Secrets SDK provides two client interfaces for resolving Canonical Secret URIs:
SecretsClient - Direct vault access for service configuration:
- File-based bootstrap (solves chicken-egg problem)
- Provider routing:
file://,openbao+kv2://,hashicorp+kv2:// - Memory caching with TTL
- Lazy authentication (auth on first vault access)
VaultClient - PDP-enforced access for multi-tenant operations:
- Canonical URI parsing and tenant-mount guard
- PEP grants with sender-binding, anti-replay, and negative caching
- Provider strategies for OpenBao/HashiCorp KVv2 (version-pinned reads, deleted/destroyed typing)
- Optional non-leaky audits using short
resource_ref
Design goals
- Strong typing and small public API surface
- Minimal required configuration; sensible defaults
- No plaintext secret leakage (logs/metrics/OTEL)
Non-goals
- Managing secret values at rest (creation/rotation workflows live in services that own policy)
- Exposing HTTP routes (that's CRUDService's role)
Architecture
secrets_sdk/
__init__.py # Exports: SecretsClient, SecretURI, BootstrapConfig
client.py # SecretsClient - direct vault access
bootstrap.py # BootstrapConfig, load_bootstrap()
context.py # ExecutionContext helpers
errors.py # Typed exceptions
secret_uri.py # Canonical URI parser/normalizer
audit.py # Pluggable audit publisher (Kafka/no-op)
vault_client.py # VaultClient - PDP-enforced access
grants/
grant_cache.py # In-memory grants, negative cache, anti-replay
services/
secret_policy_service.py # PDP facade
vault_strategies/
base_vault_strategy.py
openbao_vault_strategy.py
hashicorp_vault_strategy.py
Public API
# SecretsClient - service config (no PDP)
class SecretsClient:
@classmethod
def create(cls, bootstrap_path: str | None = None, ...) -> "SecretsClient": ...
async def resolve(self, uri: str, *, refresh: bool = False) -> Any: ...
async def close(self) -> None: ...
# VaultClient - request handling (PDP-enforced)
class VaultClient:
def __init__(self, *, enable_kafka: bool | None = None) -> None: ...
async def get_credentials(self, canonical_uri: str, ctx: ExecutionContext | None = None) -> dict | str: ...
def get_credentials_sync(self, canonical_uri: str, ctx: ExecutionContext | None = None) -> dict | str: ...
class ExecutionContext:
subject: str | None
aud: list[str] | None
token_jti: str | None
cnf_jkt: str | None
mtls_thumbprint: str | None
@staticmethod
def from_fastapi_request(request) -> "ExecutionContext": ...
@staticmethod
def from_headers(headers: dict[str, str]) -> "ExecutionContext": ...
VaultClient Behavior (PEP flow)
- Parse → Canonicalize → Tenant mount guard
- Build grant key
(subject, tenant_id, canonical_uri, "execute", cnf_binding) - Check negative cache; fetch/issue grant via
SecretPolicyServiceon miss - Enforce audience (
SECRETS_AUDIENCE), JTI anti-replay, sender binding drift - Increment grant uses atomically; on exceed → deny
- Provider read (KVv2 with optional
version=); map deleted/destroyed to typed errors - Publish non-leaky audit (if enabled)
Configuration
SecretsClient
Required:
OPENBAO_URLorVAULT_URLSECRETS_ALLOWED_MOUNTS(comma list)
Optional:
OPENBAO_TOKEN/VAULT_TOKEN(dev)FILE_MOUNT_PATH(default/run/secrets)SECRETS_BOOTSTRAP_PATH
VaultClient
Required:
VAULT_URL,VAULT_TOKEN,TENANT_ID,TENANT_ALLOWED_MOUNTSSECRET_TENANT_SALT(HMAC key forresource_ref)
Optional:
VAULT_TIMEOUT(default 30),VAULT_VERIFY_SSL(default true)GRANT_TTL_DEFAULT(default 300),GRANT_MAX_USES_DEFAULT(default 1)NEGATIVE_CACHE_TTL_S(default 5),ANTI_REPLAY_TTL_S(default 300)SECRETS_AUDIENCE(defaultcrud.secrets)- Audits:
ENABLE_KAFKA_PRODUCER,KAFKA_BOOTSTRAP_SERVERS,KAFKA_TOPIC_PREFIX
Dependencies
dependencies = ["tenacity", "cachetools", "httpx", "opentelemetry-api"]
Usage
SecretsClient
from secrets_sdk import SecretsClient
client = SecretsClient.create()
# Sync resolution (config loading, file:// only)
db_url = client.resolve_file_sync("file://primary/db-conn-string")
# Async resolution (runtime, all providers)
ldap = await client.resolve("file://dc-credentials/ldap.json")
token = await client.resolve("openbao+kv2://secret/app/api#token")
await client.close()
VaultClient (FastAPI)
from secrets_sdk.vault_client import VaultClient
from secrets_sdk.context import ExecutionContext
client = VaultClient()
async def handler(request):
ctx = ExecutionContext.from_fastapi_request(request)
payload = await client.get_credentials("openbao+kv2://secret/myapp/api#token?version=3", ctx)
return payload["token"]
VaultClient (Sync)
token = VaultClient().get_credentials_sync("openbao+kv2://secret/app/api#token", ctx=None)
Errors
from secrets_sdk import SecretURIError
from secrets_sdk.errors import AuthzDeniedError, PDPUnavailableError, BindingDriftError
from secrets_sdk.vault_strategies.errors import (
VaultSecretNotFoundError,
VaultSecretVersionDeletedError,
VaultSecretVersionDestroyedError,
)
Testing
Mock _read_secret_kvv2 for vault strategy tests:
with patch.object(strategy, "_read_secret_kvv2", new_callable=AsyncMock) as mock:
mock.return_value = {"data": {"data": {"token": "test"}, "metadata": {"version": 1}}}
result = await strategy.get_credentials("openbao+kv2://secret/app#token")
CI: set VAULT_SKIP_AUTH_CHECK=true to bypass auth check (tests only).
Build & Test
pip install -e .[dev]
pytest -q
See docs/ for detailed guides.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file empowernow_secrets_sdk-0.2.0.tar.gz.
File metadata
- Download URL: empowernow_secrets_sdk-0.2.0.tar.gz
- Upload date:
- Size: 18.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f367dfbcc94dd1d2fb60172a62500e4a1f44697c6564849f05eb63971611bca9
|
|
| MD5 |
8f5985b95fa49ac31978c83cff15cc6b
|
|
| BLAKE2b-256 |
b3d05e9ca1de752233a9a1bedbce0c630d0209bd0de47c834fd3a814e721f3f4
|
File details
Details for the file empowernow_secrets_sdk-0.2.0-py3-none-any.whl.
File metadata
- Download URL: empowernow_secrets_sdk-0.2.0-py3-none-any.whl
- Upload date:
- Size: 25.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
972e2a13daf6286cde93263a1ab474f1b9000a799899e89865998f1749cc7dd6
|
|
| MD5 |
46e6a5171269c18f039d9bd254e9db7c
|
|
| BLAKE2b-256 |
faa023e698738c5a2a2d651dafeb93344e51d83a4b5111cddccde989ced8f5e8
|