dynamic-config-server 0.9.0

An HTTP configuration server for dynamic-config: one document per application and profile, served under per-caller authorisation.
Documentation

dynamic-config-server

An HTTP configuration server for dynamic-config: one resolved document per application and profile, handed to a caller that presented a credential scoped to that application.

Run it as a service:

cargo install dynamic-config-server
dynamic-config-server server.toml

Or mount its router in a service you already run:

[dependencies]
dynamic-config-server = "0.9.0"

What it serves

Endpoint Returns
GET /{application}/{profile} the resolved document — values, secrets included
GET /{application}/{profile}/paths which keys exist; no values
GET /{application}/{profile}/explain/{path} every layer's answer, every value ***
GET /{application}/{profile}/check would the next load succeed; key paths and origins
GET /{application}/{profile}/status generation, health, staleness
GET /metrics Prometheus text, for the sections this caller may read
GET /healthz, GET /readyz liveness and readiness. Unauthenticated, and they say nothing else

Every route is a GET. A config server that can be written to is a different product with a different threat model.

It is a security boundary

Everything else in this workspace hands configuration to the process that called it; this crate hands it over a socket. So it refuses to start rather than start permissively — no clients, a token under 32 characters, an anonymous client without an explicit opt-in, two clients sharing a token, a private key that anything but its owner can read, or a non-loopback bind that neither terminates TLS nor acknowledges a terminator in front are each a refusal naming the key that fixes it.

A credential is scoped to applications rather than to the server, and "not yours" and "no such thing" are the same 404, the same body and the same work — so a caller cannot enumerate what it may not read.

TLS and client certificates

Opt-in twice — the tls feature and a [server.tls] section — because a second TLS stack is CVE surface in the one program that holds every service's secrets, and a deployment with a terminator in front should carry none of it:

cargo install dynamic-config-server --features tls
[server.tls]
certificate = "/etc/dynamic-config/server.pem"
key = "/etc/dynamic-config/server.key"
client_ca = "/etc/dynamic-config/clients-ca.pem"   # optional

With client_ca, every caller must present a certificate that chains to it, verified during the handshake by rustls — a second, independent factor beside the bearer token. It is a gate, not an identity: the token still names the caller and still scopes what it may read, so a valid certificate with no token is a 401 and a valid certificate with the wrong grant is the same 404 as anybody else's.

The private key is the sharpest secret here, and is treated like it: no error, log, Debug or audit line carries a byte of it, and a key file readable by more than its owner refuses to start with the chmod in the message.

cargo run -p dynamic-config-server --features tls --example tls_mutual

generates a CA, a server certificate and a client certificate, runs the server over TLS, and shows all three cases end to end.

The threat model, in full, is in the book.

Reading from it

Behind the client feature, client::ConfigServer is a RemoteSource that reads one application-and-profile with a bearer token — and it subscribes: watch follows GET /{application}/{profile}/stream, re-fetching whenever the generation moves and reconnecting from the Last-Event-ID it left off at. Resumption is a comparison rather than a replay, so a reconnect cannot land past a change and miss it.

Reported to the engine as WatchCapability::Native, so a caller driving it through Remote::watch gets a push rather than a poll without arranging anything.

Both halves live in one crate so that they are tested against each other rather than against a fixture of what each believes the other returns.

Documentation

License

MIT