Skip to main content

Crate dynamic_config_vault

Crate dynamic_config_vault 

Source
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_of names 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:

  1. 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.
  2. 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_up to zero and leave it there.
  3. 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.
BranchReports
the source reads several paths, so it cannot be watchedno — rule 3: nothing has been asked of Vault
the version check fails and may yet come good — a sealed Vault, a network blipyes, and the loop waits out the interval
the mount is not KV v2, so there is no version to pollyes, and the watch ends
the read after a version move failsyes, and seen is left where it was so the next tick tries again
the first tick, or a version that has not movedno — Vault answered
on_change refuses the documentno — 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.