Skip to main content

haven-client

The Python client for Haven, Rodmena's Vault-compatible secret manager.

pip install haven-client

It needs Python 3.12 or newer. Its only dependencies are httpx and resilient-circuit.

Five lines

from haven_client import HavenClient

with HavenClient(address="https://haven.rodmena.co.uk", token="hvn.0...") as haven:
    haven.kv.write("billing/db", {"username": "svc", "password": "s3cr3t"})
    print(haven.kv.read("billing/db").data["username"])

Configuration from the environment

HavenClient() with no arguments reads the environment. The VAULT_* names are accepted too, so existing deployment manifests keep working.

Variable Meaning
HAVEN_ADDR / VAULT_ADDR Base URL, e.g. https://haven.rodmena.co.uk
HAVEN_TOKEN / VAULT_TOKEN A token, if you already hold one
HAVEN_TOKEN_FILE A file holding the token. Read once at start
HAVEN_NAMESPACE / VAULT_NAMESPACE Tenant slug. Haven uses it to confirm a tenant, never to select one
HAVEN_CACERT / VAULT_CACERT CA bundle used to verify Haven's certificate
HAVEN_CLIENT_CERT, HAVEN_CLIENT_KEY Client certificate for mTLS (the VAULT_* names are read too)

TLS verification is always on. You can turn it off only in code, with TlsConfig(verify=False); no environment variable can switch it off, so a typo in a manifest can never quietly disable it.

Logging in

Kubernetes

The pod's service-account token is the credential. Nothing long-lived is stored in the pod.

haven = HavenClient.with_kubernetes(role="billing-api")
haven.start_lifecycle()
password = haven.kv.read("billing/db").data["password"]
  • The client reads /var/run/secrets/kubernetes.io/serviceaccount/token again at every login. Kubernetes rotates projected tokens, so a cached copy would expire.
  • The Haven role must name an audience, and the pod's projected token must carry that same audience:
volumes:
  - name: haven-token
    projected:
      sources:
        - serviceAccountToken:
            path: token
            audience: haven
            expirationSeconds: 3600
haven = HavenClient.with_kubernetes(role="billing-api", token_path="/var/run/secrets/haven/token")

AppRole

For a service that does not run on Kubernetes. Keep the secret id in a file readable only by the service user (mode 0400). The client reads it at each login and never keeps it.

haven = HavenClient.with_approle(role_id="...", secret_id_file="/etc/billing/haven-secret-id")
haven.start_lifecycle()

If the role sets secret_id_bound_cidrs, that file is useless off the hosts it was issued for.

A wrapped secret id

When the secret id is delivered response-wrapped, unwrap it once, with the wrapping token itself as the credential:

bootstrap = HavenClient(address=addr)
secret_id = bootstrap.wrapping.unwrap(wrapping_token).data["secret_id"]

Token lifecycle

start_lifecycle() logs in, then runs a background thread that:

  • renews the token at two thirds of the lease_duration Haven returned, with jitter;
  • logs in again when a renewal is refused or comes back too short (the token reached its maximum TTL);
  • records the failure when both renewal and re-login fail, in haven.last_auth_error, and calls on_auth_error. It never fails silently:
def alarm(exc: Exception) -> None:
    metrics.increment("haven_auth_failures")

haven = HavenClient.with_kubernetes(role="billing-api", on_auth_error=alarm)

Engines

haven.kv.read(path, version=None)        # KV v2 at "secret"; haven.kv_v2("other") for another mount
haven.kv.write(path, data, cas=0)        # cas=0: only if the path does not exist yet
haven.kv.patch(path, data)
haven.kv.metadata(path)
haven.kv.list(prefix)
haven.kv.delete_versions(path, [1])      # soft delete, can be undeleted
haven.kv.destroy_versions(path, [1])     # permanent
haven.kv_v1("kv").read(path)

haven.transit.encrypt("orders", b"...")
haven.transit.decrypt("orders", ciphertext)
haven.transit.sign("jws", payload, hash_algorithm="sha2-256", signature_algorithm="pkcs1v15")  # RS256
haven.transit.verify("jws", payload, signature, signature_algorithm="pkcs1v15")

haven.pki.issue("internal", "billing.internal", ttl="72h")
haven.database.credentials("reader")     # a fresh user and password, with a lease
haven.leases.renew(lease_id, increment="1h")
haven.sys.is_sealed()

Anything not covered by a helper is still reachable with haven.request(method, path, json_body=..., params=...).

Retries: what the client will and will not repeat

  • Retried: idempotent requests only (GET, LIST, HEAD and the explicitly idempotent reads such as transit.verify), on a network error, a timeout, 429, 502, 504, or a 503 for storage being unavailable. Waits use exponential backoff with jitter, and honour Retry-After up to 60 s. Retries go through resilient-circuit.
  • Never retried: writes (POST, PUT, PATCH, DELETE) are sent exactly once, because a repeated write can issue a second certificate or create a second database user. A write that fails raises, and you decide.
  • Not retried by default: a sealed Haven. A sealed Haven needs an operator to unseal it, and retrying cannot change that. Raise Sealed straight away, or opt in with RetryConfig(retry_sealed=True).

Errors

Exception When
HavenClientError Base of everything below
TransportError No HTTP answer: DNS, TLS, connect, timeout. The cause is chained
HavenApiError Haven answered with an error. Carries .status_code, .code, .errors and .request_id
BadRequest 400
InvalidCredentials 401
PermissionDenied 403
NotFound 404
Conflict (409), CasMismatch A check-and-set lost to another writer
RateLimited 429
Unavailable (503) Subclasses: Sealed, StorageUnavailable
Uninitialized 501
ServerError Other 5xx
AuthenticationFailed A login, or the lifecycle, could not obtain a token

Quote .request_id when reporting a problem: it is the same id Haven's audit log records.

Asynchronous use

from haven_client import AsyncHavenClient

async with AsyncHavenClient(address=addr, token=token) as haven:
    secret = await haven.kv.read("billing/db")

The async client runs the same code as the synchronous one in a worker thread, so their behaviour cannot drift apart.

Secrets never leak through repr

Tokens, secret ids, database passwords and private keys print as <redacted> in repr(), str() and log records. The value is still there: call .reveal() on a SecretString when you mean to use it.

Compatibility

  • Tested against Haven 0.1.0 with the server change from 2026-10 (machine-readable error code).
  • Older servers work too. Without code the client tells sealed from storage-down by the message text.
  • See CHANGELOG.md for what changed from 0.1.

Metadata

Release files for haven-client 1.0.0

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

Source distribution (sdist)

Source distribution for haven-client 1.0.0
File Size Uploaded
haven_client-1.0.0.tar.gz 24.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for haven-client 1.0.0
File Interpreter ABI Platform
haven_client-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 47.8 kB

Release files / haven_client-1.0.0.tar.gz

Download URL haven_client-1.0.0.tar.gz
Size 24.1 kB
Tags Source
SHA-256 checksum
How to use checksums
374baa7cd62a142e3066f889773dabefac5b32504bd14346cffc110584fbf59c
BLAKE2b-256 checksum
How to use checksums
f1bb1126353bb53e7c584a092f364679e8fb8de31435325c0aab8d65d986f662
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release files / haven_client-1.0.0-py3-none-any.whl

Download URL haven_client-1.0.0-py3-none-any.whl
Size 23.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5629f8be30e68bde421dbb2231ed5103a82f157cd11f24b0238ec8c69057f02c
BLAKE2b-256 checksum
How to use checksums
6c3f7c890cc2a038b157cb7edf07b443af41e419f5394a570c30d12b23d1a768
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

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