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}