HashiCorp Vault secret source for Hermes Agent (bulk KV v2).
Project description
hermes-vault-secret-source
A HashiCorp Vault secret source for Hermes Agent, packaged as a standalone pip plugin.
At Hermes startup it reads one Vault KV v2 secret path and injects every key found there as an environment variable, the same way the bundled Bitwarden Secrets Manager source injects a whole project. This is a bulk source, so explicit per-variable bindings from mapped sources (like 1Password) take precedence over it.
Install
pip install hermes-vault-secret-source
This pulls in hvac, the official Python client
for Vault. Install it into the same environment as hermes-agent.
To track the latest unreleased code, install from the repository instead:
pip install "hermes-vault-secret-source @ git+https://github.com/cryptoyasenka/hermes-vault-secret-source"
Hermes discovers the plugin automatically through the hermes_agent.plugins
entry point, but plugins are opt-in. Enable it once:
hermes plugins enable hermes-hashicorp-vault
Configure
Add a vault block under secrets in your Hermes config:
secrets:
vault:
enabled: true
# addr is read from $VAULT_ADDR by default; set `addr` to hard-code it.
path: apps/my-agent # KV v2 path whose keys become env vars
mount_point: secret # KV v2 mount (default: secret)
auth_method: token # "token" (default) or "approle"
cache_ttl_seconds: 300 # 0 disables caching
Credentials are never written into config; they come from environment variables:
| Auth method | Env vars used |
|---|---|
token |
VAULT_ADDR, VAULT_TOKEN |
approle |
VAULT_ADDR, VAULT_ROLE_ID, VAULT_SECRET_ID |
The env-var names are overridable per field (addr_env, token_env,
role_id_env, secret_id_env) if your deployment uses different names.
All config keys
| Key | Default | Meaning |
|---|---|---|
enabled |
false |
Master switch |
addr |
"" |
Vault URL; overrides addr_env when set |
addr_env |
VAULT_ADDR |
Env var holding the Vault URL |
path |
"" |
KV v2 path to read (required) |
mount_point |
secret |
KV v2 secrets-engine mount |
auth_method |
token |
token or approle |
token_env |
VAULT_TOKEN |
Env var with the token (token auth) |
role_id_env |
VAULT_ROLE_ID |
Env var with the role_id (approle) |
secret_id_env |
VAULT_SECRET_ID |
Env var with the secret_id (approle) |
namespace |
"" |
Vault Enterprise namespace |
verify |
true |
TLS verify: true/false or a CA-bundle path |
cache_ttl_seconds |
300 |
Disk+memory cache TTL; 0 disables caching |
request_timeout_seconds |
30 |
Per-request Vault HTTP timeout |
override_existing |
true |
Vault values overwrite existing env values |
How it behaves
- Never raises. A misconfigured or unreachable Vault produces a clean
FetchResultwith anErrorKind(not_configured,auth_failed,ref_invalid,network,timeout, ...), never a startup crash. - Protects its own credentials. The auth env vars (
VAULT_ADDR,VAULT_TOKEN,VAULT_ROLE_ID,VAULT_SECRET_ID) are marked protected, so a secret stored in Vault cannot clobber the credential used to reach Vault. - Caches values, never tokens. The optional on-disk cache stores only the resolved secret values; the token/secret_id are SHA-256 fingerprinted before they touch the cache key.
- Skips unsafe keys and values. A KV key that is not a valid environment
variable name, or one that could hijack how the process resolves binaries,
libraries, or interpreter startup (
PATH,LD_*,DYLD_*,PYTHONPATH, ...), is skipped with a warning rather than injected. Only scalar values (strings and numbers) are injected; a structured value (list or object) is skipped with a warning instead of being stringified into an unusable value.
Threat model
This plugin runs on the Hermes startup path with access to Vault credentials, so its security posture is deliberately narrow. Every protection below is already implemented; each maps to a concrete mechanism in the source.
Startup cannot be crashed by this source
fetch() never raises. Every configuration problem returns a FetchResult with
a machine-readable ErrorKind (not_configured, auth_failed, ref_invalid,
network, timeout, binary_missing, internal), and the backend in
_client.py only ever raises RuntimeError, which fetch() catches and
classifies. A misconfigured, unreachable, or unauthenticated Vault degrades to a
clean skip, never a traceback on the non-interactive startup path. Any other
unexpected error (for example a corrupt cache file surfaced by the shared cache
layer) is caught as well and reported as internal, so nothing can escape
fetch().
A Vault secret cannot hijack the credential used to reach Vault
protected_env_vars() marks the auth env vars (VAULT_ADDR, VAULT_TOKEN,
VAULT_ROLE_ID, VAULT_SECRET_ID, or their configured overrides) as protected.
The orchestrator will not overwrite a protected variable, so a key that happens
to be named VAULT_TOKEN inside the Vault payload cannot clobber the token the
process is already using to authenticate. Names that are not valid env-var
identifiers are dropped from the protected set, so a misconfigured section cannot
silently weaken it.
Tokens never reach the on-disk cache in the clear
The optional two-layer cache stores only resolved secret values. The Vault token
(or the AppRole role_id and secret_id) is SHA-256 fingerprinted by
_fingerprint() before it becomes part of the cache key, so neither the in-memory
key nor the vault_cache.json file on disk contains the raw credential. Caching
is opt-out: cache_ttl_seconds: 0 disables it entirely.
Only environment-safe keys are injected
Each KV v2 key is validated with the host's is_valid_env_name() before it is
contributed. A key that is not a valid environment-variable name is skipped with
a warning instead of being injected, so a stray Vault key cannot produce a
malformed process environment. Keys that are valid identifiers but could hijack
how the process resolves binaries, shared libraries, or interpreter startup
(PATH, LD_*, DYLD_*, PYTHONPATH, BASH_ENV, ...) are refused as well,
even when override_existing is on. Only scalar values are injected; a
structured value is skipped rather than stringified.
TLS verification is on by default
verify defaults to true. It is disabled only when the operator explicitly
sets a false-y value, and a string value is treated as a CA-bundle path. TLS
therefore fails closed rather than open.
Development
Requires a hermes-agent source checkout (for the agent.* packages and the
conformance kit at tests/secret_sources/conformance.py).
# host = hermes-agent checkout, plugin = this repo. Put the host on PYTHONPATH so
# `import tests` resolves to the host conformance package. This repo keeps its own
# tests in `checks/` (not `tests/`) precisely so it cannot shadow that package.
PYTHONPATH="/path/to/hermes-agent:$(pwd)" pytest
checks/test_conformance.py runs the host SecretSourceConformance contract;
checks/test_vault_source.py covers behavior with hvac mocked (no network, no
Vault binary needed).
To run against a real dev server:
vault server -dev # prints VAULT_ADDR + a root token
export VAULT_ADDR=http://127.0.0.1:8200
export VAULT_TOKEN=<root-token-from-output>
vault kv put secret/apps/my-agent API_KEY=abc DB_URL=postgres://x
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file hermes_vault_secret_source-0.1.1.tar.gz.
File metadata
- Download URL: hermes_vault_secret_source-0.1.1.tar.gz
- Upload date:
- Size: 14.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0ecca7201e3378d907be9f74e256f4d1872a44a0c90f956b472a79742b8e82c9
|
|
| MD5 |
f758a297b80ad7dbba5e2f18990e5113
|
|
| BLAKE2b-256 |
e9293848ddd6ca63b444e347858622e5b5cbc3ab249674095c646f8db5e99655
|
Provenance
The following attestation bundles were made for hermes_vault_secret_source-0.1.1.tar.gz:
Publisher:
release.yml on cryptoyasenka/hermes-vault-secret-source
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hermes_vault_secret_source-0.1.1.tar.gz -
Subject digest:
0ecca7201e3378d907be9f74e256f4d1872a44a0c90f956b472a79742b8e82c9 - Sigstore transparency entry: 2125766061
- Sigstore integration time:
-
Permalink:
cryptoyasenka/hermes-vault-secret-source@8222ea0b8560251c2dbd2dc605ab4a55ecff7ee3 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/cryptoyasenka
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@8222ea0b8560251c2dbd2dc605ab4a55ecff7ee3 -
Trigger Event:
push
-
Statement type:
File details
Details for the file hermes_vault_secret_source-0.1.1-py3-none-any.whl.
File metadata
- Download URL: hermes_vault_secret_source-0.1.1-py3-none-any.whl
- Upload date:
- Size: 13.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
665ea437c87b33f43e0f9a284d6ba784daa53a8d4da6b798dd49740c4f8fb535
|
|
| MD5 |
e4c4338ae2c73275ad696edbbf0b042b
|
|
| BLAKE2b-256 |
31e4e8a159da6d5132829eb298afacd038d203595ed4141cceece721ec726cf3
|
Provenance
The following attestation bundles were made for hermes_vault_secret_source-0.1.1-py3-none-any.whl:
Publisher:
release.yml on cryptoyasenka/hermes-vault-secret-source
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hermes_vault_secret_source-0.1.1-py3-none-any.whl -
Subject digest:
665ea437c87b33f43e0f9a284d6ba784daa53a8d4da6b798dd49740c4f8fb535 - Sigstore transparency entry: 2125766570
- Sigstore integration time:
-
Permalink:
cryptoyasenka/hermes-vault-secret-source@8222ea0b8560251c2dbd2dc605ab4a55ecff7ee3 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/cryptoyasenka
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@8222ea0b8560251c2dbd2dc605ab4a55ecff7ee3 -
Trigger Event:
push
-
Statement type: