centralized-secrets-management-vault-integration · v2.0.0 · 2026-08-22 · sha256 cef615aadf5abdcc

centralized-secrets-management-vault-integration v2.0.0A

Immutable. This exact content is served forever at /api/v1/blob/cef615aadf5abdcc.

---
name: centralized-secrets-management-vault-integration
description: Use when a trading process must obtain exchange API keys or database
  credentials from HashiCorp Vault instead of .env files — AppRole login, KV v2
  reads, explicit token TTL/max-TTL handling, a TTL-bounded in-memory cache so a
  rotated credential is actually picked up, and a redacting secret container that
  keeps keys out of logs.
domain: Infrastructure
subdomain: Security
tags:
- vault
- hashicorp
- secrets
- api-keys
- approle
- security
- kv-v2
- token-lifetime
brokers_frameworks:
- HashiCorp Vault
- HashiCorp Vault KV v2
version: "2.0.0"
author: algo-trading-skills-contributors
license: Apache-2.0
---

## 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:

1. **Login** — `POST auth/approle/login` with a RoleID and SecretID, recording the
   returned `lease_duration`, `renewable`, and `accessor`.
2. **Read** — `GET {mount}/data/{path}` (KV v2), returning a `SecretBundle` that carries
   the values plus `metadata.version` so a caller can see when a secret was rotated.
3. **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
  `environment` guard 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 grants `secret/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_ttl` and
  `invalidate()`.
- **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**. `HttpVaultTransport` refuses `http://` unless
  `allow_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 no `data`/`metadata`
  envelope 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.8+. No third-party package required; `hvac` can be substituted behind the
  `VaultTransport` protocol.

## Workflow

1. **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.
2. **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, call `logout()` after the last read and accept that
   the process cannot recover from token expiry on its own.
3. **Classify a login failure before reacting to it.** A rejected SecretID raises
   `VaultCredentialExhausted` — Vault expires a SecretID by `secret_id_ttl` and by
   `secret_id_num_uses`, so retrying cannot succeed and the orchestrator must issue a new
   one. A 429 or 5xx raises `VaultTransportError`, which *is* worth a backed-off retry.
   Never wrap login in an unbounded retry loop: a spent SecretID would spin forever.
4. **Read secrets by path.** `get_secret("prod/binance/market-maker")` returns a
   `SecretBundle`. Hand it to the exchange client with `bundle.as_dict()` — an explicit
   call, so the plaintext never appears by accident.
5. **Distinguish the three failure modes on a read.** `VaultPathViolation` is your own
   bug (wrong environment, traversal, malformed path) and never reached the network.
   `VaultSecretNotFound` means 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. `VaultPermissionDenied` means a freshly issued token was still
   refused, i.e. the policy genuinely forbids the path.
6. **Let the manager handle the token.** Each read checks the remaining TTL and, inside
   `renew_margin`, renews via `auth/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.
7. **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, call `invalidate(path)` rather than waiting out the TTL.
8. **Decide the outage policy.** With `stale_if_error=True` (default) a Vault outage lets
   the process keep trading on its last known credentials; with `False` a 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: see `references/standards.md`.
> Printable pre-flight checklist: see `assets/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 `401`s mid-session.
  `cache_ttl` bounds 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: null` and a
  `deletion_time` in the metadata.
- **Retrying a rejected login.** A spent `secret_id_num_uses` or an expired
  `secret_id_ttl` will 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. `SecretBundle` prints 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 `429` when 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/scripts`
  runs the suite. It drives the manager through `InMemoryVaultTransport`, 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 once `cache_ttl`
  expires, re-login on max TTL, permanent failure on a spent SecretID, and `repr` of both
  `SecretBundle` and `VaultSecretsManager` containing 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.

## Related Skills

- `secrets-rotation-without-bot-downtime`
- `api-key-least-privilege-audit-tool`
- `sandbox-credential-leakage-prevention`
- `hardware-security-module-hsm-for-signing-keys`
- `structured-logging-for-post-incident-forensics`