This plugin's version: v1.0.0. (Independently versioned from busbar itself — see Versioning below.)
The first-party, signed kind: secret plugin for
busbar: resolves a config secret reference
against a real HashiCorp Vault KV v2
secrets engine over its HTTP API — a genuine client (no mock), reading
one field out of a kv-v2 entry and authenticating with a pre-obtained
X-Vault-Token.
It is a cdylib that implements busbar's SecretModule trait (via
busbar-plugin-sdk)
and is loaded in-process by busbar over the signed hybrid plugin ABI —
dlopen'd, not spawned as a separate process.
This plugin is versioned independently of busbar — v1.0.0 here says
nothing about which busbar release it is. Compatibility with busbar is
stated separately: requires busbar 1.5.0+ (the release that ships the
signed hybrid plugin ABI this crate loads over). Pin both versions
explicitly in production; do not assume they move together.
Every secret value in busbar's config — a provider api_key,
auth.signing_key, the admin token, a TLS cert/key/client_ca — is a
secret reference, not a literal. The two built-in reference forms
({ env: VAR } and { file: /path }) need no plugin. This plugin adds a
third form, { module: vault, settings: {...} }, so a reference resolves
from a real Vault server through the same signed-plugin trust pipeline —
the plugin you reach for when key material must never sit in an env var
or an on-disk file.
This repo brings 100% of what it needs — it is a 2-crate Cargo workspace, not a thin adapter pointing back at busbarAI for its real logic:
hashicorp-vault/(cratebusbar-hashicorp-vault) — the real Vault KV v2 HTTP client: field addressing, response-size capping, and 404/403/5xx error classification. Usable statically, independent of the plugin ABI.hashicorp-vault-plugin/(cratebusbar-hashicorp-vault-plugin,src/lib.rs~45 lines) — the thincdylibadapter: turns the engine's JSON open-time config into a realVaultSecretModule(from the siblinghashicorp-vaultcrate, a same-repo path dependency) and hands the trait object to the SDK, which emits the extern-C symbols the loader resolves.
Auth is deliberately scoped to exactly one Vault auth method: a
pre-obtained token sent as X-Vault-Token — Vault's simplest and most
universal scheme, and the right initial surface for a first version.
AppRole/Kubernetes login flows are a natural future extension of
busbar-hashicorp-vault itself, not the thin ABI adapter.
Needs a Rust toolchain (rustup), and — interim,
until busbarAI ships publicly —
a sibling checkout of busbarAI at ../busbarAI (see
Dependencies below).
cargo build --release # workspace build; cdylib at target/release/libbusbar_hashicorp_vault_plugin.{so,dylib}
cargo test # both crates' unit tests + the end-to-end loader/Vault test (see hashicorp-vault-plugin/tests/e2e.rs)
cargo clippy --all-targets -- -D warnings
cargo fmt --all -- --checkhashicorp-vault-plugin depends on busbar-hashicorp-vault as a same-repo
path dependency (../hashicorp-vault) — the real logic lives in this repo,
not busbarAI. Only the core-engine contracts every plugin depends on the
same way — busbar-api, busbar-plugin-sdk (and, as dev-dependencies for
the end-to-end test, busbar-plugin-loader and busbar-plugin-abi) —
still reach into the busbarAI
monorepo. Because busbarAI is not yet public, Cargo.toml points at these
as local path dependencies (../../busbarAI/crates/...), which means
this repo expects to be checked out as a sibling of busbarAI:
some-parent-dir/
├── busbarAI/
└── hashicorp-vault/
├── hashicorp-vault/
└── hashicorp-vault-plugin/
This is an interim measure — once busbarAI ships publicly, these should
become git (pinned rev/tag) or crates.io dependencies instead. Grep
Cargo.toml for the INTERIM comments when doing that migration.
Once built, the cdylib is packed and signed like any other busbar plugin
— see
docs/plugins.md
in busbarAI for the full reference. In short:
BUSBAR_SIGN_KEY=<signing key> busbar-plugin-pack pack \
--lib target/release/libbusbar_hashicorp_vault_plugin.so \
--name busbar-hashicorp-vault --alias vault --kind secret \
--version 1.0.0 --publisher busbar \
--license Apache-2.0 \
--out busbar-hashicorp-vault-1.0.0-x86_64-linux.tar.gzFor local development without a signing key, busbar-plugin-pack pack --allow-unsigned produces a tarball busbar loads only under
plugins.trust.allow_unsigned: true.
Drop the resulting tarball into busbar's configured plugins.dir, add a
secrets: entry naming the module's own open-time config (the Vault
address + token), and reference it from any secret field — see
docs/plugins.md
for the full secrets: wiring reference. Example: enable the plugin and
point plugins.enabled: true at a directory containing the tarball, then
plugins:
enabled: true
dir: plugins
secrets:
vault:
settings:
addr: "https://vault.internal:8200"
token: { env: VAULT_TOKEN }
providers:
openai:
api_key: { module: vault, settings: { path: "kv/data/openai#api_key" } }secrets.<alias> is keyed by the module's alias (vault, matching the
signed manifest's alias field) and carries the module's own open-time
settings — the Vault address and auth, resolved once when the plugin is
opened. A field reference like providers.openai.api_key then names
{ module: vault, settings: { path } }, where path is the full Vault
v1 API path INCLUDING the KV v2 data/ segment (exactly what vault kv get/the Vault UI show), with the field to extract named either as a
#field suffix (as above) or a separate field key — see
Config below for both forms.
| Setting | Required | Default | Notes |
|---|---|---|---|
addr |
yes | — | The Vault server address, e.g. https://vault.internal:8200 (or http://127.0.0.1:8200 for a local dev-mode server). |
token |
yes | — | The Vault token sent as X-Vault-Token on every read. Should be delivered as a secret reference ({ env: VAULT_TOKEN }), never a plaintext literal — module-level settings cannot reference another secret plugin, only the built-in env/file modules. |
ca_cert_pem |
no | — | An additional trusted root CA (PEM), layered on top of the built-in public root store — for a self-hosted Vault behind a private CA. Never disables certificate validation. |
timeout_secs |
no | 10 |
HTTP timeout (connect + total), in seconds. |
Unknown config fields are rejected (deny_unknown_fields) — a typo'd or
stray key fails loudly at boot instead of being silently ignored.
A Vault KV v2 entry commonly holds multiple key/value pairs (e.g.
kv/data/openai might hold both api_key and org_id), so a reference
must name which field to extract, in one of two equivalent forms:
| Form | Example |
|---|---|
#field suffix on path |
{ "path": "kv/data/openai#api_key" } |
separate field key |
{ "path": "kv/data/openai", "field": "api_key" } |
If both are given, the explicit field key wins. path is used verbatim
after {addr}/v1/ — this plugin never prepends a mount or a data/
segment itself.
A 404 (no secret at that path), a 403 (bad token / missing Vault
policy), and a 5xx (Vault itself unhealthy) each surface as a distinct,
specific error — never collapsed into a generic "resolve failed", and
never an empty Ok.
cargo test (run at the workspace root) runs hashicorp-vault's own
hermetic unit tests (reference-parsing, the response-size cap, and —
gated on BUSBAR_TEST_VAULT_ADDR/BUSBAR_TEST_VAULT_TOKEN — a real
round trip), hashicorp-vault-plugin's own hermetic unit tests (covering
open()'s config-parsing responsibility: empty/malformed/
missing-required-field/unknown-field config, all without any network
I/O), and the end-to-end test in hashicorp-vault-plugin/tests/e2e.rs.
The end-to-end test is NOT a stub: it seeds a real secret directly into a
real Vault dev-mode server via a raw HTTP PUT, then dlopens the
actually-built busbar-hashicorp-vault-plugin cdylib over
busbar-plugin-loader's real kind: secret C ABI seam — the same seam
busbar's engine uses — and reads that secret back through it. It proves,
against a genuine Vault server:
- both field-addressing forms (
#fieldsuffix and explicitfieldkey) resolve the correct value across the real C ABI; - a missing path surfaces as a distinct, loud 404 — never an empty
Ok; - settings with no addressable field fail closed.
It needs a real Vault dev-mode server:
docker run --rm -p 8200:8200 --cap-add=IPC_LOCK -e VAULT_DEV_ROOT_TOKEN_ID=root hashicorp/vault
BUSBAR_TEST_VAULT_ADDR=http://127.0.0.1:8200 BUSBAR_TEST_VAULT_TOKEN=root cargo testWithout a running Vault, this test self-skips locally with a message —
but hard-fails under CI (CI env var set) instead of silently skipping;
this is the only over-the-ABI coverage of the real Vault-backed
kind: secret dlopen seam and must never quietly vanish. CI boots a real
hashicorp/vault dev-mode service container to provide it (see
.github/workflows/ci.yml).
Licensed Apache-2.0 (LICENSE). Contributions welcome — see CONTRIBUTING.md. Governed by our Code of Conduct; security issues go through SECURITY.md, not public issues.