When to Use
Invoke this skill when a trading process needs live exchange or database credentials at
runtime and you want them to come from Vault rather than a .env file, a config blob,
or the process image. VaultSecretsManager covers the client half of the AppRole
workflow:
- Login —
POST auth/approle/loginwith a RoleID and SecretID, recording the returnedlease_duration,renewable, andaccessor. - Read —
GET {mount}/data/{path}(KV v2), returning aSecretBundlethat carries the values plusmetadata.versionso a caller can see when a secret was rotated. - Hold — a TTL-bounded cache so the process does not read Vault on every order, and an explicit token lifetime so the token is renewed (or the process re-authenticates) before a read fails.
The design assumption throughout is a long-lived process: a bot that boots once and runs for days. That is precisely the case where a naive client breaks — its token silently passes max TTL, or its unbounded cache keeps feeding a credential the security team revoked hours ago.
When NOT to Use
- You need Vault configured, not read. Policies, AppRole provisioning, SecretID
delivery, and audit-device setup are operator work; see
references/workflows.md. This module authenticates and reads, nothing else. - You expect the client to be the access-control boundary. It is not. The
environmentguard rejects a malformed or wrong-environment path string before it leaves the process; it cannot constrain a token whose Vault policy is too broad. If the policy grantssecret/data/*, this class will happily read whatever you ask it for inside its own environment prefix. - You are rotating a credential, not fetching one. The hot-swap, dual-credential
overlap, and revocation sequence belong to
secrets-rotation-without-bot-downtime. This skill's contribution to rotation is bounding staleness:cache_ttlandinvalidate(). - You are auditing what a key is permitted to do at the broker. That is
api-key-least-privilege-audit-tool. - The secret must never exist in process memory. Vault KV hands you plaintext. For
keys that must not leave a boundary, the operation must move to the key — see
hardware-security-module-hsm-for-signing-keys. - You need Vault's dynamic secrets or leases. This client reads static KV v2 and does not track or renew secret leases; dynamic database credentials need lease renewal and revocation logic this module does not implement.
Prerequisites
- A reachable Vault server over HTTPS.
HttpVaultTransportrefuseshttp://unlessallow_insecure_http=True, because the token and every secret would otherwise cross the network in clear text. - A KV v2 mount (
{mount}/data/{path}reads). KV v1 has nodata/metadataenvelope and this client will not parse it. - An AppRole whose policy is scoped to exactly the paths this process needs, and whose
RoleID and SecretID arrive by different channels — HashiCorp's AppRole guidance
treats delivering both together as an anti-pattern, and recommends response-wrapping
the SecretID with
secret_id_num_uses=1. - A decision, made before deployment, about what happens when the SecretID is spent: with
secret_id_num_uses=1, re-login after max TTL fails permanently and an orchestrator must deliver a fresh wrapped SecretID. - Python 3.10+. No third-party package required;
hvaccan be substituted behind theVaultTransportprotocol.
Workflow
- Construct with the environment this process owns.
VaultSecretsManager("https://vault.internal:8200", "prod", mount="secret"). The environment is a single path segment and every read must begin with it. - Log in at boot, once.
login_approle(role_id, secret_id). Both credentials are retained in memory so the process can re-authenticate unattended; if that is unacceptable in your threat model, calllogout()after the last read and accept that the process cannot recover from token expiry on its own. - Classify a login failure before reacting to it. A rejected SecretID raises
VaultCredentialExhausted— Vault expires a SecretID bysecret_id_ttland bysecret_id_num_uses, so retrying cannot succeed and the orchestrator must issue a new one. A 429 or 5xx raisesVaultTransportError, which is worth a backed-off retry. Never wrap login in an unbounded retry loop: a spent SecretID would spin forever. - Read secrets by path.
get_secret("prod/binance/market-maker")returns aSecretBundle. Hand it to the exchange client withbundle.as_dict()— an explicit call, so the plaintext never appears by accident. - Distinguish the three failure modes on a read.
VaultPathViolationis your own bug (wrong environment, traversal, malformed path) and never reached the network.VaultSecretNotFoundmeans Vault answered 404 — which means the path is absent or invisible to this policy or soft-deleted; check the policy before concluding the secret is missing.VaultPermissionDeniedmeans a freshly issued token was still refused, i.e. the policy genuinely forbids the path. - Let the manager handle the token. Each read checks the remaining TTL and, inside
renew_margin, renews viaauth/token/renew-self. When renewal stops buying headroom the token has hit its max TTL, which renewal cannot extend, so the manager re-authenticates via AppRole exactly once. A 403 on a read likewise triggers exactly one re-login before the error is raised — bounded, never a loop. - Bound staleness deliberately.
cache_ttl(default 300s) is the maximum time this process can keep using a credential that has since been rotated. On a rotation notification, callinvalidate(path)rather than waiting out the TTL. - Decide the outage policy. With
stale_if_error=True(default) a Vault outage lets the process keep trading on its last known credentials; withFalsea read raises instead. Choose consciously — the safe answer differs for a market maker holding inventory and for a batch job.
Full procedure: see
references/workflows.md. Standards reference: seereferences/standards.md. Printable pre-flight checklist: seeassets/checklist.md.
Common Pitfalls
- Treating the client's environment check as the security control. A client-side
prefix test is defence in depth against a bad path string. If the AppRole's Vault policy
is broad, nothing in this module narrows it. Scope the policy; verify it with
sys/capabilities-self. - Enforcing the prefix with
startswith("prod/")."prod/../dev/binance"passes that test. The guard here splits into segments and rejects.,.., empty segments, and anything outside a conservative character allowlist. - Reading the token's expiry as "the bot is authenticated forever". AppRole tokens carry a TTL and a max TTL, and renewal cannot extend past the max (https://developer.hashicorp.com/vault/docs/concepts/tokens). A bot that logs in at boot and never checks will take a 403 at an unpredictable moment — typically the first read after a rotation, i.e. exactly when it needs to work.
- Caching a secret with no expiry. The original failure mode this module was written
against: security rotates and revokes an exchange key, the bot holds the old value in
memory indefinitely, and the first symptom is a wall of broker
401s mid-session.cache_ttlbounds it;invalidate()short-circuits it. - Reading interpretation into a 404. Vault documents 404 as "invalid path. This can
both mean that the path truly doesn't exist or that you don't have permission to view a
specific path" (https://developer.hashicorp.com/vault/api-docs). Do not respond by
creating the secret — you may be papering over a policy gap. And a KV v2 path whose
latest version was soft-deleted also answers 404, with
data: nulland adeletion_timein the metadata. - Retrying a rejected login. A spent
secret_id_num_usesor an expiredsecret_id_ttlwill never recover on retry. Distinguish it (VaultCredentialExhausted) from a transport failure and escalate to the orchestrator instead of looping. - Shipping RoleID and SecretID together. Injecting both as environment variables from the same CI job collapses AppRole to a single shared password. HashiCorp's recommended pattern delivers the SecretID response-wrapped, single-use, and ideally CIDR-bound.
- Logging the config object.
print(exchange.config)and a traceback holding the credential dict leak just as effectively as a hardcoded key.SecretBundleprints key names only;as_dict()is the deliberate escape hatch. - Re-reading Vault on every order. Vault Community Edition supports rate-limit
quotas, which answer
429when exceeded (https://developer.hashicorp.com/vault/docs/concepts/resource-quotas). Read at boot, cache with a TTL, and refresh on rotation.
Verification
python -m unittest discover -s skills/centralized-secrets-management-vault-integration/scriptsruns the suite. It drives the manager throughInMemoryVaultTransport, a deterministic double that reproduces Vault's 404-for-invisible-paths behaviour, soft-deleted KV v2 versions, token TTL/max TTL, and single-use SecretIDs.- Regression checks worth reading before trusting a change: traversal out of the
environment (
prod/../dev/...), a rotated secret being picked up oncecache_ttlexpires, re-login on max TTL, permanent failure on a spent SecretID, andreprof bothSecretBundleandVaultSecretsManagercontaining no secret material. - Against a real Vault, confirm the policy — not the client — is the boundary: with the
bot's own token, attempt a read one environment across (
vault kv get secret/dev/...from a prod AppRole) and confirm Vault refuses it. - Verify the audit device records the read, and that the recorded request contains the path but no plaintext value.