Expand description
Read dynamic-config configuration from HashiCorp Vault.
Vault’s KV v2 store speaks plain HTTP, so this implements the blocking
RemoteSource trait: nothing here needs an async runtime, and neither
does using it.
use dynamic_config_vault::Vault;
DbConfig::set_remote(
Vault::new("https://vault.internal:8200", "secret", "myapp/db")
.with_token(std::env::var("VAULT_TOKEN")?),
);
// Fetching is explicit; the load that follows touches no network.
DbConfig::refresh_remote()?;
DbConfig::builder("db").init()?;§What it reads
GET {address}/v1/{mount}/data/{path}, and takes data.data — the value
half of a KV v2 response. That object becomes the configuration document,
so a secret stored as {"host": "db", "port": 5432} maps onto a struct
with those fields.
The document is handed over as JSON with the section key wrapped around it, because Vault stores the section’s contents rather than a whole configuration file.
§Several paths as one section
One section can be split across several secrets, and Keys says which:
// Merged in the order given — later wins — and all under the one section key.
let vault = Vault::new(
address,
"secret",
Keys::several(["myapp/db-defaults", "myapp/db-credentials"]),
);That is what Vault’s shape actually offers, and the two halves of the sentence are worth separating:
- A named list is one request per path. KV v2 reads one secret at a time — there is no batch read of a caller-chosen set — so the list is not read atomically: a write landing between two of the requests can produce a section that never existed as a whole. It is also one audited read per path per fetch, which somebody pays for.
- Every path lands under the same section key, because that is what a Vault secret is: the contents of a section, not a document. So a list is layering — a shared secret and an override, or a public half and a restricted half whose difference is a policy on the path, which is the thing Vault has that the document stores do not.
There is deliberately no prefix form. KV v2 has LIST, so the missing
piece is not the protocol; it is the mapping. Folding a whole subtree into
one section makes myapp/db and myapp/server collide on host — the
ordinary layout, refused — and naming a sub-section after each secret’s
path would invent a convention no other store here has, and would make a
list of one path mean something different from one path. A deployment that
wants several sections installs one source per section, which is what it
did before. dynamic-config-consul and dynamic-config-s3 store whole
documents, and read prefixes for that reason.
Two consequences the multi-path form shares with the rest of the family:
- Provenance becomes store-grained. The merged section is one layer, so
source_ofnames the set rather than which path supplied a value. - One unreadable path fails the whole fetch. A section quietly missing half of itself is worse than a refresh that failed and left the last document serving.
§Watching
Vault is the one store here that cannot tell you when something changed:
there is no watch, no blocking query, no stream. So Vault::watch polls —
and says so, rather than dressing a timer up as a subscription.
What it does not do is pull the secret every tick. KV v2 keeps a version
counter in its metadata, so the loop asks the metadata endpoint for
current_version and only reads the secret when that number moves. A secret
that has not changed is never transferred, never decrypted, and never
written to an audit log as a read.
A multi-path source cannot be watched, and refuses at
watch rather than pretending to: the version counter it
polls belongs to one secret, and a set of secrets has no counter of its own.
Poll refresh_remote() on a timer instead.
let watch = RemoteWatch::new();
let watching = watch.watching();
std::thread::spawn(move || {
vault.watch(&watching, Duration::from_secs(30), move |document| sink.apply(document))
});
// Dropping `watch` — or calling `watch.stop()` — ends the loop.§Every failure branch of the watch loop, and what it reports
A watch is the half of a store dynamic-config cannot see, and
reporting_to is what lets it speak: the sink the
loop already holds is told about every attempt that came back with
nothing. Which attempts those are is a table rather than prose, because
the question an operator asks is which silence is deliberate.
Three rules decide the column, and they are the same three in all seven store crates:
- A failure the loop survives by retrying reports. That is the case the whole feature exists for: the stream is down, the last delivery is old, and nothing else would ever say so out loud.
- A recovery that worked stays silent. Only a delivery or a fetch
clears the streak, so reporting a five-minute token turning over on a
healthy cluster would drive
remote_upto zero and leave it there. - A refusal that never asked the store reports nowhere. No format, a
key shape that cannot be watched, material that will not build a
client:
RemoteStatus::reachable()is whether the store answered the last time it was asked, and these never ask. They are returned to the caller, who is the one holding the mistake — and a status cannot correct them, since it carries a kind and a path and no message.
| Branch | Reports |
|---|---|
| the source reads several paths, so it cannot be watched | no — rule 3: nothing has been asked of Vault |
| the version check fails and may yet come good — a sealed Vault, a network blip | yes, and the loop waits out the interval |
| the mount is not KV v2, so there is no version to poll | yes, and the watch ends |
| the read after a version move fails | yes, and seen is left where it was so the next tick tries again |
| the first tick, or a version that has not moved | no — Vault answered |
on_change refuses the document | no — Vault answered; apply counted the delivery, and what the document did next is ConfigStatus’s half |
Re-exports§
pub use auth::Auth;
Modules§
- auth
- Getting a token, and getting another one when it stops working.
Structs§
- TlsConfig
- A private certificate authority and a client certificate, as data.
- Vault
- A secret in Vault’s KV v2 store, as a configuration source.
Enums§
- Keys
- What a source reads: one path, or several named ones.