acme_proxy/config/types/server.rs
1//! `[database]`, `[server]`, `[admin]`, `[nonce]`, `[logging]`, `[dns]`,
2//! `[order]` and `[meta]` — the process-wide sections.
3//!
4//! Re-exported flat from [`super`], so nothing outside this directory names
5//! the submodule.
6
7use serde::Deserialize;
8
9use super::empty_string_is_no_values;
10
11/// The resolver every DNS lookup this server makes goes through.
12#[derive(Debug, Clone, Default, Deserialize)]
13#[serde(default)]
14pub struct DnsConfig {
15 pub resolver: Option<String>,
16}
17/// Database connection and configuration settings.
18#[derive(Debug, Clone, Deserialize)]
19#[serde(default)]
20pub struct DatabaseConfig {
21 pub url: String,
22}
23
24impl Default for DatabaseConfig {
25 fn default() -> Self {
26 Self {
27 url: "sqlite://sqlite.db".to_string(),
28 }
29 }
30}
31/// Server network and binding configuration.
32#[derive(Debug, Clone, Deserialize)]
33#[serde(default)]
34pub struct ServerConfig {
35 pub bind_address: String,
36 pub base_url: String,
37 pub tls: TlsConfig,
38 /// How many ACME requests may be in flight at once before the server starts
39 /// refusing. The value is what it has always been; what changed is that
40 /// exceeding it now *sheds* rather than queueing indefinitely.
41 pub max_concurrent_requests: usize,
42 /// How long a request may wait for a slot before it is refused. Long enough
43 /// to absorb a burst, short enough that the queue behind the limit can never
44 /// grow deeper than this in time.
45 pub admission_wait_ms: u64,
46 /// A whole-request deadline. Must exceed every hook the server runs *inside*
47 /// a request — `challenge.timeout_ms` and `signer.custom.timeout_ms` — or a
48 /// validation still in progress is cut off and reported as a server failure;
49 /// `Profile::build_all` refuses to start if it does not.
50 pub request_timeout_ms: u64,
51 /// Largest request body accepted. An ACME body is a JWS carrying at most a
52 /// CSR, i.e. kilobytes; axum's implicit default is 2 MiB.
53 pub max_body_bytes: usize,
54}
55
56impl Default for ServerConfig {
57 fn default() -> Self {
58 Self {
59 bind_address: "[::]:3000".to_string(),
60 base_url: "http://localhost:3000".to_string(),
61 tls: TlsConfig::default(),
62 max_concurrent_requests: 100,
63 admission_wait_ms: 50,
64 request_timeout_ms: 60_000,
65 max_body_bytes: 128 * 1024,
66 }
67 }
68}
69/// HTTPS termination for the server's own listener.
70#[derive(Debug, Clone, Deserialize)]
71#[serde(default)]
72pub struct TlsConfig {
73 pub enabled: bool,
74 pub cert_path: String,
75 pub key_path: String,
76 pub handshake_timeout_ms: u64,
77}
78
79impl Default for TlsConfig {
80 fn default() -> Self {
81 Self {
82 enabled: false,
83 cert_path: "server.pem".to_string(),
84 key_path: "server.key".to_string(),
85 handshake_timeout_ms: 10_000,
86 }
87 }
88}
89/// The web admin interface: a **second listener**, on its own socket, serving
90/// no ACME.
91///
92/// Process-wide, not per-profile -- an operator manages every endpoint this
93/// process serves, so there is no `[profiles.<name>].admin` and this is not in
94/// `PROFILE_SECTIONS`.
95///
96/// Off by default. A certificate authority should not grow a management
97/// surface because somebody upgraded it.
98#[derive(Debug, Clone, Deserialize)]
99#[serde(default)]
100pub struct AdminConfig {
101 pub enabled: bool,
102 /// Loopback on purpose. This listener has no admission control and no
103 /// filter chain, and until [`AdminTlsConfig::enabled`] is set, no
104 /// transport security either. `webadmin::check_config` refuses to start on
105 /// a non-loopback bind while TLS is off.
106 pub bind_address: String,
107 /// The origin the panel is reached at. Load-bearing three times over: the
108 /// CSRF origin check compares against it, a generated self-signed
109 /// certificate takes its host, and the pages will build absolute URLs from
110 /// it -- exactly as `server.base_url` does for the ACME listener.
111 pub base_url: String,
112 /// Absolute session lifetime. Never extended by activity: past it, the
113 /// operator signs in again.
114 pub session_ttl_seconds: u64,
115 /// Idle session lifetime, advanced on use (at most once a minute, so a
116 /// polling page is not a stream of database writes).
117 pub session_idle_timeout_seconds: u64,
118 /// Failed logins allowed from one address per window, then `429` with a
119 /// `Retry-After`. The password hash is deliberately expensive, so this is
120 /// an availability control as much as a credential one.
121 pub login_max_attempts: u32,
122 pub login_window_seconds: u64,
123 /// Require a second factor of every operator.
124 ///
125 /// What this changes is the operator who has **none**: with it on, their
126 /// next sign-in lands on the enrolment page and their session stays
127 /// `pending_mfa` until they finish. An operator who already has a factor is
128 /// challenged whether this is set or not -- the flag governs enrolment, not
129 /// enforcement.
130 ///
131 /// Deliberately *not* "refuse a password-only login": enrolling needs a
132 /// session and a session would then need a factor, so setting this on a
133 /// panel with no enrolled operator would brick it, with no way in to fix it.
134 ///
135 /// Does not retroactively end sessions that predate it. The lever that does
136 /// is `acme-proxy admin session revoke --all`.
137 pub require_mfa: bool,
138 /// Largest admin request body -- a small JSON object or a form.
139 pub max_body_bytes: usize,
140 /// Ceiling on `?limit=` for the list endpoints.
141 pub page_size_max: i64,
142 /// Override individual page templates on disk, mirroring
143 /// `notify.template_dir`. Empty means the compiled-in defaults.
144 pub template_dir: String,
145 pub tls: AdminTlsConfig,
146}
147
148impl Default for AdminConfig {
149 fn default() -> Self {
150 Self {
151 enabled: false,
152 bind_address: "127.0.0.1:3001".to_string(),
153 base_url: "http://localhost:3001".to_string(),
154 session_ttl_seconds: 43_200,
155 session_idle_timeout_seconds: 3_600,
156 login_max_attempts: 5,
157 login_window_seconds: 300,
158 require_mfa: false,
159 max_body_bytes: 64 * 1024,
160 page_size_max: 200,
161 template_dir: String::new(),
162 tls: AdminTlsConfig::default(),
163 }
164 }
165}
166/// HTTPS termination for the admin listener.
167///
168/// Same shape and same one-listener-not-two semantics as [`TlsConfig`], with
169/// its own certificate paths so the two listeners cannot be made to share one
170/// by accident. A separate type rather than a reuse of `TlsConfig` because the
171/// defaults differ and `config.toml.example` documents each key against its
172/// own default.
173#[derive(Debug, Clone, Deserialize)]
174#[serde(default)]
175pub struct AdminTlsConfig {
176 pub enabled: bool,
177 pub cert_path: String,
178 pub key_path: String,
179 pub handshake_timeout_ms: u64,
180}
181
182impl Default for AdminTlsConfig {
183 fn default() -> Self {
184 Self {
185 enabled: false,
186 cert_path: "admin.pem".to_string(),
187 key_path: "admin.key".to_string(),
188 handshake_timeout_ms: 10_000,
189 }
190 }
191}
192/// Nonce expiration and validation configuration.
193#[derive(Debug, Clone, Deserialize)]
194#[serde(default)]
195pub struct NonceConfig {
196 pub ttl_seconds: u64,
197}
198
199impl Default for NonceConfig {
200 fn default() -> Self {
201 Self { ttl_seconds: 300 }
202 }
203}
204/// Logging and tracing configuration.
205#[derive(Debug, Clone, Deserialize)]
206#[serde(default)]
207pub struct LoggingConfig {
208 pub filter: String,
209 pub json_format: bool,
210 /// Where records are written: `stdout` or `stderr`. Anything else is a
211 /// startup error.
212 ///
213 /// `stdout` is the default and is load-bearing beyond taste: the end-to-end
214 /// suite gates container readiness on `server_startup` appearing there.
215 pub target: String,
216 /// ANSI colour in the human-readable format. Ignored under `json_format`.
217 pub ansi: bool,
218 /// Span lifecycle records: `none`, `close` or `full`. `close` is the useful
219 /// one — it emits a record per span as it closes, carrying the time spent
220 /// busy and idle inside it.
221 pub span_events: String,
222 /// JSON only: lift a record's own fields to the top level instead of
223 /// nesting them under `fields`. What most log pipelines want, but it can
224 /// collide with the format's reserved keys, so it is not the default.
225 pub flatten_event: bool,
226}
227
228impl Default for LoggingConfig {
229 fn default() -> Self {
230 Self {
231 filter: "acme_proxy=info".to_string(),
232 json_format: false,
233 target: "stdout".to_string(),
234 ansi: true,
235 span_events: "none".to_string(),
236 flatten_event: false,
237 }
238 }
239}
240/// Order object configuration.
241#[derive(Debug, Clone, Deserialize)]
242#[serde(default)]
243pub struct OrderConfig {
244 pub validity_seconds: u64,
245 /// Most identifiers one `newOrder` may name.
246 ///
247 /// The only other bound is `server.max_body_bytes` (128 KiB), which at the
248 /// ~30 bytes an identifier costs lets a single request ask for some four
249 /// thousand names — and each one becomes an authorization plus a challenge
250 /// per offered type, all inserted in **one** transaction. SQLite has a
251 /// single writer, so that transaction stalls every other write in the
252 /// process for as long as it runs, and the response is four thousand
253 /// authorization URLs.
254 ///
255 /// 100 is what Let's Encrypt allows, and is far above what a real client
256 /// asks for; the point is a ceiling, not a policy. A refusal is
257 /// `malformed`, not `rateLimited` — the order is malformed for this server
258 /// whenever it is sent, and §6.6's retry-later reading would be a lie.
259 pub max_identifiers: usize,
260 /// Days an order is kept after it expires, before the retention sweep
261 /// deletes it. `0` keeps everything for ever.
262 ///
263 /// Nothing pruned `orders` before this, so the table and the
264 /// `authorizations` and `challenges` that cascade from it grew for the life
265 /// of a deployment — one order plus N authorizations plus N×M challenges
266 /// per `newOrder`, on a default configuration where `newAccount` is open to
267 /// anyone.
268 ///
269 /// **A `valid` order is never swept, whatever its age.** Its row is what
270 /// `revokeCert` and the CRL find a certificate by serial through, and what
271 /// RFC 9773 renewal information is derived from; deleting one would make an
272 /// issued certificate unrevokable. Only orders that ended some other way —
273 /// `invalid`, or abandoned `pending`/`ready`/`processing` — are eligible,
274 /// and only once their own `expires` is `retention_days` behind, at which
275 /// point no client can act on them either.
276 ///
277 /// Per-profile like the rest of `[order]`: the sweep is one handler that
278 /// applies each mounted profile's own value to that profile's rows.
279 pub retention_days: u64,
280}
281
282impl Default for OrderConfig {
283 fn default() -> Self {
284 Self {
285 validity_seconds: 604800,
286 max_identifiers: 100,
287 retention_days: 30,
288 }
289 }
290}
291/// The optional `meta` members of the directory object (RFC 8555 §7.1.1).
292///
293/// All empty by default, and an empty field is omitted rather than sent blank:
294/// §7.1.1 makes every one optional, and advertising `"website": ""` says less
295/// than saying nothing.
296///
297/// `terms_of_service` is the one with teeth. Setting it turns on §7.3.3's
298/// agreement requirement — `newAccount` then refuses a request that does not
299/// carry `termsOfServiceAgreed: true` — so it is not merely cosmetic, and
300/// leaving it unset (the default) keeps today's behaviour exactly.
301#[derive(Debug, Clone, Deserialize, Default)]
302#[serde(default)]
303pub struct MetaConfig {
304 /// A URL identifying the current terms of service.
305 pub terms_of_service: String,
306 /// An HTTP or HTTPS URL locating a website providing more information
307 /// about the ACME server.
308 pub website: String,
309 /// The hostnames this CA recognizes in CAA records (RFC 8555 §7.1.1).
310 /// Advertised only; this server does not itself check CAA.
311 #[serde(deserialize_with = "empty_string_is_no_values")]
312 pub caa_identities: Vec<String>,
313}