hvac review
hvac is a synchronous Python interface to HashiCorp Vault. Its client exposes authentication backends, KV secrets, Transit encryption, PKI, dynamic database credentials, identity, policies, response wrapping, Raft administration, and lower-level read/write calls. The dictionaries it returns closely follow Vault's HTTP responses, including lease and token metadata. Version 2.4.0 adds `audience` to Kubernetes role creation for Vault 1.21 and later. It is a good fit when Python code must operate Vault directly, but it does not replace Vault Agent or manage credential renewal for you.
hvac 2.4.0 installed in 0.7 seconds, occupied 4 MB across 6 packages, and showed 0 audit findings in our Python 3.12 sandbox. Use it when a synchronous Python service genuinely owns Vault API calls; choose Agent or platform injection when the process only needs delivered secrets.
We installed it
| Install | ✓ · 0.7s | 6 packages on disk · 4 MB |
| Import | ✓ | import hvac in 0.41s · pure Python · requires Python >=3.8,<4.0 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does hvac install cleanly?
Yes. In a fresh container with an empty cache, pip install hvac finished in 0.7s, leaving 6 packages and 4 MB on disk. pip-audit reported no known vulnerabilities.
What does hvac need to run?
Python >=3.8,<4.0, and nothing compiled: it is pure Python. In our run import hvac succeeded in 0.41s.
hvac or requests: which should you use?
requests: Use direct HTTP for one stable Vault route when owning URLs, headers, errors, and response parsing is acceptable. hvac 2.4.0 installed in 0.7 seconds, occupied 4 MB across 6 packages, and showed 0 audit findings in our Python 3.12 sandbox.
When should you not use hvac?
Vault Agent, a CSI mount, or platform injection can place the needed secret beside the process. Those options remove login and renewal code from the application.
Use it if
- A synchronous Python worker needs named methods across several Vault auth, secret-engine, and system endpoints.
- You need KV v2 version metadata, check-and-set writes, Transit operations, leased credentials, or response wrapping.
- The client must use an Enterprise namespace, private CA, client certificate, proxy, or custom requests session.
- Application code should see Vault's lease fields and typed hvac exceptions instead of a secret cache hiding them.
- Vault Agent, a CSI mount, or platform injection can place the needed secret beside the process. Those options remove login and renewal code from the application.
- The service is built around asyncio. hvac uses requests, so a Vault call occupies its thread until the response arrives.
- Strict dependency typing is mandatory. The 2.4.0 wheel has no `py.typed` marker, which means type checkers cannot treat the installed package as a typed distribution.
- You expect one KV call to work against either engine version. KV v1 and v2 have different routes, methods, policy paths, and response shapes.
- The SDK must cache secrets and renew every token or lease on its own. hvac exposes the operations, while your process still owns the schedule and recovery policy.
Setup reality
We installed hvac 2.4.0 in an empty Python 3.12 Bookworm sandbox. The operation took 0.7 seconds and produced 6 installed packages using 4 MB. hvac declares 2 direct dependencies, is pure Python, and accepts Python 3.8 through any release below 4.0. import hvac succeeded in 0.41 seconds. pip-audit returned 0 known vulnerabilities. The distribution has no py.typed marker and uses the Apache-2.0 license.
Before the first request, supply a Vault URL, trusted CA material, and an authentication method. Set verify to the internal CA file rather than turning certificate checks off. Mutual TLS also needs the client certificate and private key. Kubernetes auth, workload identity, AppRole, or an Agent-delivered token is preferable to a long-lived privileged token in source or a config file. Vault Enterprise requests need the namespace value as well.
KV version mismatch causes many first failures. A dev server commonly exposes KV v2 at secret/, but production can use another mount or KV v1. The v2 payload is nested at response['data']['data'], and policies refer to versioned API paths. Pass mount_point whenever the mount is customized. Version 2.4.0 still defaults raise_on_deleted_version to true; set it explicitly because the changelog says version 3 will flip that default.
Every call goes through requests and blocks. Configure a timeout. For urllib3 Retry, the hvac docs require raise_on_status=False so hvac gets the response and maps it to the right exception, including Vault's 412 status. Retrying writes can duplicate work. Login normally updates the client token, but renewable tokens and dynamic leases still need a timer, an expiry path, and fresh authentication when renewal stops.
Patterns
Connect through a private CA create-tls-client
import os
import hvac
client = hvac.Client(
url=os.environ['VAULT_ADDR'],
token=os.environ.get('VAULT_TOKEN'),
verify=os.environ['VAULT_CACERT'],
namespace=os.environ.get('VAULT_NAMESPACE'),
timeout=10,
)
if not client.is_authenticated():
raise RuntimeError('Vault login is not valid')A CA bundle keeps server verification active. Setting verify=False can expose both the token and returned secret data.
Authenticate with AppRole login-with-approle
result = client.auth.approle.login(
role_id=role_id,
secret_id=secret_id,
mount_point='approle',
)
lease_seconds = result['auth']['lease_duration']login stores the issued token on this client unless token use is disabled. Match mount_point to the server's auth mount.
Set the Kubernetes token audience define-kubernetes-role
client.auth.kubernetes.create_role(
name='orders',
bound_service_account_names=['orders-api'],
bound_service_account_namespaces=['production'],
policies=['orders-read'],
audience='vault',
)The audience argument arrived in hvac 2.4.0 and is required by Vault 1.21 or newer for this role flow.
Use a projected service-account token login-from-pod
from pathlib import Path
token_path = Path('/var/run/secrets/kubernetes.io/serviceaccount/token')
jwt = token_path.read_text().strip()
client.auth.kubernetes.login(role='orders', jwt=jwt)Projected Kubernetes tokens rotate, so read the file again before a later login instead of retaining the first contents forever.
Read data and metadata from KV v2 read-kv2
result = client.secrets.kv.v2.read_secret_version(
path='services/orders',
mount_point='secret',
raise_on_deleted_version=False,
)
value = result['data']['data']['api_key']
version = result['data']['metadata']['version']KV v2 wraps values inside two data keys. State deleted-version behavior now because its default is scheduled to change in hvac 3.
Reject a stale KV v2 update write-kv2-cas
client.secrets.kv.v2.create_or_update_secret(
path='services/orders',
secret={'api_key': replacement},
cas=version,
mount_point='secret',
)cas must equal the current version. A value of 0 only succeeds for a path that has never been written.
Read from a KV v1 mount read-kv1
result = client.secrets.kv.v1.read_secret(
path='services/orders',
mount_point='legacy',
)
value = result['data']['api_key']KV v1 has one data layer and no version metadata. Do not send this call to a KV v2 mount.
Encrypt a value with Transit encrypt-with-transit
import base64
encoded = base64.b64encode(b'customer-42').decode('ascii')
result = client.secrets.transit.encrypt_data(
name='orders-key',
plaintext=encoded,
)
ciphertext = result['data']['ciphertext']Transit accepts base64 plaintext and returns a Vault ciphertext; the engine does not retain the plaintext value.
Create a single-use handoff token wrap-handoff
wrapped = client.sys.wrap(
payload={'bootstrap_token': bootstrap_token},
ttl='60s',
)
wrapping_token = wrapped['wrap_info']['token']Only send the wrapping token across the handoff channel, and consume it before the 60-second TTL expires.
Consume a wrapped payload unwrap-handoff
result = client.sys.unwrap(token=wrapping_token)
bootstrap_token = result['data']['bootstrap_token']A wrapping token works once. A failed second unwrap can mean the value was already consumed.
Renew the client's token renew-own-token
details = client.auth.token.lookup_self()['data']
if details['renewable']:
result = client.auth.token.renew_self(increment='1h')
ttl = result['auth']['lease_duration']Renewal cannot exceed the token's maximum lifetime. Code still needs a route back to the original login method.
Retry selected response codes configure-vault-retries
from requests import Session
from requests.adapters import HTTPAdapter
from urllib3.util import Retry
policy = 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=policy))
client = hvac.Client(url=vault_addr, verify=ca_file, session=session)hvac needs raise_on_status=False to translate final HTTP responses into its exceptions. Review each write before allowing automatic retries.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| requests | PyPI | Use direct HTTP for one stable Vault route when owning URLs, headers, errors, and response parsing is acceptable. |
| vault-cli | PyPI | Use it for shell-oriented operator work rather than a client embedded in an application. |
| ansible | PyPI | Use its Vault-related collections when secrets are part of provisioning or deployment playbooks instead of runtime Python code. |
More security guides
cryptography · pyjwt · jose · requests-oauthlib · oauthlib · dompurify · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

