The HashiCorp Vault secret backend as a droppable busbar plugin: a cdylib exporting the secret C ABI. Drop it in the plugins folder, add vault to secrets: with its addr/token settings, and reference secrets as { module: vault, settings: { path: "kv/data/name#field" } }.
| kind | alias | crate | busbar | license |
|---|---|---|---|---|
secret |
vault |
busbar-secret-vault-plugin |
1.6.0 (pinned in .busbar-ref) |
Apache-2.0 |
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 exports the secret kind's door (via
busbar-contract)
and is loaded in-process by busbar over the secret kind's memory ABI —
compiled in, or dlopen'd as a signed cdylib, never 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.6.0+ (the release that ships the
secret kind's memory ABI, v2, this crate loads over; 1.6.0 loads no 1.5.x
build of this plugin). 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 — a 2-crate Cargo workspace on the
secret kind's memory ABI (busbar_contract::abi::secret):
secret-vault/(cratebusbar-secret-vault,#![forbid(unsafe_code)]) — the sans-IO Vault KV v2 client (field addressing, the request it sends, the 1 MiB response cap, and 404/403/5xx error classification) and its door,door::door: every slot a safe SDK slot over the secret kind's table. A busbar build that compiles the plugin in links this crate and registers that door.secret-vault-plugin/(cratebusbar-secret-vault-plugin) — the thincdylibthat exports the same door asbusbar_plugin_door, so compiled in or dropped in, the kernel reaches the same table.
The plugin never opens a socket, dials or does TLS. Its one read goes
through the host's framed one-shot http exchange over its declared need
(operator-infrastructure, the target from addr, ca_cert_pem as an
extra trusted root); a read that cannot finish now answers PENDING and
the host re-invokes it on the wake. The token is the Statement's one
secret reference: the kernel resolves settings.token through the
bootstrap secret plugins and hands the material to open. Error texts
name the path and the URL, never the token or the value.
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-secret-vault itself, not the thin ABI adapter.
| 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.
Needs a Rust toolchain (rustup); rust-toolchain.toml pins
the version CI uses. busbar is a pinned git dependency (see
Dependencies below).
cargo build --release # workspace build; cdylib at target/release/libbusbar_secret_vault_plugin.{so,dylib}
cargo test # unit tests + the linked/dropped-in conformance test (secret-vault-plugin/tests/conformance.rs)
cargo clippy --all-targets -- -D warnings
cargo fmt --all -- --checkbusbar-secret-vault (secret-vault/) is a same-repo crate; secret-vault-plugin
depends on it as a normal workspace path dependency (../secret-vault).
The one busbar crate either crate
names is busbar-contract — the plugin contract, whose abi::sdk module
carries the door macros. busbar-plugin-loader is a dev-dependency only,
for the linked + dropped-in conformance test. Both are git dependencies
pinned to one busbar commit: the rev in every Cargo.toml is field 1
of .busbar-ref, and CI's pin job refuses a manifest that disagrees.
No sibling checkout of busbar is needed to build or test.
Once built, the cdylib is packed and signed like any other busbar plugin
— see
docs/plugins.md
in busbar for the full reference. In short:
BUSBAR_SIGN_KEY=<signing key> busbar-plugin-pack pack \
--lib target/release/libbusbar_secret_vault_plugin.so \
--name busbar-secret-vault --alias vault --kind secret \
--version 1.0.0 --publisher busbar \
--license Apache-2.0 \
--out busbar-secret-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.
cargo test (run at the workspace root) runs:
busbar-secret-vault's hermetic unit tests: reference parsing, the request it builds, every 1.5.5 response class and its exact text, the body cap, a pending exchange, the config refusals, and the door's lease map (READY under a lease, zeroed on release, FAILED with itsERROR_KIND_*, PENDING then resumed). One test reads a real Vault dev-mode server, gated onBUSBAR_TEST_VAULT_ADDR/BUSBAR_TEST_VAULT_TOKEN(hard-fails underCIwhen unset), through the sans-IO client with a test-side HTTP exchange;secret-vault-plugin/tests/conformance.rs: the linked door and the built cdylib, loaded through busbar's real loader, driven through the secret kind's table over one script and compared, with RED arms for a wrong kind, a Statement mismatch and a wrong config.
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 testLicensed Apache-2.0 (LICENSE). Contributions welcome — see CONTRIBUTING.md. Governed by our Code of Conduct; security issues go through SECURITY.md, not public issues.