Skip to main content

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)

  1. Parse → Canonicalize → Tenant mount guard
  2. Build grant key (subject, tenant_id, canonical_uri, "execute", cnf_binding)
  3. Check negative cache; fetch/issue grant via SecretPolicyService on miss
  4. Enforce audience (SECRETS_AUDIENCE), JTI anti-replay, sender binding drift
  5. Increment grant uses atomically; on exceed → deny
  6. Provider read (KVv2 with optional version=); map deleted/destroyed to typed errors
  7. Publish non-leaky audit (if enabled)

Configuration

SecretsClient

Required:

  • OPENBAO_URL or VAULT_URL
  • SECRETS_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_MOUNTS
  • SECRET_TENANT_SALT (HMAC key for resource_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 (default crud.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


Download files

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

Source Distribution

empowernow_secrets_sdk-0.2.0.tar.gz (18.8 kB view details)

Uploaded Source

Built Distribution

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

empowernow_secrets_sdk-0.2.0-py3-none-any.whl (25.7 kB view details)

Uploaded Python 3

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

Hashes for empowernow_secrets_sdk-0.2.0.tar.gz
Algorithm Hash digest
SHA256 f367dfbcc94dd1d2fb60172a62500e4a1f44697c6564849f05eb63971611bca9
MD5 8f5985b95fa49ac31978c83cff15cc6b
BLAKE2b-256 b3d05e9ca1de752233a9a1bedbce0c630d0209bd0de47c834fd3a814e721f3f4

See more details on using hashes here.

File details

Details for the file empowernow_secrets_sdk-0.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for empowernow_secrets_sdk-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 972e2a13daf6286cde93263a1ab474f1b9000a799899e89865998f1749cc7dd6
MD5 46e6a5171269c18f039d9bd254e9db7c
BLAKE2b-256 faa023e698738c5a2a2d651dafeb93344e51d83a4b5111cddccde989ced8f5e8

See more details on using hashes here.

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