Skip to main content

dynamic_config_server/config/
mod.rs

1//! The server's own configuration, and every reason it refuses to start.
2//!
3//! A config server is the one program whose misconfiguration is not its own
4//! problem: it hands other services their secrets. So the checks here are
5//! refusals rather than warnings, and each one names the key that would fix
6//! it. The list is deliberately long and deliberately loud — the failure
7//! mode this exists to prevent is a server that starts, looks healthy, and
8//! is serving `billing` to anyone who asks.
9//!
10//! Three files, because the module answers three questions: **what a
11//! configuration is** (here), **why one is refused**
12//! ([`refusal`](refusal)), and **which refusal applies**
13//! ([`validate`](validate)). The split is a file boundary and nothing more
14//! — every type below is re-exported from `crate` exactly where it was.
15
16mod refusal;
17mod validate;
18
19pub use refusal::Refusal;
20
21use serde::Deserialize;
22
23use crate::auth::Token;
24
25/// The default bind address: loopback, so a server started with no `bind`
26/// at all is reachable from nowhere but its own host.
27fn default_bind() -> String {
28    "127.0.0.1:8080".to_owned()
29}
30
31/// The default debounce for the file watcher, in milliseconds.
32fn default_debounce_ms() -> u64 {
33    250
34}
35
36/// The default ceiling on concurrent change-stream connections.
37///
38/// A thousand-pod fleet reconnecting at once is the shape this number is
39/// chosen against: each connection costs one `Changes` handle and one
40/// registered waker and holds no document, so a thousand is nothing — and a
41/// ceiling that a fleet does not reach in normal operation is a backstop
42/// against a client that reconnects in a loop rather than a rate limit.
43fn default_max_streams() -> usize {
44    4096
45}
46
47/// One served application-and-profile pair.
48///
49/// The section key inside the files **is** the application name: a document
50/// served as `billing` is the `[billing]` table of the configured files.
51/// That is one fact rather than two, and it keeps a URL and a file readable
52/// against each other.
53#[derive(Debug, Clone, Deserialize)]
54#[serde(deny_unknown_fields)]
55pub struct SectionConfig {
56    /// The application, which is both the first path segment and the
57    /// section key inside the files.
58    pub application: String,
59    /// The profile, which is the second path segment.
60    ///
61    /// A profile here is a *different set of files*, chosen by the
62    /// operator, rather than the library's `profile_env` — that one is a
63    /// process-wide environment variable, and a server serving two profiles
64    /// cannot have two of those at once.
65    pub profile: String,
66    /// The files to merge, in order; later files win.
67    pub files: Vec<String>,
68    /// An environment-variable prefix layered above the files, as in
69    /// `APP_` reading `APP_BILLING_*`.
70    #[serde(default)]
71    pub env_prefix: Option<String>,
72    /// Whether these files carry a section header at all.
73    ///
74    /// `false` — the default — reads the application as a top-level key
75    /// inside each file, so one file can hold several applications.
76    ///
77    /// `true` says each file *is* this section: `{"host": …, "port": …}`
78    /// with nothing above it. A config server is routinely pointed at
79    /// files somebody else's tool writes, and those files have no reason
80    /// to carry a header this server invented.
81    #[serde(default)]
82    pub whole_document: bool,
83}
84
85/// Where the server's own certificate, key and client CA live.
86///
87/// Its presence is what turns TLS on; there is no `enabled` key, because a
88/// block that names a certificate and does nothing is a deployment that
89/// believes it is encrypted and is not.
90///
91/// ```toml
92/// [server.tls]
93/// certificate = "/etc/dynamic-config/server.pem"
94/// key = "/etc/dynamic-config/server.key"
95/// client_ca = "/etc/dynamic-config/clients-ca.pem"   # optional; see below
96/// ```
97///
98/// Only paths live here. The key's *bytes* are read once, at startup, by
99/// [`Tls::load`](crate::tls::Tls::load), and never reach a diagnostic — see
100/// [`TlsError`](crate::tls::TlsError).
101#[derive(Debug, Clone, Deserialize)]
102#[serde(deny_unknown_fields)]
103pub struct TlsConfig {
104    /// PEM holding the server's certificate, then any intermediates, leaf
105    /// first.
106    pub certificate: String,
107    /// PEM holding that certificate's private key: PKCS#8, PKCS#1 or SEC1.
108    ///
109    /// On Unix the server **refuses to start** if this file is readable by
110    /// anything but its owner, for the same reason it refuses a token under
111    /// 32 characters.
112    pub key: String,
113    /// PEM holding the certificate authority every client certificate must
114    /// chain to.
115    ///
116    /// Present means **mutual TLS is required**: a caller that presents no
117    /// certificate, or one signed by anything else, does not complete the
118    /// handshake and never becomes a request. Absent means the server
119    /// authenticates itself to callers and asks for nothing back.
120    ///
121    /// A certificate is a second gate, never a second identity: it is not an
122    /// alternative to the bearer token and it does not name a caller. See
123    /// the [`tls`](crate::tls) module.
124    ///
125    /// **No revocation is checked.** A certificate that chains here is good
126    /// until it expires; see [`crl`](Self::crl).
127    #[serde(default)]
128    pub client_ca: Option<String>,
129    /// A certificate revocation list — **a startup refusal**, never a file
130    /// this server reads.
131    ///
132    /// The key exists so that an operator who reaches for revocation is told
133    /// that this server does not check it, rather than being told `unknown
134    /// field 'crl'` and going looking for a different spelling. It is the
135    /// same reason [`tls`](ServerConfig::tls) itself is parsed in a build
136    /// without the feature: a security-relevant key that reads as a typo is
137    /// worse than one that reads as a decision.
138    ///
139    /// The decision, and it was measured rather than assumed
140    /// (`RevocationUnsupported`'s message is the short form): rustls will
141    /// accept a CRL whose `nextUpdate` passed years ago without a word,
142    /// because `ExpirationPolicy::Ignore` is the default — so the twenty
143    /// lines that look like revocation are a check that stops being true the
144    /// moment the file stops being refreshed, with nothing anywhere
145    /// reporting it. The one switch that refuses a stale list,
146    /// `enforce_revocation_expiration`, refuses **every** client while it is
147    /// stale, which turns a CRL publishing hiccup into a fleet-wide
148    /// configuration outage. Neither is a posture this crate will ship, and
149    /// a file watcher does not rescue it: the failure to catch is the
150    /// *absence* of a write, and no filesystem event fires for that.
151    ///
152    /// What to do instead is in [`tls`](crate::tls): short-lived client
153    /// certificates, and revoke the bearer token — the credential that
154    /// actually authorises, and the one this server can withdraw by removing
155    /// a line.
156    #[serde(default)]
157    pub crl: Option<String>,
158}
159
160/// One caller, and what it may read.
161#[derive(Debug, Clone, Deserialize)]
162#[serde(deny_unknown_fields)]
163pub struct ClientConfig {
164    /// The client's name. Appears in the audit log and nowhere else.
165    pub name: String,
166    /// The bearer token this client presents.
167    ///
168    /// Absent means **anonymous**: this client is whoever calls without a
169    /// credential. That needs [`allow_anonymous`](ServerConfig::allow_anonymous)
170    /// as well, so an omitted token can never be the accident that opens a
171    /// server up.
172    #[serde(default)]
173    pub token: Option<Token>,
174    /// The applications this client may read, by name. Exact, no wildcards.
175    pub applications: Vec<String>,
176}
177
178impl ClientConfig {
179    /// Whether this client is the anonymous one.
180    #[must_use]
181    pub fn is_anonymous(&self) -> bool {
182        self.token.is_none()
183    }
184}
185
186/// Everything the server needs to start.
187///
188/// `deny_unknown_fields` on purpose: a misspelled `allow_anonymous` that
189/// silently stayed `false` would be a harmless surprise, and a misspelled
190/// `applications` that silently granted nothing would be a confusing one —
191/// but a key this struct does not know is, in a security-relevant file, a
192/// key the operator believes is doing something. Refuse it.
193#[derive(Debug, Clone, Deserialize)]
194#[serde(deny_unknown_fields)]
195pub struct ServerConfig {
196    /// The address to listen on. Loopback unless said otherwise.
197    #[serde(default = "default_bind")]
198    pub bind: String,
199    /// Permits a bind address that is not loopback **when this server
200    /// terminates no TLS**.
201    ///
202    /// Without [`tls`](Self::tls), a non-loopback bind means configuration —
203    /// secrets included — crossing a network in the clear unless something
204    /// in front of it is doing the encryption. Setting this is the operator
205    /// saying that something is.
206    ///
207    /// With [`tls`](Self::tls) it is a **refusal**, not a no-op. The word
208    /// acknowledges an unencrypted socket, and there is not one; leaving it
209    /// set while TLS is on would make it stop meaning anything, so that
210    /// removing the TLS block later would quietly reopen the port instead of
211    /// refusing.
212    #[serde(default)]
213    pub insecure: bool,
214    /// TLS termination, and the client certificate that goes with it.
215    ///
216    /// Absent — the default — is a server that speaks plain HTTP and expects
217    /// a terminator in front of it, exactly as before. Present is this
218    /// process terminating TLS itself, and needs the `tls` Cargo feature: a
219    /// build without it **refuses to start** rather than ignoring the block.
220    #[serde(default)]
221    pub tls: Option<TlsConfig>,
222    /// Permits a client with no token.
223    #[serde(default)]
224    pub allow_anonymous: bool,
225    /// The file watcher's debounce, in milliseconds. Zero disables
226    /// watching, which is what an operator who reloads by other means
227    /// wants.
228    #[serde(default = "default_debounce_ms")]
229    pub watch_debounce_ms: u64,
230    /// How many change-stream connections may be open at once, across every
231    /// caller and every section.
232    ///
233    /// **Zero turns the endpoint off**, and a server with it off answers
234    /// `/stream` with the same 404 as everything else it does not serve — a
235    /// deployment that does not want long-lived connections says so once
236    /// here rather than in whatever is in front of it.
237    ///
238    /// It is a backstop, not a rate limit. Per-*caller* limiting belongs to
239    /// the thing in front, which is the only place that sees every replica's
240    /// share of a caller; what this bounds is the total number of sockets one
241    /// process will hold open on this endpoint, so a client reconnecting in
242    /// a loop cannot take the process with it.
243    #[serde(default = "default_max_streams")]
244    pub max_stream_connections: usize,
245    /// The served applications and profiles.
246    pub sections: Vec<SectionConfig>,
247    /// The callers.
248    pub clients: Vec<ClientConfig>,
249}
250
251impl Default for ServerConfig {
252    fn default() -> Self {
253        Self {
254            bind: default_bind(),
255            insecure: false,
256            tls: None,
257            allow_anonymous: false,
258            watch_debounce_ms: default_debounce_ms(),
259            max_stream_connections: default_max_streams(),
260            sections: Vec::new(),
261            clients: Vec::new(),
262        }
263    }
264}