acme_proxy/cli/mod.rs
1//! The command tree, and the startup path itself.
2//!
3//! **Nothing here prints or exits.** Every command body returns
4//! `Result<(), CliError>` and [`dispatch`] routes to it, so each arm is a plain
5//! function a test can call and assert on rather than an unreachable dead end.
6//! `src/main.rs` is where that `Result` becomes an exit status, and it is the
7//! only place in the project that calls `std::process::exit` — a library whose
8//! failure mode is ending the process is one nothing else can use.
9//!
10//! Startup is split on the socket boundary, which is what lets a test drive the
11//! whole path on an ephemeral port with its own shutdown future instead of a
12//! process signal:
13//!
14//! - `serve` binds `server.bind_address`, installs the `SIGHUP` handler, and
15//! hands the socket on.
16//! - [`serve_on`] validates the admin configuration and binds that socket too,
17//! when `[admin]` is enabled.
18//! - [`serve_on_with`] does everything else — profile resolution, deduplicated
19//! signer backends, per-profile filters and validators, TLS, the job registry
20//! (every signer's handlers, notification delivery and the four table sweeps),
21//! the runner draining it, and `axum::serve` with connect info attached.
22//!
23//! That assembly is [`build_generation`], and it is called again on every
24//! reload rather than only at startup — so the two cannot drift, and a
25//! subsystem added to one is added to the other by construction. What a reload
26//! may change, and what it refuses by name, is [`crate::reload`]'s to say;
27//! [`serve_on_with_reloads`] is where the two meet.
28//!
29//! The logic behind each admin subcommand lives in [`crate::admin`], not here;
30//! this module is the `clap` surface over it. [`logging`] turns `[logging]` into
31//! an installed subscriber, validating every value before installing anything.
32//!
33//! What a command *prints* is [`render`]'s, and how it is coloured is
34//! [`style`]'s. Those renderings sit here rather than in [`crate::admin`]
35//! because they have exactly one consumer — the terminal — where the JSON ones
36//! beside them are a wire format the web admin parses too. [`dispatch`]
37//! resolves one [`Palette`] and threads it down; `nonce` and `upstream` take
38//! none, printing only fixed text.
39
40use std::future::Future;
41use std::io::{BufRead, IsTerminal};
42use std::net::SocketAddr;
43use std::sync::Arc;
44use std::time::Duration;
45
46use clap::{Parser, Subcommand};
47use clap_complete::aot::Shell;
48use tokio::net::TcpListener;
49use tracing::{error, info, warn};
50
51pub mod account;
52pub mod audit;
53pub mod eab;
54pub mod filter;
55pub mod generate;
56mod logging;
57
58/// Installs the `[logging]` configuration. Re-exported because `main.rs` is
59/// what calls it — see [`dispatch`].
60pub use logging::init_logging;
61/// The `--log-level` flag and the per-invocation decision it feeds. Re-exported
62/// for `main.rs`, which is where the subscriber is installed.
63pub use logging::{LogLevel, LoggingPlan, init_command_logging, plan_logging};
64pub mod nonce;
65pub mod order;
66pub mod profile;
67pub mod render;
68pub mod style;
69pub mod upstream;
70pub mod webadmin;
71pub mod window;
72
73pub use account::AccountCommand;
74pub use audit::AuditCommand;
75pub use eab::EabCommand;
76pub use nonce::NonceCommand;
77pub use order::OrderCommand;
78pub use profile::ProfileCommand;
79pub use upstream::UpstreamCommand;
80pub use webadmin::AdminCommand;
81
82use crate::cli::filter::FilterCommand;
83pub use crate::cli::style::{ColorChoice, Palette};
84use crate::config::Config;
85use crate::sqlite::db::Database;
86use crate::{Profile, build_app, tls};
87
88#[derive(Parser)]
89#[command(
90 name = "acme-proxy",
91 version = env!("CARGO_PKG_VERSION"),
92 about = "ACME server, plus admin commands for its database"
93)]
94pub struct Cli {
95 /// Skip interactive "Are you sure?" confirmation on destructive commands.
96 #[arg(short = 'y', long, global = true)]
97 pub yes: bool,
98
99 /// When to colour human-readable output. `--json` output never carries it.
100 #[arg(long, value_enum, default_value_t = ColorChoice::Auto, global = true)]
101 pub color: ColorChoice,
102
103 /// Emit log records for this run, at this level, on stderr. Without it an
104 /// admin command prints only its own output; `serve` uses `[logging]`.
105 #[arg(long, value_enum, global = true)]
106 pub log_level: Option<LogLevel>,
107
108 #[command(subcommand)]
109 pub command: Option<Command>,
110}
111
112#[derive(Subcommand)]
113pub enum Command {
114 /// Run the ACME HTTP(S) server. Default when no subcommand is given.
115 Serve,
116 /// Inspect and manage ACME accounts.
117 Account {
118 #[command(subcommand)]
119 command: AccountCommand,
120 },
121 /// Inspect and manage ACME orders.
122 Order {
123 #[command(subcommand)]
124 command: OrderCommand,
125 },
126 /// Read and prune the CA's audit trail.
127 Audit {
128 #[command(subcommand)]
129 command: AuditCommand,
130 },
131 /// Nonce table maintenance.
132 Nonce {
133 #[command(subcommand)]
134 command: NonceCommand,
135 },
136 /// Inspect the ACME endpoints this configuration mounts.
137 Profile {
138 #[command(subcommand)]
139 command: ProfileCommand,
140 },
141 /// Manage External Account Binding (EAB) credentials.
142 Eab {
143 #[command(subcommand)]
144 command: EabCommand,
145 },
146 /// Read and test the access policy of an endpoint.
147 Filter {
148 #[command(subcommand)]
149 command: FilterCommand,
150 },
151 /// Manage this server's own account at the upstream ACME server
152 /// (`signer.backend = "relay"`).
153 Upstream {
154 #[command(subcommand)]
155 command: UpstreamCommand,
156 },
157 /// Manage the web admin's operators and their sessions. This is how the
158 /// panel is bootstrapped: it has no sign-up page.
159 Admin {
160 #[command(subcommand)]
161 command: AdminCommand,
162 },
163 /// Print a shell completion script on stdout.
164 Completions {
165 /// The shell to generate for.
166 #[arg(value_enum)]
167 shell: Shell,
168 },
169 /// Print this binary's man page, in roff, on stdout.
170 Man,
171}
172
173/// Picks the profile a command acts on.
174///
175/// `--profile` is optional only when the configuration defines exactly one:
176/// most per-profile sections would otherwise be acted on ambiguously, and
177/// guessing is worse than asking. Shared by `upstream` and `filter`, which had
178/// grown one copy each.
179pub(crate) fn resolve_profile(
180 config: &Config,
181 wanted: Option<&str>,
182) -> Result<crate::config::ProfileConfig, CliError> {
183 let profiles = config
184 .resolve_profiles()
185 .map_err(|error| CliError(format!("configuration error: {error}")))?;
186
187 match wanted {
188 Some(name) => profiles
189 .into_iter()
190 .find(|profile| profile.name == name)
191 .ok_or_else(|| CliError(format!("no profile named `{name}` in this configuration"))),
192 None if profiles.len() == 1 => Ok(profiles.into_iter().next().expect("length checked")),
193 None => {
194 let names: Vec<&str> = profiles.iter().map(|p| p.name.as_str()).collect();
195 Err(CliError(format!(
196 "this configuration defines several profiles ({}); say which one with --profile",
197 names.join(", ")
198 )))
199 }
200 }
201}
202
203/// A command that could not complete, carrying the message to print.
204///
205/// Every failing branch below returns one of these instead of calling
206/// `std::process::exit` where it stands: `main.rs` is the single place that
207/// prints and exits, so each command body stays a plain function a test can
208/// call and assert on.
209#[derive(Debug, PartialEq, Eq, thiserror::Error)]
210#[error("{0}")]
211pub struct CliError(pub String);
212
213impl From<sqlx::Error> for CliError {
214 fn from(error: sqlx::Error) -> Self {
215 Self(format!("database error: {error}"))
216 }
217}
218
219/// Routes a parsed command to its handler.
220///
221/// The library's entry point. Everything above it — parsing argv, loading the
222/// configuration, installing the subscriber, opening the database, printing a
223/// failure and exiting — lives in `src/main.rs`, because those are the
224/// binary's job and not a library's: nothing that links this crate can use a
225/// function whose failure mode is `std::process::exit`.
226///
227/// Takes the `--color` *choice* rather than a resolved [`Palette`], and
228/// resolves it here: the answer depends on whether this process's stdout is a
229/// terminal and on `NO_COLOR`, and neither belongs in `main.rs`, which is
230/// excluded from the coverage floor precisely because nothing in it is
231/// reachable from a test.
232pub async fn dispatch(
233 command: Option<Command>,
234 yes: bool,
235 color: ColorChoice,
236 reader: &mut impl BufRead,
237 config: &Arc<Config>,
238 database: Arc<Database>,
239) -> Result<(), CliError> {
240 let palette = Palette::resolve(
241 color,
242 std::io::stdout().is_terminal(),
243 std::env::var("NO_COLOR").ok().as_deref(),
244 );
245 match command.unwrap_or(Command::Serve) {
246 Command::Serve => serve(config.clone(), database).await,
247 Command::Account { command } => {
248 account::run_account_command(command, yes, palette, reader, config, database).await
249 }
250 Command::Order { command } => {
251 order::run_order_command(command, yes, palette, reader, config, database).await
252 }
253 Command::Audit { command } => {
254 audit::run_audit_command(command, yes, palette, reader, database).await
255 }
256 Command::Nonce { command } => {
257 nonce::run_nonce_command(command, yes, reader, config, database).await
258 }
259 Command::Profile { command } => {
260 profile::run_profile_command(command, palette, config).await
261 }
262 Command::Eab { command } => eab::run_eab_command(command, palette, database).await,
263 Command::Filter { command } => filter::run_filter_command(command, palette, config).await,
264 Command::Upstream { command } => {
265 upstream::run_upstream_command(command, reader, config).await
266 }
267 Command::Admin { command } => {
268 webadmin::run_admin_command(command, yes, palette, reader, config, database).await
269 }
270 // Reachable here, though `main.rs` answers both before it opens
271 // anything: an `unreachable!()` would be dead code under the coverage
272 // floor, and routing them keeps this a total function over `Command`.
273 command @ (Command::Completions { .. } | Command::Man) => {
274 generate::write(&command, &mut std::io::stdout().lock())
275 }
276 }
277}
278
279/// Binds the configured socket and runs the ACME HTTP(S) server until a
280/// shutdown signal arrives.
281pub async fn serve(config: Arc<Config>, database: Arc<Database>) -> Result<(), CliError> {
282 let listener = TcpListener::bind(&config.server.bind_address)
283 .await
284 .map_err(|error| {
285 error!(event = "server_socket_bind_failed", outcome = "failure", bind_address = %config.server.bind_address, error = %error);
286 CliError(format!(
287 "cannot bind {}: {error}",
288 config.server.bind_address
289 ))
290 })?;
291
292 // Installed here, before anything slow: `SIGHUP`'s default disposition is
293 // *terminate*, so until the handler exists a reload signal kills the
294 // process. `serve_on_with_reloads` does profile assembly and the relay's
295 // first upstream contact before it binds anything, which is exactly the
296 // window an operator's `systemctl reload` could land in.
297 let (reload_handle, reloads) = crate::reload::channel();
298 let _hangups = AbortOnDrop(tokio::spawn(watch_for_hangup(reload_handle)));
299
300 let admin_listener = bind_admin(&config).await.map_err(|error| {
301 error!(event = "server_fatal_error", outcome = "failure", error = %error);
302 CliError(error.to_string())
303 })?;
304 let metrics_listener = bind_metrics(&config).await.map_err(|error| {
305 error!(event = "server_fatal_error", outcome = "failure", error = %error);
306 CliError(error.to_string())
307 })?;
308
309 serve_on_with_reloads(
310 config,
311 database,
312 listener,
313 admin_listener,
314 metrics_listener,
315 shutdown_signal(),
316 reloads,
317 )
318 .await
319 .map_err(|error| {
320 error!(event = "server_fatal_error", outcome = "failure", error = %error);
321 CliError(error.to_string())
322 })
323}
324
325/// Turns every `SIGHUP` into a reload request, for the life of the process.
326///
327/// Unlike the shutdown signal, this one does not consume its stream: an operator
328/// reloads repeatedly, and a handler that fired once would leave the second
329/// `SIGHUP` back at its default disposition — killing the server.
330#[cfg(unix)]
331async fn watch_for_hangup(handle: crate::reload::ReloadHandle) {
332 let mut hangups = match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::hangup()) {
333 Ok(stream) => stream,
334 Err(error) => {
335 error!(event = "server_signal_handler_failed", outcome = "failure", signal = "SIGHUP", error = %error);
336 return;
337 }
338 };
339 while hangups.recv().await.is_some() {
340 handle.trigger();
341 }
342}
343
344/// No `SIGHUP` off Unix, so there is nothing to watch for.
345#[cfg(not(unix))]
346async fn watch_for_hangup(_handle: crate::reload::ReloadHandle) {
347 std::future::pending::<()>().await;
348}
349
350/// Validates the admin configuration and binds its socket when it is enabled.
351///
352/// Shared by [`serve`] and [`serve_on`] rather than living in one of them: both
353/// need it, and the validation must happen **before anything binds**, so a
354/// misconfigured panel cannot take the ACME listener down with it halfway
355/// through startup.
356async fn bind_admin(config: &Arc<Config>) -> anyhow::Result<Option<TcpListener>> {
357 crate::webadmin::check_config(config).inspect_err(|error| {
358 error!(event = "admin_config_invalid", outcome = "failure", error = %error);
359 })?;
360
361 match config.admin.enabled {
362 false => Ok(None),
363 true => Ok(Some(
364 TcpListener::bind(&config.admin.bind_address)
365 .await
366 .inspect_err(|error| {
367 error!(event = "admin_socket_bind_failed",
368 outcome = "failure",
369 bind_address = %config.admin.bind_address,
370 error = %error);
371 })?,
372 )),
373 }
374}
375
376/// Refuses a `[metrics]` bind address that collides with another listener's.
377///
378/// Pure, so a reload runs the same check before rebinding anything — the twin of
379/// [`crate::webadmin::check_config`], and beside it in `apply_reload` for the
380/// same reason: a listener configuration that would not start must not be one a
381/// running server can be reloaded into.
382///
383/// Three listeners, so the check is pairwise. `webadmin` already refuses
384/// admin-versus-server; these are the two pairs it cannot see. Checked even when
385/// `[admin]` is off, since enabling the panel later must not be what surfaces a
386/// latent conflict.
387///
388/// # Errors
389///
390/// Names the other key when the two addresses are equal.
391pub fn check_metrics_config(config: &Config) -> anyhow::Result<()> {
392 if !config.metrics.enabled {
393 return Ok(());
394 }
395
396 let bind = &config.metrics.bind_address;
397 for (name, other) in [
398 ("server.bind_address", &config.server.bind_address),
399 ("admin.bind_address", &config.admin.bind_address),
400 ] {
401 if bind == other && (name != "admin.bind_address" || config.admin.enabled) {
402 error!(event = "metrics_config_invalid",
403 outcome = "failure",
404 bind_address = %bind);
405 anyhow::bail!(
406 "metrics.bind_address and {name} are both `{bind}`: the metrics endpoint is a \
407 separate listener and cannot share a socket (give it its own port)"
408 );
409 }
410 }
411 Ok(())
412}
413
414/// Binds the metrics socket when `[metrics]` is on, refusing a collision first.
415///
416/// The twin of [`bind_admin`], and the same shape for the same reason: a socket
417/// that cannot be bound must stop startup rather than leave the process running
418/// with one of its three listeners silently missing.
419///
420/// Deliberately **no loopback check**. `webadmin::check_config` refuses a
421/// non-loopback admin bind without TLS because that listener's cookie is always
422/// `Secure`, which a browser will not store over plain HTTP, so the failure
423/// would be invisible. Nothing here has a cookie: a metrics port reachable from
424/// a Prometheus host on another machine is the intended deployment, and the
425/// firewall is what bounds it.
426async fn bind_metrics(config: &Arc<Config>) -> anyhow::Result<Option<TcpListener>> {
427 check_metrics_config(config)?;
428 if !config.metrics.enabled {
429 return Ok(None);
430 }
431
432 let bind = &config.metrics.bind_address;
433 let listener = TcpListener::bind(bind).await.inspect_err(|error| {
434 error!(event = "metrics_socket_bind_failed",
435 outcome = "failure",
436 bind_address = %bind,
437 error = %error);
438 })?;
439 Ok(Some(listener))
440}
441
442/// Assembles and serves the application over an already-bound socket.
443///
444/// Split from [`serve`] on the socket boundary: a caller supplying its own
445/// listener and its own `shutdown` future can drive the whole startup path —
446/// profile assembly, TLS, backend resume, the nonce reaper, both `axum::serve`
447/// arms — without owning a fixed port or a process signal.
448///
449/// Binds the web admin socket itself when `[admin]` is enabled. The signature
450/// is unchanged, and `admin.enabled` is false by default, so every existing
451/// caller is untouched; a test that wants to drive *both* listeners supplies
452/// its own pair through [`serve_on_with`].
453pub async fn serve_on(
454 config: Arc<Config>,
455 database: Arc<Database>,
456 listener: TcpListener,
457 shutdown: impl Future<Output = ()> + Send + 'static,
458) -> anyhow::Result<()> {
459 let admin_listener = bind_admin(&config).await?;
460 let metrics_listener = bind_metrics(&config).await?;
461 serve_on_with(
462 config,
463 database,
464 listener,
465 admin_listener,
466 metrics_listener,
467 shutdown,
468 )
469 .await
470}
471
472/// [`serve_on`] with all three sockets supplied.
473///
474/// The full version, split on the same boundary and for the same reason: a
475/// caller handing in three ephemeral ports can drive the whole startup path —
476/// including that one shutdown signal stops all of them — without owning a
477/// fixed port or a process signal.
478pub async fn serve_on_with(
479 config: Arc<Config>,
480 database: Arc<Database>,
481 listener: TcpListener,
482 admin_listener: Option<TcpListener>,
483 metrics_listener: Option<TcpListener>,
484 shutdown: impl Future<Output = ()> + Send + 'static,
485) -> anyhow::Result<()> {
486 serve_on_with_reloads(
487 config,
488 database,
489 listener,
490 admin_listener,
491 metrics_listener,
492 shutdown,
493 crate::reload::Reloads::none(),
494 )
495 .await
496}
497
498/// [`serve_on_with`], serving configuration reloads as well as requests.
499///
500/// The variant `serve` uses, so a `SIGHUP` rebuilds both routers, the job
501/// registry, the notifier map and both TLS acceptors behind the sockets that are
502/// already bound. Every other caller goes through [`serve_on_with`] and gets a
503/// source that never fires, which costs one task that ends immediately.
504///
505/// See [`crate::reload`] for what a reload may change and what it refuses.
506pub async fn serve_on_with_reloads(
507 config: Arc<Config>,
508 database: Arc<Database>,
509 listener: TcpListener,
510 admin_listener: Option<TcpListener>,
511 metrics_listener: Option<TcpListener>,
512 shutdown: impl Future<Output = ()> + Send + 'static,
513 reloads: crate::reload::Reloads,
514) -> anyhow::Result<()> {
515 info!(
516 event = "server_startup",
517 outcome = "success",
518 bind_address = %config.server.bind_address,
519 base_url = %config.server.base_url,
520 tls = config.server.tls.enabled,
521 database_database_url = %config.database.url
522 );
523
524 // One `shutdown` future, several consumers: both listeners and the job
525 // runner. Created here rather than beside `axum::serve` below so a signal
526 // arriving *during* startup is not ignored — profile assembly and the
527 // relay's first upstream contact both happen before anything binds. The
528 // relay task is held under `AbortOnDrop` so an error path below does not
529 // leak a task parked on a signal that will never arrive.
530 let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
531 let _shutdown_relay = AbortOnDrop(tokio::spawn(async move {
532 shutdown.await;
533 let _ = shutdown_tx.send(true);
534 }));
535
536 // The enqueue side of the durable queue. Built before the profiles because
537 // a signer backend that defers issuance is handed one at construction, and
538 // process-wide for the reason `[audit]` is: one table, one runner, and a
539 // per-endpoint retry budget would make a job's pacing depend on which
540 // profile happened to queue it.
541 let job_queue = crate::jobs::JobQueue::new(database.clone(), &config.jobs);
542
543 let resolved = config.resolve_profiles().inspect_err(|error| {
544 error!(event = "profile_init_failed", outcome = "failure", error = %error);
545 })?;
546 // Everything that outlives a configuration generation — the signer backends
547 // above all, which are carried rather than rebuilt. See `crate::Assembly`.
548 let (assembly, parts) =
549 crate::Assembly::new(&resolved, database.clone(), job_queue.clone(), &config).inspect_err(
550 |error| {
551 error!(event = "profile_init_failed", outcome = "failure", error = %error);
552 },
553 )?;
554 let assembly = Arc::new(assembly);
555
556 let generation =
557 build_generation(&config, &resolved, &assembly, &parts, None).inspect_err(|error| {
558 error!(event = "profile_init_failed", outcome = "failure", error = %error);
559 })?;
560
561 // Every endpoint in the first generation came up, so every one is announced.
562 // A reload announces only the endpoints it *added* — see
563 // `supervise_reloads`, which shares this function for exactly that reason.
564 for profile in &generation.profiles {
565 announce_profile(profile).await;
566 }
567
568 let Generation {
569 profiles: _,
570 acme_app,
571 admin_app,
572 job_registry,
573 tls,
574 admin_tls,
575 logins,
576 } = generation;
577
578 // The one task that drains the queue. Held under `AbortOnDrop` for the same
579 // reason the reapers are — an un-cancelled loop holding an `Arc<Database>`
580 // per `serve_on` call — and, unlike the relay tasks this replaced, it does
581 // *not* need to outlive this function: the work is durable now, so a job cut
582 // short is re-claimed from its own row rather than lost. It takes the same
583 // shutdown signal as both listeners, so a stop is graceful rather than an
584 // abort: it releases its leases on the way out, and a restart therefore
585 // re-claims its own work immediately instead of waiting one out.
586 //
587 // Neither the registry it drains nor the `[jobs]` section it paces itself
588 // from is a value: both are cells a reload republishes, so a changed
589 // retention, a rebuilt notify map and a retuned lease or concurrency all
590 // reach the runner without restarting it. `jobs.max_attempts` is the third
591 // piece and does not come through here — it belongs to the enqueue side, so
592 // it is published onto `job_queue` itself.
593 let (registry_tx, registry_rx) = tokio::sync::watch::channel(Arc::new(job_registry));
594 let (jobs_tx, jobs_rx) = tokio::sync::watch::channel(Arc::new(config.jobs.clone()));
595 let _job_runner = AbortOnDrop(crate::jobs::spawn_runner_watching(
596 job_queue,
597 registry_rx,
598 jobs_rx,
599 shutdown_rx.clone(),
600 ));
601
602 info!(
603 event = "server_listening",
604 outcome = "success",
605 bind_address = %config.server.bind_address,
606 protocol = if tls.is_some() { "https" } else { "http" }
607 );
608
609 // One accept loop per role, each owning a socket a reload can replace and a
610 // TLS mode it can switch — see `crate::listener`. `axum::serve` below is
611 // handed one of these instead of a `TcpListener` and therefore outlives
612 // every rebind, which is what removes the listener from the list of things
613 // only a restart can change.
614 let admin_bound = bound_address(admin_listener.as_ref(), &config.admin.bind_address);
615 let metrics_bound = bound_address(metrics_listener.as_ref(), &config.metrics.bind_address);
616 let (acme_socket, acme_handle) = crate::listener::spawn("acme", Some(listener), tls);
617 let (admin_socket, admin_handle) = crate::listener::spawn("admin", admin_listener, admin_tls);
618 let (metrics_socket, metrics_handle) =
619 crate::listener::spawn("metrics", metrics_listener, None);
620
621 // Behind a swap cell rather than served directly, so a configuration reload
622 // can replace the whole router without the socket moving. The cell is what
623 // `axum::serve` holds; `acme_app` itself is only ever generation one.
624 let (acme_router_tx, acme_router_rx) = crate::reload::router_channel(acme_app);
625 let acme = serve_role(
626 crate::reload::swappable(acme_router_rx),
627 acme_socket,
628 shutdown_rx.clone(),
629 );
630
631 // Opened whether or not the panel is on, unlike the app inside it: with
632 // `admin.enabled` reloadable, a cell created only when the panel starts
633 // would be the one thing a reload turning it on could not reach. An empty
634 // `Router` answers `404` to everything, which is also what the panel being
635 // switched off later publishes here.
636 let (admin_router_tx, admin_router_rx) =
637 crate::reload::router_channel(admin_app.unwrap_or_default());
638 let admin = serve_role(
639 crate::reload::swappable(admin_router_rx),
640 admin_socket,
641 shutdown_rx.clone(),
642 );
643 if config.admin.enabled {
644 announce_admin_listener(&config, &database, &admin_bound).await;
645 }
646
647 // The third listener. Served directly rather than through a
648 // `reload::router_channel` like the other two, and the asymmetry is
649 // deliberate: this router has one route whose only state is the registry,
650 // and the registry is carried across generations rather than rebuilt (see
651 // `Assembly`), so a new generation could put nothing new in it.
652 if config.metrics.enabled {
653 announce_metrics_listener(&metrics_bound);
654 }
655 let metrics = serve_role(
656 crate::metrics_app(assembly.metrics.clone()),
657 metrics_socket,
658 shutdown_rx,
659 );
660
661 // The supervisor owns every cell sender from here on, which is what makes it
662 // the only writer: a generation is published by one task or by nobody.
663 // Aborted on drop, so an error return below does not leave it parked on a
664 // channel nothing will ever send to.
665 let _reload_supervisor = AbortOnDrop(tokio::spawn(supervise_reloads(
666 reloads,
667 config.clone(),
668 resolved,
669 assembly,
670 Cells {
671 acme_router: acme_router_tx,
672 admin_router: admin_router_tx,
673 job_registry: registry_tx,
674 jobs: jobs_tx,
675 acme: acme_handle,
676 admin: admin_handle,
677 metrics: metrics_handle,
678 },
679 logins,
680 )));
681
682 // Nothing is drained here any more. A notification in flight at shutdown is
683 // a `notify_deliver` row, not a spawned task: the runner released its lease
684 // on the way out and whoever starts next claims it. That is what replaced a
685 // best-effort five-second drain which still lost anything slower than it.
686 tokio::try_join!(acme, admin, metrics)?;
687 Ok(())
688}
689
690/// Serves `app` on one role's socket until the process shuts down.
691///
692/// One shape for all three roles, where there used to be a boxed future per
693/// listener per TLS arm: [`crate::listener::RoleSocket`] is the same type
694/// whether the role is speaking TLS, speaking cleartext or — a socket having
695/// been closed by a reload — not serving at all, so the four cases collapse into
696/// this one call. Its future lives for the process: a rebind replaces what is
697/// underneath it, never the `axum::serve` above.
698fn serve_role(
699 app: axum::Router,
700 socket: crate::listener::RoleSocket,
701 shutdown: tokio::sync::watch::Receiver<bool>,
702) -> impl Future<Output = std::io::Result<()>> + Send {
703 axum::serve(
704 socket,
705 app.into_make_service_with_connect_info::<SocketAddr>(),
706 )
707 .with_graceful_shutdown(on_shutdown(shutdown))
708 .into_future()
709}
710
711/// Everything one configuration generation contributes, built and validated
712/// before any of it is published.
713///
714/// The unit exists so startup and reload cannot drift: both go through
715/// [`build_generation`], so a subsystem added to one is added to the other by
716/// construction rather than by remembering.
717pub(crate) struct Generation {
718 /// Kept so startup can announce them; a reload drops them on the floor,
719 /// since `profile_mounted` is a lifecycle event and not a heartbeat.
720 profiles: Vec<Arc<Profile>>,
721 acme_app: axum::Router,
722 admin_app: Option<axum::Router>,
723 job_registry: crate::jobs::JobRegistry,
724 tls: Option<tls::TlsSettings>,
725 admin_tls: Option<tls::TlsSettings>,
726 /// The limiter this generation ended up with, for the next one to carry.
727 logins: Option<Arc<crate::webadmin::LoginLimiter>>,
728}
729
730/// Builds one generation: profiles, both routers, the job registry, the audit
731/// trail and both TLS acceptors.
732///
733/// Fallible throughout and side-effect-free on the *serving* state: nothing here
734/// touches a cell, so a failure leaves whatever is already running exactly as it
735/// was. That is what makes an atomic reload possible — everything is built
736/// first, and only a complete success publishes anything.
737///
738/// `dispatchers` is passed in rather than built here because a reload needs to
739/// hold it back: it is published to the long-lived [`crate::notify::Notifiers`]
740/// handle at swap time, *before* the routers, so a request served by the new
741/// generation cannot queue a delivery the job runner's map does not know.
742///
743/// Whether the panel is part of this generation is read from `admin.enabled`
744/// rather than from whether a socket exists. It used to be the latter, which
745/// was the same answer while the key was frozen and is the wrong one now that a
746/// reload can turn the panel on: the app, its TLS, its session sweep and its
747/// login limiter all have to appear in the generation *before* there is a
748/// listener to serve them.
749pub(crate) fn build_generation(
750 config: &Arc<Config>,
751 resolved: &[crate::config::ProfileConfig],
752 assembly: &crate::Assembly,
753 parts: &crate::GenerationParts,
754 previous_logins: Option<&crate::webadmin::LoginLimiter>,
755) -> anyhow::Result<Generation> {
756 let admin_enabled = config.admin.enabled;
757 let database = assembly.database.clone();
758 let profiles = Profile::build_all_with(config, resolved, parts)?;
759
760 let tls = tls::from_config(&config.server)
761 .inspect_err(|error| {
762 error!(event = "tls_init_failed", outcome = "failure", error = %error);
763 })?
764 .map(|acceptor| {
765 tls::TlsSettings::new(
766 acceptor,
767 Duration::from_millis(config.server.tls.handshake_timeout_ms),
768 )
769 });
770
771 let admin_tls = match admin_enabled {
772 false => None,
773 true => tls::admin_from_config(&config.admin)
774 .inspect_err(|error| {
775 error!(event = "admin_tls_init_failed", outcome = "failure", error = %error);
776 })?
777 .map(|acceptor| {
778 tls::TlsSettings::new(
779 acceptor,
780 Duration::from_millis(config.admin.tls.handshake_timeout_ms),
781 )
782 }),
783 };
784
785 // Every subsystem with background work, in one registry. **Nothing here
786 // registers a handler per backend**: the registry refuses a second handler
787 // for one kind outright, since two would each claim about half the rows, and
788 // two profiles over different `[signer]` sections are two backends that
789 // `build_backends` deliberately does not collapse. So each backend hands
790 // over *state* and one handler is built over all of it.
791 let mut job_registry = crate::jobs::JobRegistry::new();
792 // The CRL ledgers are collected once per distinct backend, since two
793 // profiles sharing one CA share one ledger and pruning it twice a day would
794 // be pointless work. The identity is kept as a `usize` rather than the
795 // pointer itself, so this function's caller stays `Send`; it is spawned.
796 let mut registered: Vec<usize> = Vec::new();
797 let mut pruners: Vec<Arc<dyn crate::signer::CrlPruner>> = Vec::new();
798 // The relay backends are collected per *profile*, because that is the key a
799 // job row is dispatched on — and because taking the profile list from the
800 // backend would take a stale one: a backend whose configuration did not
801 // move is reused verbatim across a reload, so a profile newly mounted onto
802 // it is not in any list it remembers.
803 let mut relays: Vec<(String, crate::signer::relay::RelayState)> = Vec::new();
804 for profile in &profiles {
805 relays.extend(
806 profile
807 .signer
808 .relay_state()
809 .map(|state| (profile.name.clone(), state)),
810 );
811 let identity = Arc::as_ptr(&profile.signer).cast::<()>() as usize;
812 if registered.contains(&identity) {
813 continue;
814 }
815 registered.push(identity);
816 pruners.extend(profile.signer.crl_pruner());
817 }
818 // The periodic CRL prune, over whichever CAs keep a ledger of their own.
819 // Registered only when there is one, the way the audit sweep is registered
820 // only for a non-zero retention.
821 if !pruners.is_empty() {
822 job_registry
823 .register(Arc::new(crate::signer::local_ca::sweep::CrlSweepJob::new(
824 pruners,
825 )))
826 .inspect_err(|error| {
827 error!(event = "job_registry_init_failed", outcome = "failure", error = %error);
828 })?;
829 }
830 // Relayed issuance, over every relay profile at once. Registered only when
831 // some profile relays, for `CrlSweepJob`'s reason: a deployment with none
832 // has no row of this kind to claim.
833 if !relays.is_empty() {
834 job_registry
835 .register(Arc::new(crate::signer::relay::flow::RelayJob::new(
836 database.clone(),
837 relays,
838 )))
839 .inspect_err(|error| {
840 error!(event = "job_registry_init_failed", outcome = "failure", error = %error);
841 })?;
842 }
843 // Notification delivery. The same shape as the two above and the reason it
844 // is: one handler for every profile, holding the whole
845 // `profile name -> dispatcher` map, with a job row naming its own profile.
846 // Registered unconditionally — a profile with no `[notify]`
847 // backends queues nothing, so the handler simply never claims a row, and
848 // making the registration conditional would mean a row queued before a
849 // configuration change had nobody to run it.
850 //
851 // It takes the *handle*, not this generation's map: the handler is
852 // registered per generation but must read whichever map is current, and a
853 // row queued by a reloaded router names a slot id only the new one has.
854 job_registry
855 .register(Arc::new(crate::notify::NotifyJob::new(
856 assembly.notifiers.clone(),
857 )))
858 .inspect_err(|error| {
859 error!(event = "job_registry_init_failed", outcome = "failure", error = %error);
860 })?;
861
862 // The expiry digest, registered only when some profile asked for one
863 // (`notify.expiry.lead_days`), the way `CrlSweepJob` is registered only
864 // when there is a ledger to prune. It takes the `Notifiers` handle rather
865 // than the profiles' own dispatchers for `NotifyJob`'s reason above, and
866 // the queue because its per-profile rows are something it maintains on
867 // every pass rather than only at `recover`.
868 if let Some(digest) = crate::notify::expiry::ExpiryDigestJob::from_profiles(
869 resolved,
870 assembly.notifiers.clone(),
871 database.clone(),
872 assembly.jobs.clone(),
873 ) {
874 job_registry
875 .register(Arc::new(digest))
876 .inspect_err(|error| {
877 error!(event = "job_registry_init_failed", outcome = "failure", error = %error);
878 })?;
879 }
880
881 // The periodic table sweeps. Each is one self-rescheduling row rather than
882 // its own interval loop, so a sweep that dies is reclaimed by lease expiry
883 // and its schedule survives a restart. Their `recover` is also the startup
884 // sweep — it queues at `run_at = now`, so the runner performs the first pass
885 // on its way into the loop and there is nothing to run separately here.
886 let ttl = Duration::from_secs(config.nonce.ttl_seconds);
887 let mut sweeps = vec![crate::jobs::SweepJob::nonces(database.clone(), ttl)];
888 // `0` keeps everything for ever on both of these, and is a handler not
889 // registered rather than a sweep with a cutoff at the epoch.
890 if config.audit.retention_days > 0 {
891 sweeps.push(crate::jobs::SweepJob::audit(
892 database.clone(),
893 config.audit.retention_days,
894 ));
895 }
896 if config.jobs.retention_days > 0 {
897 sweeps.push(crate::jobs::SweepJob::jobs(
898 database.clone(),
899 config.jobs.retention_days,
900 ));
901 }
902 // One handler covering every mounted profile, since the registry refuses a
903 // second handler for one kind. A profile keeping everything (`0`) is left
904 // out of the list rather than swept with a cutoff at the epoch.
905 let order_retention: Vec<(String, u64)> = resolved
906 .iter()
907 .filter(|profile| profile.sections.order.retention_days > 0)
908 .map(|profile| (profile.name.clone(), profile.sections.order.retention_days))
909 .collect();
910 if !order_retention.is_empty() {
911 sweeps.push(crate::jobs::SweepJob::orders(
912 database.clone(),
913 order_retention,
914 ));
915 }
916 if admin_enabled {
917 sweeps.push(crate::jobs::SweepJob::admin_sessions(
918 database.clone(),
919 Duration::from_secs(config.admin.session_idle_timeout_seconds),
920 config.admin.session_ttl_seconds,
921 ));
922 }
923 for sweep in sweeps {
924 job_registry
925 .register(Arc::new(sweep))
926 .inspect_err(|error| {
927 error!(event = "job_registry_init_failed", outcome = "failure", error = %error);
928 })?;
929 }
930
931 // The CA's audit trail: one per process, shared by every profile's router
932 // and by the web admin listener, because `[audit]` is process-wide. Built
933 // here rather than in `Profile::build_all` for exactly that reason — it is
934 // not a per-endpoint subsystem.
935 let auditor = Arc::new(
936 crate::audit::Auditor::from_config(
937 &config.audit,
938 &config.dns,
939 database.clone(),
940 // The registry the certificate counters land in. Carried by
941 // `Assembly`, so it is the same one across every generation and the
942 // same one `metrics_app` serves from.
943 assembly.metrics.clone(),
944 )
945 .inspect_err(|error| {
946 error!(event = "audit_init_failed", outcome = "failure", error = %error);
947 })?,
948 );
949
950 // Built **before** `build_app`, which consumes `profiles`. The admin state
951 // needs the same profiles (revoking an order resolves that order's own
952 // signer), and `build_admin_app` takes a slice precisely so the ordering
953 // is a signature constraint rather than a borrow error to rediscover.
954 let (admin_app, logins) = match admin_enabled {
955 false => (None, None),
956 true => {
957 let (router, logins) = crate::webadmin::build_admin_app_with_logins(
958 database.clone(),
959 config.clone(),
960 &profiles,
961 auditor.clone(),
962 previous_logins,
963 );
964 (Some(router), Some(logins))
965 }
966 };
967 let acme_app = build_app(
968 database,
969 config.clone(),
970 profiles.clone(),
971 auditor,
972 assembly.metrics.clone(),
973 );
974
975 Ok(Generation {
976 profiles,
977 acme_app,
978 admin_app,
979 job_registry,
980 tls,
981 admin_tls,
982 logins,
983 })
984}
985
986/// The cells one generation is published into.
987///
988/// Held by the supervisor and by nothing else. Every field is a `watch::Sender`,
989/// and `send_replace` is synchronous — so publishing a generation is a run of
990/// sends with no `.await` between them, which no other task can interleave with.
991/// That is what makes a reload atomic without a lock.
992struct Cells {
993 acme_router: tokio::sync::watch::Sender<axum::routing::RouterIntoService<axum::body::Body>>,
994 admin_router: tokio::sync::watch::Sender<axum::routing::RouterIntoService<axum::body::Body>>,
995 job_registry: tokio::sync::watch::Sender<Arc<crate::jobs::JobRegistry>>,
996 /// The runner's own pacing. Separate from the registry above because the two
997 /// reach it by different routes: the registry carries what a *handler*
998 /// captured, this carries what the *loop* re-reads each pass.
999 jobs: tokio::sync::watch::Sender<Arc<crate::config::JobsConfig>>,
1000 /// The three sockets. Each carries its role's TLS mode as well, since both
1001 /// are read by the same accept loop and both are published the same
1002 /// synchronous way — see [`crate::listener::ListenerHandle`].
1003 acme: crate::listener::ListenerHandle,
1004 admin: crate::listener::ListenerHandle,
1005 metrics: crate::listener::ListenerHandle,
1006}
1007
1008/// Serves reload requests for the life of the process.
1009///
1010/// One task, so reloads are serialised: two overlapping rebuilds could publish
1011/// their cells interleaved, and the second-newest generation would win some of
1012/// them. Ends when the last [`crate::reload::ReloadHandle`] is dropped, which is
1013/// what makes [`crate::reload::Reloads::none`] cost nothing.
1014async fn supervise_reloads(
1015 mut reloads: crate::reload::Reloads,
1016 mut config: Arc<Config>,
1017 mut resolved: Vec<crate::config::ProfileConfig>,
1018 assembly: Arc<crate::Assembly>,
1019 cells: Cells,
1020 mut logins: Option<Arc<crate::webadmin::LoginLimiter>>,
1021) {
1022 let mut generation: u64 = 1;
1023
1024 while let Some(request) = reloads.recv().await {
1025 let started = std::time::Instant::now();
1026 info!(
1027 event = "server_config_reload_requested",
1028 outcome = "progress",
1029 generation = generation,
1030 );
1031
1032 // The build phase runs on a blocking thread, and that is not a
1033 // precaution: `RelaySigner::from_config` contacts the upstream the first
1034 // time it is built for an account with no `kid` sidecar yet, on a scoped
1035 // OS thread it then *joins*. Mounting a relay profile by `SIGHUP` would
1036 // otherwise park a runtime worker for as long as
1037 // `signer.relay.poll_timeout_secs` allows — five minutes by default —
1038 // with every connection that worker was polling parked behind it. The
1039 // publish phase stays on this task, where its lack of an await point is
1040 // what makes a generation unobservable half-applied.
1041 let outcome = {
1042 let config = config.clone();
1043 let resolved = resolved.clone();
1044 let assembly = assembly.clone();
1045 let logins = logins.clone();
1046 tokio::task::spawn_blocking(move || {
1047 prepare_reload(&config, &resolved, &assembly, logins.as_deref())
1048 })
1049 .await
1050 .unwrap_or_else(|error| {
1051 Err(crate::reload::ReloadError::Build(format!(
1052 "the reload build task did not finish: {error}"
1053 )))
1054 })
1055 }
1056 .map(|prepared| {
1057 publish_reload(
1058 prepared,
1059 &config,
1060 &assembly,
1061 &cells,
1062 generation + 1,
1063 started,
1064 )
1065 });
1066
1067 match outcome {
1068 Ok(reloaded) => {
1069 let report = reloaded.report;
1070 config = reloaded.config;
1071 resolved = reloaded.resolved;
1072 logins = reloaded.logins;
1073 generation = report.generation;
1074 info!(
1075 event = "server_config_reloaded",
1076 outcome = "success",
1077 generation = report.generation,
1078 profiles = ?report.profiles,
1079 job_kinds = ?report.job_kinds,
1080 tls_reloaded = report.tls_reloaded,
1081 admin_tls_reloaded = report.admin_tls_reloaded,
1082 listeners_rebound = ?report.listeners_rebound,
1083 logging_reloaded = report.logging_reloaded,
1084 duration_ms = crate::millis(report.duration),
1085 );
1086 // After the reload's own line, and under the new configuration,
1087 // since that is what these describe. Each is the same
1088 // announcement startup makes for a listener that has just come
1089 // up — including the panel's two warnings, which is why this is
1090 // here rather than inside the synchronous publishing run.
1091 for (role, address) in reloaded.opened {
1092 match role {
1093 Role::Acme => info!(
1094 event = "server_listening",
1095 outcome = "success",
1096 bind_address = %address,
1097 protocol = if config.server.tls.enabled { "https" } else { "http" }
1098 ),
1099 Role::Admin => {
1100 announce_admin_listener(&config, &assembly.database, &address).await;
1101 }
1102 Role::Metrics => announce_metrics_listener(&address),
1103 }
1104 }
1105 // An endpoint this reload mounted really did come up, so it gets
1106 // the same announcement and the same notification startup makes
1107 // for one. An endpoint that was *already* mounted stays silent:
1108 // `profile_mounted` is a lifecycle event and not a heartbeat,
1109 // and re-firing it per `SIGHUP` would make the notify surface
1110 // noisiest in exactly the config-managed deployments that would
1111 // least want it. Here rather than in the publishing run because
1112 // dispatching reaches the database.
1113 for profile in reloaded.mounted {
1114 announce_profile(&profile).await;
1115 }
1116 if let Some(respond) = request.respond {
1117 let _ = respond.send(Ok(report));
1118 }
1119 }
1120 Err(error) => {
1121 // Two names, because they are two different things for whoever
1122 // is reading: a refusal is a configuration an operator must
1123 // change, a failure is one the server could not build.
1124 match &error {
1125 crate::reload::ReloadError::Frozen { .. } => warn!(
1126 event = "server_config_reload_refused",
1127 outcome = "failure",
1128 generation = generation,
1129 reason = error.kind(),
1130 error = %error,
1131 ),
1132 _ => error!(
1133 event = "server_config_reload_failed",
1134 outcome = "failure",
1135 generation = generation,
1136 reason = error.kind(),
1137 error = %error,
1138 ),
1139 }
1140 if let Some(respond) = request.respond {
1141 let _ = respond.send(Err(error));
1142 }
1143 }
1144 }
1145 }
1146}
1147
1148/// One of the three sockets this process may hold.
1149///
1150/// An enum rather than the `&'static str` the log field wants, so the reload
1151/// path's per-role handling is exhaustive: a fourth listener would be a compile
1152/// error at every point that has to decide something about one, which is exactly
1153/// how the third arrived with `bind_metrics` and `check_metrics_config` in
1154/// place and nothing else remembering it existed.
1155#[derive(Clone, Copy, PartialEq, Eq)]
1156enum Role {
1157 Acme,
1158 Admin,
1159 Metrics,
1160}
1161
1162impl Role {
1163 /// The `listener` field every log line about this socket carries.
1164 fn label(self) -> &'static str {
1165 match self {
1166 Self::Acme => "acme",
1167 Self::Admin => "admin",
1168 Self::Metrics => "metrics",
1169 }
1170 }
1171
1172 /// The key an operator edits to move this socket, for a refusal to name.
1173 fn bind_key(self) -> &'static str {
1174 match self {
1175 Self::Acme => "server.bind_address",
1176 Self::Admin => "admin.bind_address",
1177 Self::Metrics => "metrics.bind_address",
1178 }
1179 }
1180}
1181
1182/// What a reload does to one role's socket.
1183///
1184/// Built while a failure can still refuse the whole reload, applied once nothing
1185/// can fail — the same build-then-publish split every other part of a generation
1186/// makes, applied to the one resource that cannot simply be constructed twice.
1187enum SocketPlan {
1188 /// The role's address and enablement are both unchanged. Note this is
1189 /// decided from the *configuration*, never from the address actually bound:
1190 /// a caller supplying its own socket (every test that binds `127.0.0.1:0`)
1191 /// is entitled to one that does not match the file, and rebinding it out
1192 /// from under them would be this feature breaking its own callers.
1193 Keep,
1194 /// Serve this newly bound socket: the role was switched on, or its address
1195 /// moved. Connections already established are untouched — hyper owns those,
1196 /// and only the socket beneath them changes.
1197 Serve(TcpListener),
1198 /// Release the socket: the role was switched off.
1199 Close,
1200}
1201
1202/// The three roles' socket plans, and what to say about them afterwards.
1203struct SocketPlans {
1204 acme: SocketPlan,
1205 admin: SocketPlan,
1206 metrics: SocketPlan,
1207 /// The resolved address of each freshly bound socket, so the announcement
1208 /// after the swap names where the listener actually landed.
1209 bound: Vec<(Role, String)>,
1210}
1211
1212impl SocketPlans {
1213 /// The roles whose socket this reload moved, for [`ReloadReport`] — which is
1214 /// what a test waits on and what an operator greps.
1215 ///
1216 /// [`ReloadReport`]: crate::reload::ReloadReport
1217 fn rebound(&self) -> Vec<&'static str> {
1218 self.bound.iter().map(|(role, _)| role.label()).collect()
1219 }
1220
1221 /// Hands each socket to its accept loop. Synchronous and infallible, so it
1222 /// sits inside the publishing run beside the routers.
1223 fn publish(self, cells: &Cells) {
1224 for (role, plan, handle) in [
1225 (Role::Acme, self.acme, &cells.acme),
1226 (Role::Admin, self.admin, &cells.admin),
1227 (Role::Metrics, self.metrics, &cells.metrics),
1228 ] {
1229 match plan {
1230 SocketPlan::Keep => {}
1231 SocketPlan::Serve(listener) => handle.serve(listener),
1232 SocketPlan::Close => {
1233 handle.close();
1234 info!(
1235 event = "server_listener_stopped",
1236 outcome = "success",
1237 listener = role.label(),
1238 "switched off by a configuration reload: the socket is released \
1239 and nothing new is accepted on it"
1240 );
1241 }
1242 }
1243 }
1244 }
1245}
1246
1247/// Decides, and performs, every bind this reload needs.
1248///
1249/// The ordering rule the whole reload path rests on, applied to sockets: bind
1250/// first, so a bad address refuses the reload rather than having already
1251/// dropped the live one. Two addresses that differ as strings but collide in
1252/// the kernel — `[::]:3000` against `0.0.0.0:3000` — make that bind fail with
1253/// `EADDRINUSE`, which is the safe direction: the running socket is still
1254/// serving and the refusal names the key.
1255///
1256/// A `tls.enabled` flip does not appear here at all. The mode is read per
1257/// connection (see [`crate::listener`]), so turning TLS on or off keeps the
1258/// socket exactly where it is — which is what makes the one case a bind-first
1259/// scheme could not serve, an unchanged address, not a case.
1260fn plan_sockets(
1261 applied: &Config,
1262 proposed: &Config,
1263) -> Result<SocketPlans, crate::reload::ReloadError> {
1264 let mut bound = Vec::new();
1265 let mut plan = |role: Role,
1266 was: Option<&str>,
1267 now: Option<&str>|
1268 -> Result<SocketPlan, crate::reload::ReloadError> {
1269 match (was, now) {
1270 (None, None) => Ok(SocketPlan::Keep),
1271 (Some(_), None) => Ok(SocketPlan::Close),
1272 (Some(was), Some(now)) if was == now => Ok(SocketPlan::Keep),
1273 (_, Some(now)) => {
1274 let listener = crate::listener::bind_blocking(now).map_err(|error| {
1275 error!(event = "server_socket_bind_failed",
1276 outcome = "failure",
1277 listener = role.label(),
1278 bind_address = %now,
1279 error = %error);
1280 crate::reload::ReloadError::Build(format!(
1281 "`{}` is `{now}`, which cannot be bound: {error}",
1282 role.bind_key()
1283 ))
1284 })?;
1285 bound.push((role, bound_address(Some(&listener), now)));
1286 Ok(SocketPlan::Serve(listener))
1287 }
1288 }
1289 };
1290
1291 // The ACME listener is never switched off — there is no `server.enabled`,
1292 // and a CA serving no ACME would be a process with nothing to do.
1293 let acme = plan(
1294 Role::Acme,
1295 Some(&applied.server.bind_address),
1296 Some(&proposed.server.bind_address),
1297 )?;
1298 let admin = plan(
1299 Role::Admin,
1300 applied
1301 .admin
1302 .enabled
1303 .then_some(applied.admin.bind_address.as_str()),
1304 proposed
1305 .admin
1306 .enabled
1307 .then_some(proposed.admin.bind_address.as_str()),
1308 )?;
1309 let metrics = plan(
1310 Role::Metrics,
1311 applied
1312 .metrics
1313 .enabled
1314 .then_some(applied.metrics.bind_address.as_str()),
1315 proposed
1316 .metrics
1317 .enabled
1318 .then_some(proposed.metrics.bind_address.as_str()),
1319 )?;
1320
1321 Ok(SocketPlans {
1322 acme,
1323 admin,
1324 metrics,
1325 bound,
1326 })
1327}
1328
1329/// What one successful reload hands back to the supervisor: the report to log,
1330/// and the pieces of state the *next* reload compares against.
1331///
1332/// A struct rather than the tuple this used to return — which needed
1333/// `#[allow(clippy::type_complexity)]` and left the caller destructuring four
1334/// same-shaped values positionally, where swapping two would still compile.
1335/// The same move `ProfileParts` made, for the same reason.
1336struct Reloaded {
1337 report: crate::reload::ReloadReport,
1338 config: Arc<Config>,
1339 resolved: Vec<crate::config::ProfileConfig>,
1340 logins: Option<Arc<crate::webadmin::LoginLimiter>>,
1341 /// Each socket this reload bound, with the address it landed on. Announced
1342 /// by the supervisor rather than here, because saying a listener is up
1343 /// reaches the database (the panel's "nobody can sign in yet" warning) and
1344 /// [`publish_reload`] has no await point to spend on it — deliberately, that
1345 /// being what keeps its publishing run uninterruptible.
1346 opened: Vec<(Role, String)>,
1347 /// The endpoints this reload **added**, for the same reason and announced in
1348 /// the same place: `profile_mounted` is dispatched to the `[notify]`
1349 /// backends, which queues a job row.
1350 mounted: Vec<Arc<Profile>>,
1351}
1352
1353/// Everything a reload built and validated, waiting to be published.
1354///
1355/// The build/publish split is the whole shape of a reload, and making it two
1356/// values rather than two halves of one function buys the thing the split was
1357/// always claiming: [`prepare_reload`] can run wherever it likes — it runs on a
1358/// blocking thread, since building a `relay` backend can contact its upstream —
1359/// while [`publish_reload`] stays on the supervisor task, where having no await
1360/// point is what makes a generation unobservable half-applied.
1361struct Prepared {
1362 config: Arc<Config>,
1363 resolved: Vec<crate::config::ProfileConfig>,
1364 parts: crate::GenerationParts,
1365 generation: Generation,
1366 sockets: SocketPlans,
1367 logging: logging::PreparedLogging,
1368 /// Whether `RUST_LOG` is what the filter came from, so the publish phase can
1369 /// say when an edited `logging.filter` changed nothing, and which of the
1370 /// two outranking layers is why.
1371 logging_filter_source: logging::FilterSource,
1372 /// The endpoints in this generation that the previous one did not mount.
1373 mounted: Vec<Arc<Profile>>,
1374 /// The endpoints the previous generation mounted and this one does not.
1375 unmounted: Vec<String>,
1376}
1377
1378/// The build half of one reload: everything that can fail, and everything that
1379/// can block.
1380///
1381/// Nothing here touches a cell, so a failure anywhere leaves the running
1382/// generation exactly as it was — which is the property the whole "atomic,
1383/// refuse by name" decision exists for. Every socket this reload needs is bound
1384/// here too, where a port already in use is still a refusal rather than a
1385/// listener already dropped.
1386///
1387/// Run on a blocking thread by [`supervise_reloads`], because building a `relay`
1388/// backend for the first time contacts its upstream synchronously.
1389fn prepare_reload(
1390 config: &Arc<Config>,
1391 resolved: &[crate::config::ProfileConfig],
1392 assembly: &crate::Assembly,
1393 logins: Option<&crate::webadmin::LoginLimiter>,
1394) -> Result<Prepared, crate::reload::ReloadError> {
1395 use crate::reload::{Applied, ReloadError, check_frozen};
1396
1397 // Re-read from scratch: `Config::load` consults the file *and* the
1398 // `ACME_PROXY_*` environment, so a reload sees whatever the process would
1399 // see if it restarted right now.
1400 let next = Arc::new(Config::load().map_err(|error| ReloadError::Load(error.to_string()))?);
1401 let next_resolved = next
1402 .resolve_profiles()
1403 .map_err(|error| ReloadError::Load(error.to_string()))?;
1404
1405 check_frozen(
1406 &Applied {
1407 config,
1408 profiles: resolved,
1409 },
1410 &Applied {
1411 config: &next,
1412 profiles: &next_resolved,
1413 },
1414 )?;
1415
1416 // Built here rather than published here: a bad `logging.target` must refuse
1417 // the whole reload with the message startup would have printed, not leave a
1418 // half-swapped generation behind. The same build-then-publish split
1419 // `Assembly::build_parts` makes.
1420 // `flag_override()` is how a `--log-level` typed at startup survives a
1421 // `SIGHUP`: the stack is rebuilt from the file, so without re-reading it
1422 // the reload would silently demote the server to `logging.filter`.
1423 let logging = logging::prepare_logging(&next.logging, logging::flag_override())
1424 .map_err(ReloadError::Build)?;
1425 let logging_filter_source = logging.filter_source;
1426
1427 // The same validation startup runs before either socket binds, so a panel
1428 // that would refuse to start refuses to be reloaded into. It also compiles
1429 // every `admin.template_dir` override, which is what keeps a broken one a
1430 // failed reload rather than a 500 in a browser.
1431 crate::webadmin::check_config(&next).map_err(|error| ReloadError::Build(error.to_string()))?;
1432 // Its twin for the third listener: `webadmin::check_config` sees the
1433 // admin-versus-server pair, this one sees the two it cannot.
1434 check_metrics_config(&next).map_err(|error| ReloadError::Build(error.to_string()))?;
1435
1436 // Every socket this reload needs is bound **here**, where a failure is still
1437 // a refusal: a port already taken, an address that does not resolve, a
1438 // privileged port after a `setcap` was lost. Past the publish phase nothing
1439 // can fail, so the running listeners are never dropped for a configuration
1440 // that then turns out not to work.
1441 let sockets = plan_sockets(config, &next)?;
1442
1443 // The egress clients, the notification dispatchers and the signer backends.
1444 // The last is where a newly mounted endpoint gets a backend, a removed one's
1445 // is left out, and an edited `[signer]` is rebuilt over the live instance's
1446 // in-memory state — see `signer::build_backends`. It is also the one step
1447 // that can make a network call, hence this whole function's blocking thread.
1448 let parts = assembly
1449 .build_parts(&next_resolved, &next)
1450 .map_err(|error| ReloadError::Build(error.to_string()))?;
1451 let generation = build_generation(&next, &next_resolved, assembly, &parts, logins)
1452 .map_err(|error| ReloadError::Build(error.to_string()))?;
1453
1454 // Compared by name against what is running, not against what is written
1455 // down: `resolve_profiles` has already dropped every `enabled = false`
1456 // entry, so this is the set of endpoints actually served.
1457 let running: std::collections::HashSet<&str> = resolved
1458 .iter()
1459 .map(|profile| profile.name.as_str())
1460 .collect();
1461 let mounted = generation
1462 .profiles
1463 .iter()
1464 .filter(|profile| !running.contains(profile.name.as_str()))
1465 .cloned()
1466 .collect();
1467 let next_names: std::collections::HashSet<&str> = next_resolved
1468 .iter()
1469 .map(|profile| profile.name.as_str())
1470 .collect();
1471 let unmounted = resolved
1472 .iter()
1473 .map(|profile| profile.name.clone())
1474 .filter(|name| !next_names.contains(name.as_str()))
1475 .collect();
1476
1477 Ok(Prepared {
1478 config: next,
1479 resolved: next_resolved,
1480 parts,
1481 generation,
1482 sockets,
1483 logging,
1484 logging_filter_source,
1485 mounted,
1486 unmounted,
1487 })
1488}
1489
1490/// The publish half: infallible, synchronous, and uninterruptible.
1491///
1492/// There is no `.await` in here, and that is load-bearing rather than
1493/// incidental. `watch::Sender::send_replace` and
1494/// `mpsc::UnboundedSender::send` are both synchronous, so a run of them with no
1495/// await point between cannot be interleaved — no task can observe a generation
1496/// half-applied, and no lock is needed to say so.
1497///
1498/// The order matters in three places. `[logging]` goes **first**, because an
1499/// operator who raised the level did it to see what happens next, starting with
1500/// this reload's own completion line. The notifier map, the signer set and the
1501/// job registry go **before** the routers: a request served by the new
1502/// generation queues a `notify_deliver` row naming a slot id from the new
1503/// configuration, and a `NotifyJob` still holding the old map would retire it —
1504/// permanently, since an unknown backend id is a `Failed`, not a `Retry`. And
1505/// the TLS mode goes before the socket, so a freshly bound listener's very first
1506/// connection is already accepted under this generation's settings.
1507fn publish_reload(
1508 prepared: Prepared,
1509 applied: &Arc<Config>,
1510 assembly: &crate::Assembly,
1511 cells: &Cells,
1512 generation: u64,
1513 started: std::time::Instant,
1514) -> Reloaded {
1515 use crate::reload::ReloadReport;
1516
1517 let Prepared {
1518 config: next,
1519 resolved: next_resolved,
1520 parts,
1521 generation: built,
1522 sockets,
1523 logging,
1524 logging_filter_source,
1525 mounted,
1526 unmounted,
1527 } = prepared;
1528
1529 let logging_reloaded = logging::publish_logging(logging);
1530
1531 let report = ReloadReport {
1532 generation,
1533 profiles: built
1534 .profiles
1535 .iter()
1536 .map(|profile| profile.name.clone())
1537 .collect(),
1538 job_kinds: built.job_registry.kinds(),
1539 tls_reloaded: built.tls.is_some(),
1540 admin_tls_reloaded: built.admin_tls.is_some(),
1541 listeners_rebound: sockets.rebound(),
1542 logging_reloaded,
1543 duration: started.elapsed(),
1544 };
1545 let next_logins = built.logins.clone();
1546
1547 assembly.publish_notifiers(parts.dispatchers);
1548 // The set the *next* reload compares against, and the point at which the
1549 // backends this one dropped are finally released — after their replacements
1550 // were built and adopted their state, never before.
1551 assembly.publish_signers(parts.signers);
1552 cells
1553 .job_registry
1554 .send_replace(Arc::new(built.job_registry));
1555
1556 // `[jobs]` in its two halves, both synchronous and neither able to fail —
1557 // which is what lets them sit in this run rather than needing a build phase
1558 // of their own. The runner re-derives its pacing from the cell on its next
1559 // pass; `max_attempts` goes to the queue instead, because it is the enqueue
1560 // side that reads it, and it sets the budget for work queued from here on
1561 // rather than for the rows already waiting.
1562 cells.jobs.send_replace(Arc::new(next.jobs.clone()));
1563 assembly.jobs.set_max_attempts(next.jobs.max_attempts);
1564
1565 cells.acme.set_tls(built.tls);
1566 cells.admin.set_tls(built.admin_tls);
1567 let opened = sockets.bound.clone();
1568 sockets.publish(cells);
1569
1570 cells
1571 .acme_router
1572 .send_replace(built.acme_app.into_service::<axum::body::Body>());
1573 // An empty router when the panel is off, which is what a request arriving
1574 // on a connection established a moment before it was switched off now gets:
1575 // closing the socket stops the next client, and this stops that one.
1576 cells.admin_router.send_replace(
1577 built
1578 .admin_app
1579 .unwrap_or_default()
1580 .into_service::<axum::body::Body>(),
1581 );
1582
1583 // Said here rather than by the supervisor because it needs nothing but a
1584 // name, unlike the mounting half, which dispatches a notification.
1585 for profile in unmounted {
1586 warn!(
1587 event = "profile_unmounted",
1588 outcome = "advisory",
1589 profile = %profile,
1590 "the endpoint is no longer served: its accounts and orders stay in the \
1591 database and come back if it is mounted again, but any issuance still in \
1592 flight for it has no handler left to finish it"
1593 );
1594 }
1595
1596 // `--log-level` and `RUST_LOG` both outrank `logging.filter` on a reload
1597 // exactly as they do at startup — the two disagreeing would be worse — but
1598 // that makes an edited `logging.filter` a silent no-op, which is the one
1599 // outcome an operator would read as "my reload did not land". Said only
1600 // when both halves hold: something outranked the file, *and* the file's
1601 // filter actually moved. `source` names which, since the two are unset in
1602 // different places.
1603 if logging_filter_source.outranks_config() && applied.logging.filter != next.logging.filter {
1604 warn!(
1605 event = "server_logging_filter_overridden",
1606 outcome = "advisory",
1607 source = logging_filter_source.as_str(),
1608 configured = %next.logging.filter,
1609 );
1610 }
1611
1612 Reloaded {
1613 report,
1614 config: next,
1615 resolved: next_resolved,
1616 logins: next_logins,
1617 opened,
1618 mounted,
1619 }
1620}
1621
1622/// The one announcement an endpoint that has just come up makes: a log line and
1623/// a `[notify]` lifecycle event.
1624///
1625/// Shared by startup and by a reload that mounted a new endpoint, so the two
1626/// cannot drift — before the profile set could reload there was only one caller
1627/// and the sharing was not needed.
1628async fn announce_profile(profile: &Arc<Profile>) {
1629 info!(
1630 event = "profile_mounted",
1631 outcome = "success",
1632 profile = %profile.name,
1633 directory = %profile.directory_url(),
1634 challenge_bypass = profile.challenges.is_bypassed(),
1635 eab_enabled = profile.eab.enabled
1636 );
1637 profile
1638 .notify
1639 .dispatch(crate::notify::NotifyEvent::ProfileMounted(
1640 crate::notify::ProfileMountedData {
1641 profile: profile.name.clone(),
1642 },
1643 ))
1644 .await;
1645}
1646
1647/// A future that completes when the shutdown relay fires.
1648async fn on_shutdown(mut receiver: tokio::sync::watch::Receiver<bool>) {
1649 // An error means the sender was dropped, which only happens when the relay
1650 // task itself is gone — treat it as "shut down" rather than parking
1651 // forever.
1652 let _ = receiver.wait_for(|ready| *ready).await;
1653}
1654
1655/// Says the panel is up, and warns about the two states that make it useless.
1656///
1657/// Run whenever the admin listener **opens** — at startup, and again on a
1658/// reload that turns `admin.enabled` on. Separate from the serving path since
1659/// that no longer starts or stops per role: the socket is what comes and goes,
1660/// and this is what an operator needs told when it does.
1661async fn announce_admin_listener(config: &Arc<Config>, database: &Arc<Database>, bound: &str) {
1662 // A listener nobody holds an account for is a running service with no way
1663 // in; say so once, naming the command that fixes it.
1664 if crate::sqlite::admin_user::AdminUser::list_all(database)
1665 .await
1666 .is_ok_and(|users| users.is_empty())
1667 {
1668 warn!(
1669 event = "admin_no_users",
1670 outcome = "advisory",
1671 "the web admin is enabled but has no operators: create one with \
1672 `acme-proxy admin user create <username>`"
1673 );
1674 }
1675
1676 // Repeated on every start while it holds, the `challenge_validation_bypassed`
1677 // treatment: these operators can still sign in, they are simply made to
1678 // enrol before their session becomes usable, and that stays worth seeing
1679 // for exactly as long as it is true.
1680 if config.admin.require_mfa
1681 && let Ok(count) = crate::admin::mfa::operators_without_a_factor(database.clone()).await
1682 && count > 0
1683 {
1684 warn!(
1685 event = "admin_mfa_enrolment_pending",
1686 outcome = "advisory",
1687 count = count,
1688 "admin.require_mfa is on and some operators have no second factor: \
1689 their next sign-in will require enrolment before the session is usable"
1690 );
1691 }
1692
1693 // Swept once now, then on an interval: sessions outlive a restart, so a
1694 // startup-only sweep would leak every one an operator never signed out of.
1695 let idle = Duration::from_secs(config.admin.session_idle_timeout_seconds);
1696 if let Err(error) = crate::sqlite::admin_session::AdminSession::cleanup(idle, database).await {
1697 error!(event = "admin_session_cleanup_failed", outcome = "failure", error = %error);
1698 }
1699
1700 // The **resolved** address, so a `:0` bind is discoverable.
1701 info!(
1702 event = "admin_listening",
1703 outcome = "success",
1704 bind_address = %bound,
1705 protocol = if config.admin.tls.enabled { "https" } else { "http" },
1706 base_url = %config.admin.base_url
1707 );
1708}
1709
1710/// The metrics listener's own one-line announcement, and its standing warning.
1711fn announce_metrics_listener(bound: &str) {
1712 info!(
1713 event = "metrics_listening",
1714 outcome = "success",
1715 bind_address = %bound,
1716 "unauthenticated by design: the port is the boundary, so firewall it"
1717 );
1718}
1719
1720/// The address a socket ended up on, falling back to what was configured.
1721///
1722/// The two differ for `:0` and for a caller that supplied its own listener; the
1723/// resolved one is what an operator needs, and the configured one is all there
1724/// is to say when the socket cannot answer.
1725fn bound_address(listener: Option<&TcpListener>, configured: &str) -> String {
1726 listener
1727 .and_then(|listener| listener.local_addr().ok())
1728 .map_or_else(|| configured.to_string(), |address| address.to_string())
1729}
1730
1731async fn shutdown_signal() {
1732 let ctrl_c = async {
1733 tokio::signal::ctrl_c()
1734 .await
1735 .expect("failed to install Ctrl+C handler");
1736 };
1737
1738 #[cfg(unix)]
1739 let terminate = async {
1740 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
1741 .expect("failed to install signal handler")
1742 .recv()
1743 .await;
1744 };
1745
1746 #[cfg(not(unix))]
1747 let terminate = std::future::pending::<()>();
1748
1749 tokio::select! {
1750 _ = ctrl_c => {},
1751 _ = terminate => {},
1752 }
1753}
1754
1755/// Aborts a background task when it goes out of scope.
1756struct AbortOnDrop(tokio::task::JoinHandle<()>);
1757
1758impl Drop for AbortOnDrop {
1759 fn drop(&mut self) {
1760 self.0.abort();
1761 }
1762}
1763
1764#[cfg(test)]
1765mod tests {
1766 use super::*;
1767
1768 /// `--version` exists and reports the crate version. The bug report
1769 /// template tells people to run it, and clap generates the flag only
1770 /// because `#[command(version = …)]` says so — drop that and the first
1771 /// instruction on the form starts erroring out.
1772 #[test]
1773 fn version_flag_reports_the_crate_version() {
1774 let Err(error) = Cli::try_parse_from(["acme-proxy", "--version"]) else {
1775 panic!("--version parsed as a command rather than printing a version");
1776 };
1777 assert_eq!(error.kind(), clap::error::ErrorKind::DisplayVersion);
1778 assert!(error.to_string().contains(env!("CARGO_PKG_VERSION")));
1779 }
1780
1781 /// `--log-level` is global like `--yes` and `--color`, so it may be given
1782 /// on either side of the subcommand — which is the whole reason an
1783 /// operator reaches for it, having already typed the command once.
1784 #[test]
1785 fn log_level_is_a_global_flag_with_a_closed_set_of_values() {
1786 let cli = Cli::try_parse_from(["acme-proxy", "account", "list"]).unwrap();
1787 assert_eq!(
1788 cli.log_level, None,
1789 "absent by default: an admin command says nothing unless asked",
1790 );
1791
1792 for argv in [
1793 ["acme-proxy", "--log-level", "debug", "account", "list"],
1794 ["acme-proxy", "account", "list", "--log-level", "debug"],
1795 ] {
1796 let cli = Cli::try_parse_from(argv).unwrap();
1797 assert_eq!(cli.log_level, Some(LogLevel::Debug), "{argv:?}");
1798 }
1799
1800 let cli = Cli::try_parse_from(["acme-proxy", "serve", "--log-level", "off"]).unwrap();
1801 assert_eq!(cli.log_level, Some(LogLevel::Off));
1802
1803 // A `value_enum`, so an unrecognised level is refused with the six
1804 // spellings listed rather than treated as a filter directive.
1805 let Err(error) =
1806 Cli::try_parse_from(["acme-proxy", "account", "list", "--log-level", "loud"])
1807 else {
1808 panic!("`--log-level loud` must be refused");
1809 };
1810 assert_eq!(error.kind(), clap::error::ErrorKind::InvalidValue);
1811 }
1812
1813 #[test]
1814 fn parse_cli_subcommands() {
1815 let cli = Cli::try_parse_from(["acme-proxy"]).unwrap();
1816 assert!(cli.command.is_none());
1817
1818 let cli = Cli::try_parse_from(["acme-proxy", "serve"]).unwrap();
1819 assert!(matches!(cli.command, Some(Command::Serve)));
1820
1821 let cli = Cli::try_parse_from(["acme-proxy", "account", "list", "--json"]).unwrap();
1822 assert!(matches!(
1823 cli.command,
1824 Some(Command::Account {
1825 command: AccountCommand::List {
1826 json: true,
1827 profile: None,
1828 limit: window::DEFAULT_LIMIT,
1829 offset: 0
1830 }
1831 })
1832 ));
1833
1834 let cli = Cli::try_parse_from(["acme-proxy", "account", "show", "acct-1"]).unwrap();
1835 assert!(matches!(
1836 cli.command,
1837 Some(Command::Account {
1838 command: AccountCommand::Show { id, json: false }
1839 }) if id == "acct-1"
1840 ));
1841
1842 let cli = Cli::try_parse_from([
1843 "acme-proxy",
1844 "account",
1845 "update-contact",
1846 "acct-1",
1847 "--contact",
1848 "mailto:test@example.com",
1849 ])
1850 .unwrap();
1851 assert!(matches!(
1852 cli.command,
1853 Some(Command::Account {
1854 command: AccountCommand::UpdateContact { id, contact }
1855 }) if id == "acct-1" && contact == vec!["mailto:test@example.com"]
1856 ));
1857
1858 let cli = Cli::try_parse_from(["acme-proxy", "account", "deactivate", "acct-1"]).unwrap();
1859 assert!(matches!(
1860 cli.command,
1861 Some(Command::Account {
1862 command: AccountCommand::Deactivate { id }
1863 }) if id == "acct-1"
1864 ));
1865
1866 let cli = Cli::try_parse_from(["acme-proxy", "-y", "account", "delete", "acct-1"]).unwrap();
1867 assert!(cli.yes);
1868 assert!(matches!(
1869 cli.command,
1870 Some(Command::Account {
1871 command: AccountCommand::Delete { id }
1872 }) if id == "acct-1"
1873 ));
1874
1875 let cli = Cli::try_parse_from([
1876 "acme-proxy",
1877 "order",
1878 "list",
1879 "--account-id",
1880 "acct-1",
1881 "--status",
1882 "pending",
1883 "--json",
1884 ])
1885 .unwrap();
1886 assert!(matches!(
1887 cli.command,
1888 Some(Command::Order {
1889 command: OrderCommand::List {
1890 profile: None,
1891 account_id: Some(a),
1892 status: Some(s),
1893 expiring_in: None,
1894 hide_superseded: false,
1895 limit: window::DEFAULT_LIMIT,
1896 offset: 0,
1897 json: true
1898 }
1899 }) if a == "acct-1" && s == "pending"
1900 ));
1901
1902 let cli = Cli::try_parse_from([
1903 "acme-proxy",
1904 "order",
1905 "list",
1906 "--expiring-in",
1907 "30",
1908 "--hide-superseded",
1909 ])
1910 .unwrap();
1911 assert!(matches!(
1912 cli.command,
1913 Some(Command::Order {
1914 command: OrderCommand::List {
1915 expiring_in: Some(30),
1916 hide_superseded: true,
1917 status: None,
1918 account_id: None,
1919 profile: None,
1920 limit: window::DEFAULT_LIMIT,
1921 offset: 0,
1922 json: false
1923 }
1924 })
1925 ));
1926
1927 let cli = Cli::try_parse_from(["acme-proxy", "order", "show", "ord-1"]).unwrap();
1928 assert!(matches!(
1929 cli.command,
1930 Some(Command::Order {
1931 command: OrderCommand::Show { id, json: false }
1932 }) if id == "ord-1"
1933 ));
1934
1935 let cli = Cli::try_parse_from(["acme-proxy", "order", "delete", "ord-1"]).unwrap();
1936 assert!(matches!(
1937 cli.command,
1938 Some(Command::Order {
1939 command: OrderCommand::Delete { id }
1940 }) if id == "ord-1"
1941 ));
1942
1943 let cli = Cli::try_parse_from(["acme-proxy", "order", "revoke", "ord-1", "--reason", "1"])
1944 .unwrap();
1945 assert!(matches!(
1946 cli.command,
1947 Some(Command::Order {
1948 command: OrderCommand::Revoke { id, reason: Some(1) }
1949 }) if id == "ord-1"
1950 ));
1951
1952 let cli =
1953 Cli::try_parse_from(["acme-proxy", "nonce", "cleanup", "--ttl-seconds", "60"]).unwrap();
1954 assert!(matches!(
1955 cli.command,
1956 Some(Command::Nonce {
1957 command: NonceCommand::Cleanup {
1958 ttl_seconds: Some(60)
1959 }
1960 })
1961 ));
1962
1963 let cli = Cli::try_parse_from([
1964 "acme-proxy",
1965 "eab",
1966 "create",
1967 "--label",
1968 "test-key",
1969 "--json",
1970 ])
1971 .unwrap();
1972 assert!(matches!(
1973 cli.command,
1974 Some(Command::Eab {
1975 command: EabCommand::Create { label: Some(l), profile: None, json: true }
1976 }) if l == "test-key"
1977 ));
1978
1979 let cli = Cli::try_parse_from(["acme-proxy", "eab", "list"]).unwrap();
1980 assert!(matches!(
1981 cli.command,
1982 Some(Command::Eab {
1983 command: EabCommand::List {
1984 limit: 50,
1985 offset: 0,
1986 json: false
1987 }
1988 })
1989 ));
1990
1991 // The four commands added with `TODO.md`'s "last few asymmetries": a
1992 // detail for the one listable object that had none, and the three reads
1993 // the panel could already answer and the host could not.
1994 let cli = Cli::try_parse_from(["acme-proxy", "order", "chain", "ord-1"]).unwrap();
1995 assert!(matches!(
1996 cli.command,
1997 Some(Command::Order {
1998 command: OrderCommand::Chain { id }
1999 }) if id == "ord-1"
2000 ));
2001
2002 let cli = Cli::try_parse_from(["acme-proxy", "nonce", "count", "--json"]).unwrap();
2003 assert!(matches!(
2004 cli.command,
2005 Some(Command::Nonce {
2006 command: NonceCommand::Count { json: true }
2007 })
2008 ));
2009
2010 let cli = Cli::try_parse_from(["acme-proxy", "profile", "list"]).unwrap();
2011 assert!(matches!(
2012 cli.command,
2013 Some(Command::Profile {
2014 command: ProfileCommand::List { json: false }
2015 })
2016 ));
2017
2018 let cli = Cli::try_parse_from(["acme-proxy", "admin", "user", "show", "alice"]).unwrap();
2019 assert!(matches!(
2020 cli.command,
2021 Some(Command::Admin {
2022 command: AdminCommand::User {
2023 command: crate::cli::webadmin::AdminUserCommand::Show { username, json: false }
2024 }
2025 }) if username == "alice"
2026 ));
2027
2028 // The window the three formerly unwindowed listings grew, defaulted the
2029 // same way as the four that already had one.
2030 let cli =
2031 Cli::try_parse_from(["acme-proxy", "admin", "user", "list", "--limit", "2"]).unwrap();
2032 assert!(matches!(
2033 cli.command,
2034 Some(Command::Admin {
2035 command: AdminCommand::User {
2036 command: crate::cli::webadmin::AdminUserCommand::List {
2037 limit: 2,
2038 offset: 0,
2039 json: false
2040 }
2041 }
2042 })
2043 ));
2044
2045 let cli =
2046 Cli::try_parse_from(["acme-proxy", "admin", "session", "list", "--offset=5"]).unwrap();
2047 assert!(matches!(
2048 cli.command,
2049 Some(Command::Admin {
2050 command: AdminCommand::Session {
2051 command: crate::cli::webadmin::AdminSessionCommand::List {
2052 username: None,
2053 limit: window::DEFAULT_LIMIT,
2054 offset: 5,
2055 json: false
2056 }
2057 }
2058 })
2059 ));
2060
2061 let cli = Cli::try_parse_from(["acme-proxy", "eab", "show", "kid-1", "--json"]).unwrap();
2062 assert!(matches!(
2063 cli.command,
2064 Some(Command::Eab {
2065 command: EabCommand::Show { kid, json: true }
2066 }) if kid == "kid-1"
2067 ));
2068
2069 let cli = Cli::try_parse_from(["acme-proxy", "upstream", "register", "--eab-kid", "kid-1"])
2070 .unwrap();
2071 assert!(matches!(
2072 cli.command,
2073 Some(Command::Upstream {
2074 command: UpstreamCommand::Register { eab_kid: Some(kid), eab_hmac_key_file: None, profile: None }
2075 }) if kid == "kid-1"
2076 ));
2077
2078 // Registering against an upstream that needs no credential.
2079 let cli = Cli::try_parse_from(["acme-proxy", "upstream", "register"]).unwrap();
2080 assert!(matches!(
2081 cli.command,
2082 Some(Command::Upstream {
2083 command: UpstreamCommand::Register {
2084 eab_kid: None,
2085 eab_hmac_key_file: None,
2086 profile: None,
2087 }
2088 })
2089 ));
2090
2091 // The secret itself has no flag: it is stdin- or file-only, never argv.
2092 assert!(
2093 Cli::try_parse_from(["acme-proxy", "upstream", "register", "--eab-hmac-key", "s"])
2094 .is_err(),
2095 "an EAB secret must not be accepted on the command line"
2096 );
2097
2098 let cli = Cli::try_parse_from(["acme-proxy", "upstream", "show", "--json"]).unwrap();
2099 assert!(matches!(
2100 cli.command,
2101 Some(Command::Upstream {
2102 command: UpstreamCommand::Show {
2103 json: true,
2104 profile: None
2105 }
2106 })
2107 ));
2108
2109 let cli = Cli::try_parse_from(["acme-proxy", "eab", "revoke", "kid-1"]).unwrap();
2110 assert!(matches!(
2111 cli.command,
2112 Some(Command::Eab {
2113 command: EabCommand::Revoke { kid }
2114 }) if kid == "kid-1"
2115 ));
2116
2117 let cli = Cli::try_parse_from(["acme-proxy", "admin", "user", "create", "alice"]).unwrap();
2118 assert!(matches!(
2119 cli.command,
2120 Some(Command::Admin {
2121 command: AdminCommand::User {
2122 command: crate::cli::webadmin::AdminUserCommand::Create {
2123 username,
2124 password_file: None
2125 }
2126 }
2127 }) if username == "alice"
2128 ));
2129
2130 let cli = Cli::try_parse_from([
2131 "acme-proxy",
2132 "admin",
2133 "user",
2134 "passwd",
2135 "alice",
2136 "--password-file",
2137 "/run/secrets/pw",
2138 ])
2139 .unwrap();
2140 assert!(matches!(
2141 cli.command,
2142 Some(Command::Admin {
2143 command: AdminCommand::User {
2144 command: crate::cli::webadmin::AdminUserCommand::Passwd {
2145 username,
2146 password_file: Some(path)
2147 }
2148 }
2149 }) if username == "alice" && path == std::path::Path::new("/run/secrets/pw")
2150 ));
2151
2152 // The password itself has no flag, for the same reason the EAB secret
2153 // has none: argv is visible in `ps` and lands in shell history.
2154 for command in ["create", "passwd"] {
2155 assert!(
2156 Cli::try_parse_from([
2157 "acme-proxy",
2158 "admin",
2159 "user",
2160 command,
2161 "alice",
2162 "--password",
2163 "hunter2",
2164 ])
2165 .is_err(),
2166 "`admin user {command}` must not accept a password on the command line"
2167 );
2168 }
2169
2170 // `--color` is global like `--yes`, so it may sit anywhere on the line,
2171 // and an unknown value is refused by clap rather than falling back to
2172 // `auto` — the same rule `--status`/`--event` follow, for the same
2173 // reason: a silently ignored value looks exactly like a working one.
2174 let cli = Cli::try_parse_from(["acme-proxy", "account", "list", "--color", "never"])
2175 .expect("--color is global and accepts `never`");
2176 assert_eq!(cli.color, ColorChoice::Never);
2177
2178 let cli = Cli::try_parse_from(["acme-proxy", "--color", "always", "account", "list"])
2179 .expect("--color is global, so it may precede the subcommand");
2180 assert_eq!(cli.color, ColorChoice::Always);
2181
2182 assert_eq!(
2183 Cli::try_parse_from(["acme-proxy", "account", "list"])
2184 .unwrap()
2185 .color,
2186 ColorChoice::Auto,
2187 "unset means auto"
2188 );
2189
2190 assert!(
2191 Cli::try_parse_from(["acme-proxy", "account", "list", "--color", "sometimes"]).is_err(),
2192 "an unknown --color value must be refused, not ignored"
2193 );
2194
2195 let cli = Cli::try_parse_from(["acme-proxy", "admin", "user", "totp", "status", "alice"])
2196 .unwrap();
2197 assert!(matches!(
2198 cli.command,
2199 Some(Command::Admin {
2200 command: AdminCommand::User {
2201 command: crate::cli::webadmin::AdminUserCommand::Totp {
2202 command: crate::cli::webadmin::AdminUserTotpCommand::Status {
2203 username,
2204 json: false
2205 }
2206 }
2207 }
2208 }) if username == "alice"
2209 ));
2210
2211 let cli = Cli::try_parse_from([
2212 "acme-proxy",
2213 "admin",
2214 "user",
2215 "totp",
2216 "recovery-codes",
2217 "alice",
2218 ])
2219 .unwrap();
2220 assert!(matches!(
2221 cli.command,
2222 Some(Command::Admin {
2223 command: AdminCommand::User {
2224 command: crate::cli::webadmin::AdminUserCommand::Totp {
2225 command: crate::cli::webadmin::AdminUserTotpCommand::RecoveryCodes {
2226 username
2227 }
2228 }
2229 }
2230 }) if username == "alice"
2231 ));
2232
2233 // There is no `enrol` from a terminal, deliberately: it would put the
2234 // base32 secret in scrollback and shell history. See the doc comment on
2235 // `AdminUserTotpCommand`.
2236 assert!(
2237 Cli::try_parse_from(["acme-proxy", "admin", "user", "totp", "enrol", "alice"]).is_err()
2238 );
2239
2240 let cli =
2241 Cli::try_parse_from(["acme-proxy", "-y", "admin", "user", "delete", "alice"]).unwrap();
2242 assert!(cli.yes);
2243 assert!(matches!(
2244 cli.command,
2245 Some(Command::Admin {
2246 command: AdminCommand::User {
2247 command: crate::cli::webadmin::AdminUserCommand::Delete { username }
2248 }
2249 }) if username == "alice"
2250 ));
2251
2252 let cli =
2253 Cli::try_parse_from(["acme-proxy", "admin", "session", "list", "--json"]).unwrap();
2254 assert!(matches!(
2255 cli.command,
2256 Some(Command::Admin {
2257 command: AdminCommand::Session {
2258 command: crate::cli::webadmin::AdminSessionCommand::List {
2259 username: None,
2260 limit: 50,
2261 offset: 0,
2262 json: true
2263 }
2264 }
2265 })
2266 ));
2267
2268 // `--user` and `--all` answer the same question two ways; clap refuses
2269 // both rather than letting one silently win.
2270 assert!(
2271 Cli::try_parse_from([
2272 "acme-proxy",
2273 "admin",
2274 "session",
2275 "revoke",
2276 "--user",
2277 "alice",
2278 "--all",
2279 ])
2280 .is_err(),
2281 "--user and --all are mutually exclusive"
2282 );
2283 }
2284
2285 #[test]
2286 fn a_database_error_renders_as_a_cli_error() {
2287 let error = CliError::from(sqlx::Error::PoolClosed);
2288 assert!(error.to_string().starts_with("database error: "), "{error}");
2289 }
2290
2291 /// Every arm reaches its command handler. `Serve` is deliberately absent —
2292 /// it owns a socket, and [`serve_on`] is what the tests below drive.
2293 #[tokio::test]
2294 async fn dispatch_routes_each_command() {
2295 let database = Arc::new(Database::connect_in_memory().await.unwrap());
2296 let config = Arc::new(Config::default());
2297 let mut reader: &[u8] = &[];
2298
2299 let commands = vec![
2300 Command::Account {
2301 command: AccountCommand::List {
2302 profile: None,
2303 limit: window::DEFAULT_LIMIT,
2304 offset: 0,
2305 json: false,
2306 },
2307 },
2308 Command::Order {
2309 command: OrderCommand::List {
2310 profile: None,
2311 account_id: None,
2312 status: None,
2313 expiring_in: None,
2314 hide_superseded: false,
2315 limit: window::DEFAULT_LIMIT,
2316 offset: 0,
2317 json: false,
2318 },
2319 },
2320 Command::Nonce {
2321 command: NonceCommand::Cleanup {
2322 ttl_seconds: Some(1),
2323 },
2324 },
2325 Command::Nonce {
2326 command: NonceCommand::Count { json: false },
2327 },
2328 Command::Eab {
2329 command: EabCommand::List {
2330 limit: 50,
2331 offset: 0,
2332 json: false,
2333 },
2334 },
2335 Command::Man,
2336 Command::Completions {
2337 shell: clap_complete::aot::Shell::Bash,
2338 },
2339 // `Profile` is deliberately absent for `Upstream`'s reason below,
2340 // arrived at from the other end: it resolves the profiles, and
2341 // `Config::default()` mounts none, so it reports that rather than
2342 // listing nothing. Its arm is driven from `cli::profile`'s own
2343 // tests, against a configuration that has some.
2344 // `Upstream` is deliberately absent: it acts on a *profile's*
2345 // `[signer.relay]`, and this config has none, so it now
2346 // reports that rather than silently reading the global base
2347 // section nothing serves from. Covered in `cli::upstream`'s own
2348 // tests, which supply a configuration with profiles.
2349 ];
2350 for command in commands {
2351 dispatch(
2352 Some(command),
2353 true,
2354 ColorChoice::Never,
2355 &mut reader,
2356 &config,
2357 database.clone(),
2358 )
2359 .await
2360 .expect("every command must succeed against an empty database");
2361 }
2362 }
2363
2364 /// A failing command's message reaches [`dispatch`]'s caller rather than
2365 /// exiting the process where it was raised.
2366 #[tokio::test]
2367 async fn dispatch_propagates_a_command_failure() {
2368 let database = Arc::new(Database::connect_in_memory().await.unwrap());
2369 let config = Arc::new(Config::default());
2370 let mut reader: &[u8] = &[];
2371
2372 let error = dispatch(
2373 Some(Command::Account {
2374 command: AccountCommand::Show {
2375 id: "acct-nope".to_string(),
2376 json: false,
2377 },
2378 }),
2379 true,
2380 ColorChoice::Never,
2381 &mut reader,
2382 &config,
2383 database,
2384 )
2385 .await
2386 .expect_err("an unknown account must fail");
2387 assert_eq!(error, CliError("no such account: acct-nope".to_string()));
2388 }
2389
2390 mod serving {
2391 use super::*;
2392 use tokio::io::{AsyncReadExt, AsyncWriteExt};
2393 use tokio::net::TcpStream;
2394
2395 /// A single-profile configuration whose CA and TLS material all live
2396 /// under `dir`, so a test never touches the repository.
2397 fn config_in(dir: impl AsRef<std::path::Path>, tls: bool) -> Config {
2398 let dir = dir.as_ref();
2399 let _lock = crate::config::ENV_LOCK
2400 .lock()
2401 .unwrap_or_else(std::sync::PoisonError::into_inner);
2402 let ca = dir.join("ca");
2403 let body = format!(
2404 r#"
2405 [server]
2406 bind_address = "127.0.0.1:0"
2407 base_url = "http://localhost:3000"
2408
2409 [server.tls]
2410 enabled = {tls}
2411 cert_path = "{dir}/server.pem"
2412 key_path = "{dir}/server.key"
2413
2414 [profiles.default]
2415 signer.local_ca.cert_path = "{ca}.pem"
2416 signer.local_ca.key_path = "{ca}.key"
2417 signer.local_ca.crl_path = "{ca}.crl"
2418 "#,
2419 dir = dir.display(),
2420 ca = ca.display(),
2421 );
2422 std::fs::write(dir.join("config.toml"), body).unwrap();
2423 // SAFETY: the lock above makes this the only thread touching the
2424 // environment, and the variable is removed before returning.
2425 unsafe {
2426 std::env::set_var("ACME_PROXY_CONFIG", dir.join("config").to_str().unwrap());
2427 }
2428 let config = Config::load().expect("the configuration must load");
2429 unsafe {
2430 std::env::remove_var("ACME_PROXY_CONFIG");
2431 }
2432 config
2433 }
2434
2435 /// Two profiles, each relaying to an upstream of its own.
2436 ///
2437 /// The two `[signer]` sections must genuinely differ — different
2438 /// `directory_url` *and* `account_key_path` — or `build_backends`
2439 /// collapses them to one shared backend and the case under test never
2440 /// arises. `signer_paths` would refuse a shared account key outright.
2441 fn two_relay_profiles(
2442 dir: impl AsRef<std::path::Path>,
2443 first: &crate::signer::relay::testsrv::Upstream,
2444 second: &crate::signer::relay::testsrv::Upstream,
2445 ) -> Config {
2446 let dir = dir.as_ref();
2447 let _lock = crate::config::ENV_LOCK
2448 .lock()
2449 .unwrap_or_else(std::sync::PoisonError::into_inner);
2450 let body = format!(
2451 r#"
2452 [server]
2453 bind_address = "127.0.0.1:0"
2454 base_url = "http://localhost:3000"
2455
2456 [profiles.first]
2457 signer.backend = "relay"
2458 signer.relay.directory_url = "{first_url}"
2459 signer.relay.account_key_path = "{dir}/first.key"
2460
2461 [profiles.second]
2462 signer.backend = "relay"
2463 signer.relay.directory_url = "{second_url}"
2464 signer.relay.account_key_path = "{dir}/second.key"
2465 "#,
2466 first_url = first.directory_url(),
2467 second_url = second.directory_url(),
2468 dir = dir.display(),
2469 );
2470 std::fs::write(dir.join("config.toml"), body).unwrap();
2471 // SAFETY: the lock above makes this the only thread touching the
2472 // environment, and the variable is removed before returning.
2473 unsafe {
2474 std::env::set_var("ACME_PROXY_CONFIG", dir.join("config").to_str().unwrap());
2475 }
2476 let config = Config::load().unwrap();
2477 unsafe {
2478 std::env::remove_var("ACME_PROXY_CONFIG");
2479 }
2480 config
2481 }
2482
2483 fn temp_dir() -> crate::testutil::TempDir {
2484 crate::testutil::TempDir::new("serve")
2485 }
2486
2487 /// Boots `serve_on` on an ephemeral loopback port and returns it with
2488 /// the handle and the trigger that shuts it back down.
2489 async fn boot(
2490 config: Config,
2491 ) -> (
2492 SocketAddr,
2493 tokio::sync::oneshot::Sender<()>,
2494 tokio::task::JoinHandle<anyhow::Result<()>>,
2495 ) {
2496 let database = Arc::new(Database::connect_in_memory().await.unwrap());
2497 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2498 let addr = listener.local_addr().unwrap();
2499 let (tx, rx) = tokio::sync::oneshot::channel::<()>();
2500 let handle = tokio::spawn(serve_on(Arc::new(config), database, listener, async {
2501 let _ = rx.await;
2502 }));
2503 (addr, tx, handle)
2504 }
2505
2506 /// The cleartext path: real socket, real router, real graceful
2507 /// shutdown. `/health` is a root route, so this also proves the app
2508 /// `serve_on` assembles is the one `build_app` produces.
2509 #[tokio::test]
2510 async fn a_cleartext_server_answers_then_shuts_down() {
2511 let dir = temp_dir();
2512 let (addr, shutdown, handle) = boot(config_in(&dir, false)).await;
2513
2514 let mut stream = TcpStream::connect(addr).await.unwrap();
2515 stream
2516 .write_all(b"GET /health HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
2517 .await
2518 .unwrap();
2519 let mut response = String::new();
2520 stream.read_to_string(&mut response).await.unwrap();
2521 assert!(response.starts_with("HTTP/1.1 200 OK"), "{response}");
2522
2523 shutdown.send(()).unwrap();
2524 handle
2525 .await
2526 .unwrap()
2527 .expect("a clean shutdown is not an error");
2528 }
2529
2530 /// The same path with `server.tls.enabled`, which swaps the listener
2531 /// for a `TlsListener` — a different `axum::serve` arm, and the only
2532 /// place `TlsListener::spawn` is wired up in production.
2533 #[tokio::test]
2534 async fn a_tls_server_answers_over_a_real_handshake() {
2535 use tokio_rustls::TlsConnector;
2536 use tokio_rustls::rustls::pki_types::ServerName;
2537
2538 let dir = temp_dir();
2539 let (addr, shutdown, handle) = boot(config_in(&dir, true)).await;
2540
2541 // The generated certificate is self-signed, so the client must not
2542 // try to verify it — the point here is the listener, not the trust
2543 // chain.
2544 let client =
2545 crate::challenge::tls_alpn_01::accept_any_client_config(&[b"http/1.1"]).unwrap();
2546 let stream = TcpStream::connect(addr).await.unwrap();
2547 let mut tls = TlsConnector::from(client)
2548 .connect(ServerName::try_from("localhost").unwrap(), stream)
2549 .await
2550 .unwrap();
2551 tls.write_all(b"GET /health HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
2552 .await
2553 .unwrap();
2554 let mut response = String::new();
2555 tls.read_to_string(&mut response).await.unwrap();
2556 assert!(response.starts_with("HTTP/1.1 200 OK"), "{response}");
2557
2558 shutdown.send(()).unwrap();
2559 handle
2560 .await
2561 .unwrap()
2562 .expect("a clean shutdown is not an error");
2563 }
2564
2565 /// The three-listener path: one shutdown signal, three sockets, and
2566 /// each one serving only what belongs to it.
2567 ///
2568 /// This is the regression test for the `watch` split — before it, the
2569 /// `shutdown` future was consumed once and only one listener stopped —
2570 /// and now also for the metrics listener being genuinely *separate*:
2571 /// `/metrics` answering on the ACME port would put issuance volume and
2572 /// every profile name on the public socket, which is exactly what
2573 /// giving it its own port is for.
2574 #[tokio::test]
2575 async fn all_three_listeners_serve_and_one_signal_stops_them() {
2576 let dir = temp_dir();
2577 let mut config = config_in(&dir, false);
2578 config.admin.enabled = true;
2579 config.metrics.enabled = true;
2580
2581 let database = Arc::new(Database::connect_in_memory().await.unwrap());
2582 let acme_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2583 let admin_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2584 let metrics_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2585 let acme_addr = acme_listener.local_addr().unwrap();
2586 let admin_addr = admin_listener.local_addr().unwrap();
2587 let metrics_addr = metrics_listener.local_addr().unwrap();
2588 assert_ne!(acme_addr, admin_addr);
2589 assert_ne!(acme_addr, metrics_addr);
2590 assert_ne!(admin_addr, metrics_addr);
2591
2592 let (tx, rx) = tokio::sync::oneshot::channel::<()>();
2593 let handle = tokio::spawn(serve_on_with(
2594 Arc::new(config),
2595 database,
2596 acme_listener,
2597 Some(admin_listener),
2598 Some(metrics_listener),
2599 async {
2600 let _ = rx.await;
2601 },
2602 ));
2603
2604 // `/health` is on both of the two that have it.
2605 for addr in [acme_addr, admin_addr] {
2606 let response = get(addr, "/health").await;
2607 assert!(
2608 response.starts_with("HTTP/1.1 200 OK"),
2609 "{addr}: {response}"
2610 );
2611 }
2612
2613 // The admin API answers on the admin socket (401, since this
2614 // request carries no session) and is absent from the ACME one.
2615 let admin = get(admin_addr, "/api/accounts").await;
2616 assert!(admin.starts_with("HTTP/1.1 401"), "{admin}");
2617 let acme = get(acme_addr, "/api/accounts").await;
2618 assert!(acme.starts_with("HTTP/1.1 404"), "{acme}");
2619
2620 // And the converse: ACME is on the ACME socket only.
2621 let directory = get(acme_addr, "/profile/default/directory").await;
2622 assert!(directory.starts_with("HTTP/1.1 200 OK"), "{directory}");
2623 let no_directory = get(admin_addr, "/profile/default/directory").await;
2624 assert!(no_directory.starts_with("HTTP/1.1 404"), "{no_directory}");
2625
2626 // The exposition is on its own socket...
2627 let metrics = get(metrics_addr, "/metrics").await;
2628 assert!(metrics.starts_with("HTTP/1.1 200 OK"), "{metrics}");
2629 assert!(metrics.contains("acme_proxy_requests_total"), "{metrics}");
2630 // ...and on **neither** of the other two. The whole point of the
2631 // third listener is that firewalling this port is what controls who
2632 // can read it.
2633 for addr in [acme_addr, admin_addr] {
2634 let leaked = get(addr, "/metrics").await;
2635 assert!(leaked.starts_with("HTTP/1.1 404"), "{addr}: {leaked}");
2636 }
2637
2638 // One signal, all three stop.
2639 tx.send(()).unwrap();
2640 tokio::time::timeout(Duration::from_secs(10), handle)
2641 .await
2642 .expect("every listener must stop on one signal")
2643 .unwrap()
2644 .expect("a clean shutdown is not an error");
2645 }
2646
2647 /// Three listeners means the collision check is pairwise, and
2648 /// `webadmin::check_config` only covers admin-versus-server.
2649 #[tokio::test]
2650 async fn a_metrics_bind_colliding_with_another_listener_is_refused() {
2651 let dir = temp_dir();
2652
2653 for (other, set) in [
2654 (
2655 "server.bind_address",
2656 Box::new(|c: &mut Config| {
2657 c.metrics.bind_address = c.server.bind_address.clone()
2658 }) as Box<dyn Fn(&mut Config)>,
2659 ),
2660 (
2661 "admin.bind_address",
2662 Box::new(|c: &mut Config| {
2663 c.admin.enabled = true;
2664 c.metrics.bind_address = c.admin.bind_address.clone();
2665 }),
2666 ),
2667 ] {
2668 let mut config = config_in(&dir, false);
2669 config.metrics.enabled = true;
2670 set(&mut config);
2671
2672 let error = bind_metrics(&Arc::new(config))
2673 .await
2674 .expect_err("a shared socket must not start");
2675 let message = error.to_string();
2676 assert!(message.contains(other), "{message}");
2677 }
2678 }
2679
2680 /// The admin bind is only a conflict when the panel is actually going
2681 /// to bind it. Both default to loopback ports, so refusing on the
2682 /// *value* alone would reject a configuration that works.
2683 #[tokio::test]
2684 async fn a_metrics_bind_matching_a_disabled_admin_is_allowed() {
2685 let dir = temp_dir();
2686 let mut config = config_in(&dir, false);
2687 config.metrics.enabled = true;
2688 config.admin.enabled = false;
2689 config.metrics.bind_address = config.admin.bind_address.clone();
2690
2691 let listener = bind_metrics(&Arc::new(config))
2692 .await
2693 .expect("a disabled panel holds no socket");
2694 assert!(listener.is_some());
2695 }
2696
2697 /// Off by default, and off means no socket at all rather than one
2698 /// answering 404.
2699 #[tokio::test]
2700 async fn metrics_disabled_binds_nothing() {
2701 let dir = temp_dir();
2702 let config = config_in(&dir, false);
2703 assert!(!config.metrics.enabled);
2704
2705 assert!(bind_metrics(&Arc::new(config)).await.unwrap().is_none());
2706 }
2707
2708 /// The admin listener's **own** TLS arm.
2709 ///
2710 /// `[server.tls]` and `[admin.tls]` are separate settings with separate
2711 /// certificate paths, on purpose — the two listeners answer to
2712 /// different names — and they go through separate `axum::serve` arms in
2713 /// `serve_admin`. `a_tls_server_answers_over_a_real_handshake` covers
2714 /// the ACME one; this one was the only `TlsListener::spawn` call site
2715 /// in the crate with no test at all, which for the listener that
2716 /// carries an operator's session cookie is the wrong one to miss.
2717 #[tokio::test]
2718 async fn the_admin_listener_answers_over_its_own_tls() {
2719 use tokio_rustls::TlsConnector;
2720 use tokio_rustls::rustls::pki_types::ServerName;
2721
2722 let dir = temp_dir();
2723 let mut config = config_in(&dir, false);
2724 config.admin.enabled = true;
2725 // A distinct certificate from the ACME listener's, which is the
2726 // whole reason these are two settings.
2727 config.admin.tls.enabled = true;
2728 config.admin.tls.cert_path = dir.as_ref().join("admin.pem").display().to_string();
2729 config.admin.tls.key_path = dir.as_ref().join("admin.key").display().to_string();
2730 // `check_config` refuses a non-loopback bind without TLS; with TLS
2731 // on it is allowed, and this exercises that branch too.
2732 config.admin.base_url = "https://localhost:3001".to_string();
2733
2734 let database = Arc::new(Database::connect_in_memory().await.unwrap());
2735 let acme_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2736 let admin_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2737 let acme_addr = acme_listener.local_addr().unwrap();
2738 let admin_addr = admin_listener.local_addr().unwrap();
2739
2740 let (tx, rx) = tokio::sync::oneshot::channel::<()>();
2741 let handle = tokio::spawn(serve_on_with(
2742 Arc::new(config),
2743 database,
2744 acme_listener,
2745 Some(admin_listener),
2746 // This test is about the admin listener's own TLS arm; the
2747 // metrics listener has none.
2748 None,
2749 async {
2750 let _ = rx.await;
2751 },
2752 ));
2753
2754 // The ACME socket is still cleartext — the two settings really are
2755 // independent, which a shared switch would hide.
2756 let acme = get(acme_addr, "/health").await;
2757 assert!(acme.starts_with("HTTP/1.1 200 OK"), "{acme}");
2758
2759 // The admin socket needs a handshake. Self-signed, so the client
2760 // verifies nothing: the listener is the subject, not the chain.
2761 let client =
2762 crate::challenge::tls_alpn_01::accept_any_client_config(&[b"http/1.1"]).unwrap();
2763 let stream = TcpStream::connect(admin_addr).await.unwrap();
2764 let mut tls = TlsConnector::from(client)
2765 .connect(ServerName::try_from("localhost").unwrap(), stream)
2766 .await
2767 .expect("the admin listener must complete a handshake");
2768 tls.write_all(
2769 b"GET /api/accounts HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n",
2770 )
2771 .await
2772 .unwrap();
2773 let mut response = String::new();
2774 tls.read_to_string(&mut response).await.unwrap();
2775 assert!(
2776 response.starts_with("HTTP/1.1 401"),
2777 "the admin API answers over TLS, unauthenticated: {response}"
2778 );
2779
2780 // The certificate really was written to the admin paths, not the
2781 // server's — a shared path would make one listener overwrite the
2782 // other's key on every start.
2783 assert!(dir.as_ref().join("admin.pem").exists());
2784 assert!(dir.as_ref().join("admin.key").exists());
2785 assert!(!dir.as_ref().join("server.pem").exists());
2786
2787 tx.send(()).unwrap();
2788 tokio::time::timeout(Duration::from_secs(10), handle)
2789 .await
2790 .expect("both listeners must stop")
2791 .unwrap()
2792 .expect("a clean shutdown is not an error");
2793 }
2794
2795 /// With `[admin]` off — the default — nothing is bound but the ACME
2796 /// socket, and the panel's routes do not exist anywhere.
2797 #[tokio::test]
2798 async fn the_admin_listener_is_absent_by_default() {
2799 let dir = temp_dir();
2800 let config = config_in(&dir, false);
2801 assert!(!config.admin.enabled, "the default must stay off");
2802 let (addr, shutdown, handle) = boot(config).await;
2803
2804 let response = get(addr, "/api/accounts").await;
2805 assert!(response.starts_with("HTTP/1.1 404"), "{response}");
2806
2807 shutdown.send(()).unwrap();
2808 handle.await.unwrap().unwrap();
2809 }
2810
2811 /// A `[admin]` section that cannot work stops the whole process before
2812 /// either socket is bound — it must not leave the ACME listener up and
2813 /// the panel silently missing.
2814 #[tokio::test]
2815 async fn an_invalid_admin_section_refuses_to_serve() {
2816 let dir = temp_dir();
2817 let mut config = config_in(&dir, false);
2818 config.admin.enabled = true;
2819 // Non-loopback with TLS off: the `Secure` cookie would never be
2820 // stored, so this is a hard startup error.
2821 config.admin.bind_address = "0.0.0.0:0".to_string();
2822
2823 let database = Arc::new(Database::connect_in_memory().await.unwrap());
2824 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2825 let error = serve_on(Arc::new(config), database, listener, std::future::ready(()))
2826 .await
2827 .expect_err("a panel that cannot work must not start");
2828 assert!(error.to_string().contains("is not loopback"), "{error}");
2829 }
2830
2831 /// Sends one request and returns the raw response.
2832 async fn get(addr: SocketAddr, path: &str) -> String {
2833 let mut stream = TcpStream::connect(addr).await.unwrap();
2834 stream
2835 .write_all(
2836 format!("GET {path} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
2837 .as_bytes(),
2838 )
2839 .await
2840 .unwrap();
2841 let mut response = String::new();
2842 stream.read_to_string(&mut response).await.unwrap();
2843 response
2844 }
2845
2846 /// Two profiles relaying to **different** upstreams start.
2847 ///
2848 /// The whole shape of the reported bug: each profile's `[signer]`
2849 /// section differs, so `build_backends` keeps them apart, so there are
2850 /// two relay backends — and asking each for its own job handler made
2851 /// `JobRegistry::register` refuse the second for kind
2852 /// `signer_relay_issue`, taking the process down before the socket was
2853 /// ever served. This is the end-to-end form of
2854 /// `signer::relay::tests::multi_profile`, and the only test here that
2855 /// exercises `build_generation` assembling the registry from more than
2856 /// one relay.
2857 #[tokio::test(flavor = "multi_thread")]
2858 async fn two_profiles_relaying_to_different_upstreams_start() {
2859 use crate::signer::relay::testsrv;
2860
2861 let first = testsrv::start(testsrv::Script::default()).await;
2862 let second = testsrv::start(testsrv::Script::default()).await;
2863 let dir = temp_dir();
2864 let config = two_relay_profiles(&dir, &first, &second);
2865
2866 let (addr, shutdown, handle) = boot(config).await;
2867 let mut stream = TcpStream::connect(addr).await.unwrap();
2868 stream
2869 .write_all(b"GET /health HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
2870 .await
2871 .unwrap();
2872 let mut response = String::new();
2873 stream.read_to_string(&mut response).await.unwrap();
2874 assert!(response.starts_with("HTTP/1.1 200 OK"), "{response}");
2875
2876 shutdown.send(()).unwrap();
2877 handle
2878 .await
2879 .unwrap()
2880 .expect("two relay profiles must not refuse to start");
2881 }
2882
2883 /// A configuration mounting nothing fails before the socket is ever
2884 /// served, rather than starting a server that answers 404 everywhere.
2885 #[tokio::test]
2886 async fn a_configuration_with_no_profile_refuses_to_serve() {
2887 let database = Arc::new(Database::connect_in_memory().await.unwrap());
2888 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2889 let error = serve_on(
2890 Arc::new(Config::default()),
2891 database,
2892 listener,
2893 std::future::ready(()),
2894 )
2895 .await
2896 .expect_err("a server with no endpoint must not start");
2897 assert!(error.to_string().contains("profile"), "{error}");
2898 }
2899
2900 /// `Serve` is a `dispatch` arm like any other: its failure travels back
2901 /// as a value instead of taking the process down where it happened.
2902 #[tokio::test]
2903 async fn dispatch_serve_reports_a_startup_failure() {
2904 let database = Arc::new(Database::connect_in_memory().await.unwrap());
2905 // Binds fine, but mounts no endpoint — so it fails inside
2906 // `serve_on` rather than at the socket.
2907 let mut config = Config::default();
2908 config.server.bind_address = "127.0.0.1:0".to_string();
2909 let mut reader: &[u8] = &[];
2910
2911 let error = dispatch(
2912 Some(Command::Serve),
2913 true,
2914 ColorChoice::Never,
2915 &mut reader,
2916 &Arc::new(config),
2917 database,
2918 )
2919 .await
2920 .expect_err("a server with no endpoint must not start");
2921 assert!(error.to_string().contains("profile"), "{error}");
2922 }
2923
2924 /// Unreadable TLS material stops startup instead of silently falling
2925 /// back to cleartext on a port operators believe is HTTPS.
2926 #[tokio::test]
2927 async fn unusable_tls_material_stops_startup() {
2928 let dir = temp_dir();
2929 let mut config = config_in(&dir, true);
2930 std::fs::write(dir.join("server.pem"), "not a certificate").unwrap();
2931 std::fs::write(dir.join("server.key"), "not a key").unwrap();
2932 config.server.tls.cert_path = dir.join("server.pem").display().to_string();
2933 config.server.tls.key_path = dir.join("server.key").display().to_string();
2934
2935 let database = Arc::new(Database::connect_in_memory().await.unwrap());
2936 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2937 let error = serve_on(Arc::new(config), database, listener, std::future::ready(()))
2938 .await
2939 .expect_err("unreadable TLS material must not start a server");
2940 assert!(!error.to_string().is_empty());
2941 }
2942
2943 /// `serve` binds `server.bind_address` itself, so an unusable one is
2944 /// reported rather than panicking.
2945 #[tokio::test]
2946 async fn an_unbindable_address_is_reported() {
2947 let database = Arc::new(Database::connect_in_memory().await.unwrap());
2948 let mut config = Config::default();
2949 // A port on an address this process does not hold.
2950 config.server.bind_address = "192.0.2.1:1".to_string();
2951 let error = serve(Arc::new(config), database)
2952 .await
2953 .expect_err("binding an unroutable address must fail");
2954 assert!(error.to_string().contains("192.0.2.1:1"), "{error}");
2955 }
2956 }
2957}