hvac
hvac is a synchronous Python client for the HashiCorp Vault HTTP API. Its namespaced interface covers common authentication methods, KV v1 and v2, Transit, PKI, database credentials, cloud secret engines, token operations, policies, health, Raft, and other system endpoints. Responses remain Vault-shaped dictionaries, so the client saves endpoint plumbing without hiding mounts, namespaces, token leases, secret versions, or policy decisions. It uses requests underneath and supports custom sessions and adapters.
The default Python choice when an application genuinely must speak to Vault, with broad endpoint coverage and transparent response shapes. Prefer Vault Agent or injected secrets when possible, and do not mistake the client for a token lifecycle manager.
Use it if
- A Python service needs to authenticate to Vault and read versioned KV secrets without maintaining raw endpoint paths
- You use several Vault features such as Transit encryption, dynamic database credentials, PKI, or token renewal
- Your environment needs Enterprise namespaces, private CA verification, client certificates, or a custom requests session
- Operations tooling needs typed method names for Vault system and secret-engine APIs while retaining access to raw responses
- Your application can use Vault Agent injection or a platform secret mount instead: avoiding a live Vault client removes token renewal, startup availability, and network failure from application code
- You need native async I/O: hvac is built on requests, and its documented extension points are requests.Session and synchronous adapters
- You expect one generic read_secret call to discover everything: KV v1 and KV v2 have different paths and response shapes, and custom mount points must be supplied explicitly
- You need support guarantees for an old Vault cluster: the README says testing tracks the latest release, HEAD, and three prior minor versions, while official support starts at Vault 1.4.7
- You want a high-level secret cache with automatic renewal and rotation: hvac exposes leases and renewal methods, but your process still owns caching, expiry scheduling, reauthentication, and failure policy
Setup reality
pip install hvac installs the client and requests; Python 3.8 through 3.x below 4.0 is accepted. The optional parser extra adds pyhcl only for methods that return HCL. A real deployment needs VAULT_ADDR, an authentication method, a least-privilege policy, and TLS trust. The default local-looking URL is not a production configuration. Pass verify with a CA bundle for private PKI instead of setting it to false; mutual TLS additionally needs the client certificate and key. Tokens should come from workload identity, AppRole, Kubernetes, Vault Agent, or another managed login, not a source file. KV is the first common trap: dev Vault mounts secret/ as v2, a normal server may mount nothing, and v1 versus v2 changes both the method and where data appears in the response. Custom mounts require mount_point, and Vault Enterprise users must set namespace. Authentication methods mounted under custom paths need the matching mount_point too. hvac sets the client token after most login calls when use_token remains true, but your process must still renew renewable tokens and leases or log in again. Calls are synchronous. Configure requests timeouts and a deliberate retry policy; the docs call out Vault's 412 eventual-consistency response and advise raise_on_status=False so hvac can raise its own exceptions. Do not broadly retry non-idempotent POST requests. High availability, sealed nodes, standby responses, quotas, permissions, and secret-engine configuration remain Vault operational concerns. Vault Agent is often simpler for applications because it can own authentication and renewal outside the process.
Patterns
Connect with a token and private CAconnect-with-token
import os
import hvac
client = hvac.Client(
url=os.environ["VAULT_ADDR"],
token=os.environ["VAULT_TOKEN"],
verify=os.environ["VAULT_CACERT"],
namespace=os.getenv("VAULT_NAMESPACE"),
timeout=10,
)
if not client.is_authenticated():
raise RuntimeError("Vault authentication failed")verify should point to the issuing CA bundle. Setting verify=False hides certificate mistakes and exposes secrets to interception.
Authenticate with AppRolelogin-with-approle
response = client.auth.approle.login(
role_id=os.environ["VAULT_ROLE_ID"],
secret_id=os.environ["VAULT_SECRET_ID"],
mount_point="approle",
)
lease_seconds = response["auth"]["lease_duration"]login uses the returned token on the client by default. Treat Secret IDs as credentials and use the actual auth mount path.
Authenticate from a Kubernetes podlogin-from-kubernetes
from pathlib import Path
jwt = Path(
"/var/run/secrets/kubernetes.io/serviceaccount/token"
).read_text().strip()
client.auth.kubernetes.login(
role=os.environ["VAULT_K8S_ROLE"],
jwt=jwt,
mount_point="kubernetes",
)The service account, audience, Vault role, and Kubernetes auth configuration must agree. The token file is rotated by Kubernetes, so re-read it when reauthenticating.
Read the latest KV v2 secretread-kv2-secret
response = client.secrets.kv.v2.read_secret_version(
path="apps/payments",
mount_point="secret",
)
secret = response["data"]["data"]
version = response["data"]["metadata"]["version"]KV v2 nests values under data.data and metadata beside them. KV v1 has a different method and response shape.
Update a KV v2 secret with check-and-setwrite-kv2-with-cas
current = client.secrets.kv.v2.read_secret_version(
path="apps/payments", mount_point="secret"
)
client.secrets.kv.v2.create_or_update_secret(
path="apps/payments",
secret={"api_key": new_key},
cas=current["data"]["metadata"]["version"],
mount_point="secret",
)CAS prevents overwriting a concurrent update. cas=0 succeeds only when the path has never been written.
List child keys under a KV v2 pathlist-kv2-paths
response = client.secrets.kv.v2.list_secrets(
path="apps",
mount_point="secret",
)
for key in response["data"]["keys"]:
print(key)Vault policies need list capability on the metadata path for KV v2. A trailing slash in a returned key denotes another folder-like level.
Read from a KV v1 mountread-kv1-secret
response = client.secrets.kv.v1.read_secret(
path="legacy/app",
mount_point="kv-v1",
)
password = response["data"]["password"]Do not use this shape against KV v2. Confirm the mount's options with Vault rather than guessing from its name.
Encrypt and decrypt with Transittransit-encrypt-decrypt
import base64
plaintext = base64.b64encode(b"customer-42").decode()
enc = client.secrets.transit.encrypt_data(
name="app-key", plaintext=plaintext
)
ciphertext = enc["data"]["ciphertext"]
dec = client.secrets.transit.decrypt_data(
name="app-key", ciphertext=ciphertext
)
value = base64.b64decode(dec["data"]["plaintext"])Transit plaintext and decrypted output are base64 strings. Vault retains the encryption key; it does not store your plaintext.
Request dynamic database credentialsgenerate-database-credentials
response = client.secrets.database.generate_credentials(
name="readonly",
mount_point="database",
)
username = response["data"]["username"]
password = response["data"]["password"]
lease_id = response["lease_id"]
lease_seconds = response["lease_duration"]These credentials expire with the lease. The application must reconnect or renew before expiry and must never log the returned password.
Inspect and renew the current tokenrenew-client-token
lookup = client.auth.token.lookup_self()
if lookup["data"]["renewable"]:
renewed = client.auth.token.renew_self(increment="1h")
next_ttl = renewed["auth"]["lease_duration"]Renewal can be denied by token policy or maximum TTL. Schedule from the returned TTL and handle reauthentication when renewal stops.
Retry transient and standby consistency responsesconfigure-retries
from requests import Session
from requests.adapters import HTTPAdapter
from urllib3.util import Retry
retry = Retry(
total=3, backoff_factor=0.2,
status_forcelist=[412, 500, 502, 503],
raise_on_status=False,
)
session = Session()
session.mount("https://", HTTPAdapter(max_retries=retry))
client = hvac.Client(url=vault_addr, session=session, verify=ca_file)The hvac docs require raise_on_status=False to preserve hvac exceptions. urllib3 does not retry POST by default, which protects many non-idempotent operations.
Handle permission and missing-secret errorshandle-vault-errors
import hvac
try:
response = client.secrets.kv.v2.read_secret_version(
path="apps/payments"
)
except hvac.exceptions.Forbidden:
raise RuntimeError("Vault policy denied this path")
except hvac.exceptions.InvalidPath:
response = NoneA missing path and a forbidden path are different operational states. Avoid exposing raw Vault error text in user-facing responses.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| async-hvac | PyPI | An asyncio service accepts a smaller third-party client in exchange for nonblocking Vault calls |
| vault-cli | PyPI | Humans and shell automation need a Python-installed command line rather than an application SDK |
| requests | PyPI | A tiny integration calls one or two stable Vault endpoints and can own paths, errors, and authentication itself |