Skip to main content

acme_proxy/
lib.rs

1// Feature badges on docs.rs. Turned on by `--cfg docsrs` from
2// `[package.metadata.docs.rs]`, so a stable `cargo doc`, `cargo build` and
3// clippy never see this nightly-only attribute. `doc_cfg` annotates every
4// `#[cfg(…)]` item on its own, so the `hsm`-gated items need no per-item
5// attribute and a future one is covered for free — the behaviour that used to
6// be a separate `doc_auto_cfg` feature, removed in 1.92 and merged into this
7// one. Do not reintroduce that name; it no longer compiles.
8#![cfg_attr(docsrs, feature(doc_cfg))]
9
10//! ACME (RFC 8555) Server Implementation
11//!
12//! This is a server-side implementation of the ACME protocol (RFC 8555) for
13//! issuing and managing SSL/TLS certificates. It serves as a backend for
14//! certificate clients like certbot and acme.sh.
15//!
16//! ## Features
17//!
18//! - The full RFC 8555 flow: directory, newNonce, newAccount, account
19//!   lookup/update and deactivation, newOrder, authorizations and challenges,
20//!   finalize, certificate retrieval via signed POST-as-GET, and revocation
21//! - JWS signature verification for EC (ES256) and RSA (RS256) keys
22//! - Automatic nonce management with replay protection
23//! - Challenge validation behind pluggable validators (`http-01`, `dns-01`,
24//!   `tls-alpn-01`), with a configurable bypass
25//! - Certificate issuance behind a pluggable signer backend: a local CA (whose
26//!   key may live in a PKCS#11 token), a relay to an upstream ACME CA, or an
27//!   operator-supplied script
28//! - **Profiles** — several independent ACME endpoints in one process, each with
29//!   its own signer, filters, challenge validators and EAB policy
30//! - External Account Binding (§7.3.4), account key rollover (§7.3.5) and
31//!   Renewal Information (RFC 9773)
32//! - Access control behind a policy engine of named checks combined by boolean
33//!   rules, including an IPAM lookup (NetBox, phpIPAM or a script) asking the
34//!   inventory whether the client's own address owns the names it is requesting
35//! - An append-only audit trail of every issuance *and every refusal*
36//! - An optional web admin listener, and admin subcommands in the same binary
37//! - Optional Prometheus metrics on a third listener of their own
38//! - A durable job queue, so work the server owes itself survives a restart and
39//!   an upstream blip is retried rather than invalidating a client's order
40//! - Configuration reload on `SIGHUP` — a rebuild and a swap, with
41//!   `database.url` the only key that still needs a restart
42//! - `SQLite` persistence for accounts, nonces, orders and the audit trail
43//! - Configurable via TOML, environment variables, or defaults
44//!
45//! ## Architecture
46//!
47//! The ACME request path, in the order a request meets it:
48//! - [`middlewares`] - Server-wide layers: request correlation and the access
49//!   line, admission control, the `Replay-Nonce` and `Link: rel="index"` headers
50//! - [`filter`] - Pluggable request filtering (who may ask at all)
51//! - [`extractors`] - Parse and validate ACME JWS requests, verifying the media
52//!   type, the `crit` header, the signature, the JWS `url` and the nonce before
53//!   any handler runs
54//! - [`handlers`] - One module per ACME resource
55//! - [`challenge`] - Pluggable challenge validators (http-01, dns-01, tls-alpn-01)
56//! - [`signer`] - Pluggable certificate-issuance backends (local CA, ACME relay,
57//!   custom script)
58//!
59//! Supporting subsystems:
60//! - [`audit`] - The durable record of who asked this CA to sign or revoke
61//! - [`notify`] - Pluggable operator notifications on lifecycle events (email,
62//!   webhook, custom)
63//! - [`ipam`] - The inventory [`filter`] asks which names an address owns
64//!   (NetBox, phpIPAM, a custom script), behind one trait
65//! - [`eab`] - Verification of the External Account Binding inner JWS (§7.3.4)
66//! - [`key_change`] - Verification of account key rollover JWS (§7.3.5)
67//! - [`dns`] - The resolver shared by every subsystem that looks anything up
68//! - [`http_client`] - The transport every outbound HTTP client is built on,
69//!   including the `CONNECT` tunnel
70//! - [`proxy`] - Which forward proxy, if any, that transport dials through
71//! - [`script_hook`] - The hardened contract every `custom` hook runs under
72//! - [`tls`] - Optional HTTPS termination for either listener
73//! - [`cert`] - X.509 parsing helpers (serial, SPKI, leaf-from-chain)
74//! - [`pemfile`] - PEM reading, atomic writing and key-permission warnings
75//! - [`sqlite`] - Database access, one module per table
76//! - [`config`] - Configuration loading from multiple sources
77//! - [`error`] - ACME error types and problem document rendering
78//!
79//! Process lifecycle — what keeps the server running and lets it be retuned
80//! without a restart:
81//! - [`listener`] - The sockets, and replacing one while it serves
82//! - [`reload`] - Rebuild-and-swap on `SIGHUP`; nothing is mutated in place
83//! - [`jobs`] - The durable queue and its runner, so work outlives the process
84//!   that queued it
85//! - [`metrics`] - The Prometheus registry and its text exposition
86//!
87//! Administration, which serves no ACME and is a second listener plus a CLI:
88//! - [`admin`] - The operation layer both front ends dispatch to
89//! - [`webadmin`] - The optional HTML + JSON admin listener
90//! - [`cli`] - The `clap` command tree, and the startup path itself
91//!
92//! ## Usage
93//!
94//! The main entry point is `build_app()`, which mounts one ACME router per
95//! configured profile under `/profile/<name>` and serves the server-level
96//! routes (`/health`) at the root.
97//!
98//! ```rust,no_run
99//! use std::net::SocketAddr;
100//! use std::sync::Arc;
101//! use acme_proxy::{
102//!     Profile, ProfileParts, build_app, challenge, config::Config, filter, ipam, jobs, notify,
103//!     signer, sqlite::db::Database,
104//! };
105//!
106//! #[tokio::main]
107//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
108//!     let config = Arc::new(Config::load()?);
109//!     let database = Arc::new(Database::connect(&config.database.url).await?);
110//!
111//!     let resolved = config.resolve_profiles()?;
112//!     // The resolver and the proxy policy, resolved before anything can dial:
113//!     // a proxy URL that cannot be understood must stop the process rather
114//!     // than leave egress elsewhere, and `dns.resolver` governs every outbound
115//!     // connection this server makes, not just challenge lookups. Bundled,
116//!     // because every outbound client takes them together — and because the
117//!     // rendering beside them is what tells a reload whether a signer backend
118//!     // has to be rebuilt.
119//!     let egress = Arc::new(acme_proxy::Egress::from_config(&config)?);
120//!     let outbound = egress.outbound();
121//!     // The enqueue side of the durable queue, built first because everything
122//!     // below queues into it. A backend that defers issuance (`relay`) is
123//!     // handed one at construction, and so is every notify dispatcher — a
124//!     // notification is a job row too. The runner that drains it is started
125//!     // separately, below.
126//!     let job_queue = jobs::JobQueue::new(database.clone(), &config.jobs);
127//!     // Built once, up front: an asynchronous signer backend (`relay`)
128//!     // has no `Profile` to reach a notifier through from its background
129//!     // completion task, so it is handed this whole map instead — and so is
130//!     // the `NotifyJob` that performs the deliveries.
131//!     let mut notifiers = std::collections::HashMap::new();
132//!     for profile in &resolved {
133//!         notifiers.insert(
134//!             profile.name.clone(),
135//!             notify::from_config(
136//!                 &profile.name,
137//!                 &profile.sections.notify,
138//!                 outbound.clone(),
139//!                 &job_queue,
140//!             )?,
141//!         );
142//!     }
143//!     let notifiers = Arc::new(notifiers);
144//!     // The Prometheus counters. Built here rather than per generation, so a
145//!     // `SIGHUP` does not reset every counter to zero — see `Assembly`.
146//!     let metrics = Arc::new(acme_proxy::metrics::Metrics::new(database.clone()));
147//!
148//!     let mut profiles = Vec::new();
149//!     for profile in &resolved {
150//!         let sections = &profile.sections;
151//!         profiles.push(Arc::new(Profile::new(
152//!             &profile.name,
153//!             &config.server.base_url,
154//!             ProfileParts {
155//!                 signer: signer::from_config(
156//!                     &sections.signer,
157//!                     vec![profile.name.clone()],
158//!                     &signer::SignerParts {
159//!                         database: database.clone(),
160//!                         notifiers: notifiers.clone().into(),
161//!                         metrics: metrics.clone(),
162//!                         egress: egress.clone(),
163//!                         jobs: job_queue.clone(),
164//!                     },
165//!                     // Nothing to adopt at startup; a reload passes what the
166//!                     // previous generation's backends handed over.
167//!                     &signer::CarriedState::new(),
168//!                 )?,
169//!                 filter: filter::from_config(
170//!                     &sections.filter,
171//!                     &config.dns,
172//!                     ipam::from_config(&sections.ipam, outbound.clone())?,
173//!                     sections.eab.enabled,
174//!                 )?,
175//!                 challenges: challenge::from_config(
176//!                     &sections.challenge,
177//!                     &config.dns,
178//!                     egress.proxies.clone(),
179//!                 )?,
180//!                 order: sections.order.clone(),
181//!                 eab: sections.eab.clone(),
182//!                 meta: sections.meta.clone(),
183//!                 notify: notifiers[&profile.name].clone(),
184//!             },
185//!         )));
186//!     }
187//!     // Process-wide, like `[audit]` itself: one trail for the whole CA,
188//!     // shared by every profile's router and by the web admin listener.
189//!     // The registry is a parameter rather than a builder step, so a serving
190//!     // process cannot build an auditor that counts into nothing. The counters
191//!     // come off the same `AuditRecord` the trail is written from, so the two
192//!     // can never disagree.
193//!     let audit = Arc::new(acme_proxy::audit::Auditor::from_config(
194//!         &config.audit,
195//!         &config.dns,
196//!         database.clone(),
197//!         metrics.clone(),
198//!     )?);
199//!     let app = build_app(
200//!         database.clone(),
201//!         config.clone(),
202//!         profiles,
203//!         audit,
204//!         metrics.clone(),
205//!     );
206//!
207//!     // One runner drains the queue for the process. Every handler comes from
208//!     // a subsystem that has background work — `SignerBackend::jobs`,
209//!     // notification delivery, and the periodic table sweeps — and the runner
210//!     // calls `recover` on each before it claims anything, which is how work a
211//!     // previous run left in flight is picked back up, and how each sweep's
212//!     // single row gets queued.
213//!     let mut registry = jobs::JobRegistry::new();
214//!     registry.register(Arc::new(notify::NotifyJob::new(notifiers)))?;
215//!     registry.register(Arc::new(jobs::SweepJob::nonces(
216//!         database.clone(),
217//!         std::time::Duration::from_secs(config.nonce.ttl_seconds),
218//!     )))?;
219//!     let (_shutdown, shutdown_rx) = tokio::sync::watch::channel(false);
220//!     jobs::spawn_runner(job_queue, Arc::new(registry), &config.jobs, shutdown_rx);
221//!
222//!     let listener = tokio::net::TcpListener::bind(&config.server.bind_address).await?;
223//!     axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>()).await?;
224//!
225//!     Ok(())
226//! }
227//! ```
228
229use std::sync::Arc;
230
231use axum::body::Body;
232use axum::http::{HeaderValue, Request, header};
233use axum::{
234    Router,
235    extract::DefaultBodyLimit,
236    middleware,
237    middleware::Next,
238    response::Redirect,
239    routing::{get, post},
240};
241use tower_http::set_header::SetResponseHeaderLayer;
242use tracing::{Span, info};
243
244pub mod admin;
245pub mod audit;
246pub mod cert;
247pub mod challenge;
248pub mod cli;
249pub mod config;
250pub mod dns;
251pub mod eab;
252pub mod error;
253pub mod extractors;
254pub mod filter;
255pub mod handlers;
256pub mod http_client;
257pub mod ipam;
258pub mod jobs;
259pub mod key_change;
260pub mod listener;
261pub mod metrics;
262pub mod middlewares;
263pub mod notify;
264pub mod pemfile;
265pub mod proxy;
266mod random;
267pub mod reload;
268pub mod script_hook;
269pub mod signer;
270pub mod sqlite;
271mod templating;
272#[cfg(test)]
273pub(crate) mod testutil;
274pub mod tls;
275pub mod webadmin;
276
277use crate::challenge::ChallengeRegistry;
278use crate::config::Config;
279use crate::error::Problem;
280use crate::filter::FilterPolicy;
281use crate::notify::NotifyDispatcher;
282use crate::signer::SignerBackend;
283use crate::sqlite::db::Database;
284
285// Re-export name shape helpers for backwards compatibility
286pub use handlers::helpers::{is_wildcard, normalize_dns_name, well_formed_name};
287
288/// The ACME resource paths, profile-relative.
289///
290/// One definition each, because they are written in three places that must
291/// agree and previously agreed only by inspection: the router that *mounts*
292/// them (`build_router`), the directory that *advertises* them
293/// (`handlers::get_directory`), and `middlewares::nonce`, which singles out
294/// `newNonce`. A directory advertising a path nothing serves is a client that
295/// fails on its very first request, and nothing structural caught it.
296///
297/// Only the resources with a fixed path are here; the id-bearing ones
298/// (`/acct/{id}`, `/order/{id}/finalize`, …) are never advertised, so they have
299/// exactly one call site and gain nothing from a constant.
300pub mod routes {
301    pub const DIRECTORY: &str = "/directory";
302    pub const NEW_NONCE: &str = "/newNonce";
303    pub const NEW_ACCOUNT: &str = "/newAccount";
304    pub const NEW_ORDER: &str = "/newOrder";
305    pub const REVOKE_CERT: &str = "/revokeCert";
306    pub const KEY_CHANGE: &str = "/keyChange";
307    /// RFC 9773 §4.1 has the client append the certID, so the directory
308    /// advertises this bare while the router mounts `{id}` under it.
309    pub const RENEWAL_INFO: &str = "/renewalInfo";
310    pub const CRL: &str = "/crl";
311    /// The trust anchor a client installs to accept this profile's leaves.
312    /// Routed beside [`CRL`] and, like it, deliberately not advertised in the
313    /// directory — both are CA infrastructure rather than ACME resources.
314    pub const CA_CHAIN: &str = "/ca.pem";
315}
316
317/// The URL namespace every ACME endpoint is mounted under: a profile named
318/// `le` serves `/profile/le/directory`.
319///
320/// Reserved and fixed, which is the point — server-level routes live at the
321/// root and a profile can never collide with one, now or when the next one is
322/// added.
323pub const PROFILE_PREFIX: &str = "/profile";
324
325/// A duration in milliseconds, as a log field.
326///
327/// `Duration::as_millis` returns `u128`, which `tracing` has no primitive
328/// visitor for and so records through `Display` — landing in the JSON output as
329/// a quoted `"42"` rather than the number `42`. That output exists to be
330/// aggregated by machines, and a latency field a collector has to re-parse (or
331/// silently indexes as a string) is a defect in it. Every duration logged
332/// anywhere in this crate goes through here.
333///
334/// The saturation is unreachable — `u64::MAX` milliseconds is some 584 million
335/// years — and is written out only to avoid a silent truncating cast.
336#[must_use]
337pub fn millis(duration: std::time::Duration) -> u64 {
338    u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
339}
340
341/// One ACME endpoint: its identity, its URLs, and the three subsystems that
342/// answer for it.
343///
344/// Everything per-endpoint lives here rather than beside the global config in
345/// [`AppState`], so a handler cannot pair one profile's signer with another's
346/// base URL — the two always travel together.
347pub struct Profile {
348    /// The configured name (`[profiles.<name>]`), also the URL segment and the
349    /// value stored in `accounts.profile` / `orders.profile`.
350    pub name: String,
351    /// Where the router mounts it: `/profile/<name>`.
352    pub path: String,
353    /// The public base for every URL this endpoint hands out and for the
354    /// RFC 8555 §6.4 `url` check: `server.base_url` + [`Profile::path`].
355    pub base_url: String,
356    pub signer: Arc<dyn SignerBackend>,
357    pub filter: Arc<FilterPolicy>,
358    pub challenges: Arc<ChallengeRegistry>,
359    pub order: config::OrderConfig,
360    pub eab: config::EabConfig,
361    /// The optional `meta` members this endpoint's directory advertises
362    /// (RFC 8555 §7.1.1). Per-profile, like everything else here: two endpoints
363    /// on one process can have different terms of service.
364    pub meta: config::MetaConfig,
365    pub notify: Arc<NotifyDispatcher>,
366}
367
368/// The subsystems and per-endpoint sections a [`Profile`] is assembled from.
369///
370/// A struct because [`Profile::new`] took nine positional parameters, four of
371/// them `Arc<dyn …>` or config sections that a reader has to count commas to
372/// tell apart. It also retires the crate's last
373/// `#[allow(clippy::too_many_arguments)]`.
374///
375/// `name` and `base_url` stay positional: they are what the constructor
376/// *derives* from rather than stores, and keeping them out of here is what
377/// makes "the path is never configured" visible in the signature.
378pub struct ProfileParts {
379    pub signer: Arc<dyn SignerBackend>,
380    pub filter: Arc<FilterPolicy>,
381    pub challenges: Arc<ChallengeRegistry>,
382    pub order: config::OrderConfig,
383    pub eab: config::EabConfig,
384    pub meta: config::MetaConfig,
385    pub notify: Arc<NotifyDispatcher>,
386}
387
388impl Profile {
389    /// Assembles a profile, deriving its path and base URL from its name —
390    /// the two are never configured, so they cannot drift from each other or
391    /// from what the database records.
392    pub fn new(name: &str, base_url: &str, parts: ProfileParts) -> Self {
393        let path = format!("{PROFILE_PREFIX}/{name}");
394        Self {
395            name: name.to_string(),
396            base_url: format!("{}{path}", base_url.trim_end_matches('/')),
397            path,
398            signer: parts.signer,
399            filter: parts.filter,
400            challenges: parts.challenges,
401            order: parts.order,
402            eab: parts.eab,
403            meta: parts.meta,
404            notify: parts.notify,
405        }
406    }
407
408    /// This endpoint's directory URL — where a client starts.
409    ///
410    /// Derived here rather than `format!`-ed at each of the three call sites
411    /// (the startup log line, the admin API's profile listing, and anything
412    /// added later), all of which have to agree with what `build_router`
413    /// actually mounts.
414    #[must_use]
415    pub fn directory_url(&self) -> String {
416        format!("{}{}", self.base_url, routes::DIRECTORY)
417    }
418
419    /// Builds every endpoint this configuration mounts, ready to serve.
420    ///
421    /// Lives here rather than in `cli::serve_on` because it is the assembly step,
422    /// not dispatch: it resolves the profiles, builds the signer backends
423    /// (deduplicated by configuration — see [`signer::build_backends`]), and
424    /// gives each profile its own filter chain and challenge registry. Every
425    /// failure is fatal at startup, so they come back as one error for the
426    /// caller to report and exit on.
427    ///
428    /// Each profile's subsystems are built inside a span naming it, so the
429    /// warnings they emit at build time (`filter_disabled`,
430    /// `challenge_validation_bypassed`) say *which* endpoint is wide open —
431    /// with several mounted, an unattributed warning is worse than none.
432    /// `jobs` is the enqueue side of the durable queue, handed in rather than
433    /// built here for the reason the `Auditor` is built in `serve_on_with`:
434    /// `[jobs]` is process-wide, one queue drained by one runner, and a profile
435    /// is not the thing that owns it.
436    pub fn build_all(
437        config: &Config,
438        database: Arc<Database>,
439        jobs: &crate::jobs::JobQueue,
440    ) -> anyhow::Result<Vec<Arc<Profile>>> {
441        let resolved = config.resolve_profiles()?;
442        let (_assembly, first) = Assembly::new(&resolved, database, jobs.clone(), config)?;
443        Self::build_all_with(config, &resolved, &first)
444    }
445
446    /// One generation of profiles, over an [`Assembly`] that outlives it.
447    ///
448    /// The half of [`build_all`](Self::build_all) a configuration reload runs
449    /// again. Everything it touches is cheap and side-effect-free to rebuild —
450    /// a filter policy, an IPAM client, a challenge registry — which is exactly
451    /// why the *stateful* half lives in the `Assembly` instead. The signer
452    /// backends are the interesting middle case: they are rebuilt here too, but
453    /// only the ones whose configuration actually moved, and those adopt what
454    /// the outgoing instance held (see [`signer::build_backends`]).
455    pub fn build_all_with(
456        config: &Config,
457        resolved: &[config::ProfileConfig],
458        generation: &GenerationParts,
459    ) -> anyhow::Result<Vec<Arc<Profile>>> {
460        let egress = &generation.egress;
461        let dispatchers = &generation.dispatchers;
462        let backends = &generation.signers;
463
464        let mut profiles = Vec::with_capacity(resolved.len());
465        for profile in resolved {
466            let sections = &profile.sections;
467            let span = tracing::info_span!("profile", profile = %profile.name);
468            let (filter, challenges) = span.in_scope(|| {
469                // Built per profile with no dedup pass, unlike
470                // `signer::build_backends`. Sharing a signer backend is a
471                // correctness requirement — two `LocalCa` over one CRL file
472                // would clobber each other's ledger — whereas an IPAM client
473                // owns no files and holds no mutable state, so two profiles
474                // naming the same inventory each building one costs nothing
475                // but a `rustls::ClientConfig`.
476                let ipam = ipam::from_config(&sections.ipam, egress.outbound())
477                    .map_err(|error| anyhow::anyhow!("profile `{}`: {error}", profile.name))?;
478                let filter =
479                    filter::from_config(&sections.filter, &config.dns, ipam, sections.eab.enabled)
480                        .map_err(|error| anyhow::anyhow!("profile `{}`: {error}", profile.name))?;
481                let challenges = challenge::from_config(
482                    &sections.challenge,
483                    &config.dns,
484                    egress.proxies.clone(),
485                )
486                .map_err(|error| anyhow::anyhow!("profile `{}`: {error}", profile.name))?;
487                check_request_timeout(config, profile.name.as_str(), sections)?;
488                Ok::<_, anyhow::Error>((filter, challenges))
489            })?;
490
491            profiles.push(Arc::new(Profile::new(
492                &profile.name,
493                &config.server.base_url,
494                ProfileParts {
495                    signer: backends
496                        .get(&profile.name)
497                        .ok_or_else(|| {
498                            anyhow::anyhow!("profile `{}`: no signer backend", profile.name)
499                        })?
500                        .clone(),
501                    filter,
502                    challenges,
503                    order: sections.order.clone(),
504                    eab: sections.eab.clone(),
505                    meta: sections.meta.clone(),
506                    notify: dispatchers[&profile.name].clone(),
507                },
508            )));
509        }
510        Ok(profiles)
511    }
512}
513
514/// The outbound plumbing one configuration generation dials through, and the
515/// identity of the configuration it came from.
516///
517/// `[dns]` and `[proxy]` are process-wide but no longer frozen, so they belong
518/// to a *generation* rather than to the [`Assembly`]: a reload builds a fresh
519/// resolver and proxy policy from the file, and every subsystem that reaches the
520/// network is handed this generation's pair. The signer backends look like the
521/// exception and are not — they cache what they were built with, so
522/// [`signer::build_backends`] folds `identity` into a backend's identity key and
523/// rebuilds any backend whose egress moved. Keeping the identity here rather
524/// than beside the call site is what stops the two disagreeing, which would make
525/// a `dns.resolver` edit a silent no-op for every signer.
526pub struct Egress {
527    /// Uncached, for the reason `challenge::build_resolver` explains: a client
528    /// publishing a `dns-01` record moments before triggering must not be
529    /// defeated by a cached negative answer.
530    pub resolver: Arc<dyn dns::Resolver>,
531    pub proxies: Arc<proxy::OutboundProxies>,
532    /// `[dns]` and `[proxy]` rendered. Only ever compared to another one — never
533    /// parsed, never shown — which is the same contract `signer::build_backends`
534    /// keys a `[signer]` section on.
535    pub identity: String,
536}
537
538impl Egress {
539    /// Builds both clients from `config`.
540    ///
541    /// Fallible for two separate reasons worth keeping apart: a proxy URL that
542    /// cannot be understood, and a `dns.resolver` that is not a socket address.
543    /// Both must stop a startup and refuse a reload rather than degrade — a
544    /// server that silently fell back to direct egress would dial around exactly
545    /// the control its operator configured.
546    pub fn from_config(config: &Config) -> anyhow::Result<Self> {
547        let proxies = crate::proxy::from_config(&config.proxy)?;
548        // One resolver per generation, handed to every subsystem that makes an
549        // outbound connection. `dns.resolver` is documented as "the nameserver
550        // every DNS lookup this server makes goes through", and three of the
551        // four HTTP clients used to bypass it — so an operator on a
552        // split-horizon estate had NetBox and their upstream CA resolving
553        // differently from the challenge validators, with nothing saying so.
554        let resolver = challenge::build_resolver(crate::dns::resolver_addr(&config.dns)?)?;
555        Ok(Self {
556            resolver,
557            proxies,
558            identity: format!("{:?}|{:?}", config.dns, config.proxy),
559        })
560    }
561
562    /// The resolver and proxy policy as one value, for the subsystems that make
563    /// outbound HTTP requests.
564    ///
565    /// An accessor rather than a stored field: `challenge::from_config` builds
566    /// its *own* resolver past the bypass branch (constructing one is what
567    /// reads `/etc/resolv.conf`, so it must not happen when validation is off),
568    /// and so needs the proxy half on its own.
569    #[must_use]
570    pub fn outbound(&self) -> http_client::Outbound {
571        http_client::Outbound::new(self.resolver.clone(), self.proxies.clone())
572    }
573}
574
575/// The three things one configuration generation contributes to its profiles,
576/// built before any of them is published.
577///
578/// A struct because [`Profile::build_all_with`] would otherwise take three
579/// same-shaped values positionally, and because the three are built together and
580/// must be published together — `cli::apply_reload` swaps the notifier map and
581/// the signer set in the same uninterruptible run as the routers built from
582/// them.
583pub struct GenerationParts {
584    pub egress: Arc<Egress>,
585    pub dispatchers: notify::DispatcherMap,
586    pub signers: signer::SignerSet,
587}
588
589/// What survives a configuration reload.
590///
591/// Every generation rebuilds its profiles, its routers, its job registry, its
592/// egress clients and any signer backend whose configuration moved. The things
593/// here are built once for the life of the process and handed to each generation
594/// instead — and after three rounds of moving things *off* this list, everything
595/// left is here because rebuilding it would lose something, never because
596/// rebuilding it would merely cost something:
597///
598/// - `database` and `jobs` are the pool and its enqueue side. `database.url` is
599///   the one key [`crate::reload`] still refuses, and this is why.
600/// - `metrics` is a **correctness** requirement. A registry rebuilt per
601///   generation would reset every counter on `SIGHUP`, and a counter going
602///   backwards is precisely how Prometheus recognises a process restart — so
603///   `rate()` would report the whole pre-reload total as a spike on every
604///   configuration change.
605/// - `signers` is the *previous* generation's backend set, kept so the next
606///   reload can reuse a backend whose configuration did not move and hand the
607///   live in-memory state of one that did to its replacement (see
608///   [`signer::CarriedState`]). Behind a `Mutex` because it is written once per
609///   generation; nothing reads it to serve a request, since a `Profile` holds
610///   its own `Arc<dyn SignerBackend>`.
611/// - `notifiers` is a handle rather than a map, so `[notify]` can reload
612///   underneath the backends that captured it.
613///
614/// `resolver` and `proxies` used to be here, justified by the signers caching
615/// them at construction. They moved to [`Egress`] when that stopped being a
616/// reason to freeze `[dns]`/`[proxy]` and became a reason to rebuild a signer.
617pub struct Assembly {
618    pub database: Arc<Database>,
619    pub jobs: crate::jobs::JobQueue,
620    pub metrics: Arc<metrics::Metrics>,
621    pub notifiers: notify::Notifiers,
622    notifiers_tx: notify::NotifiersSender,
623    signers: std::sync::Mutex<signer::SignerSet>,
624}
625
626impl Assembly {
627    /// Builds everything that outlives a generation, plus the first generation's
628    /// own parts.
629    ///
630    /// Those come back rather than being kept here because they are *not*
631    /// long-lived: the caller hands them to [`Profile::build_all_with`] and then
632    /// forgets them, and every later generation builds its own through
633    /// [`build_parts`](Self::build_parts).
634    pub fn new(
635        resolved: &[config::ProfileConfig],
636        database: Arc<Database>,
637        jobs: crate::jobs::JobQueue,
638        config: &Config,
639    ) -> anyhow::Result<(Self, GenerationParts)> {
640        // Built before the signers, because the `relay` backend settles an
641        // issuance from a background task that has no request and no `Auditor`,
642        // so it counts that issuance through a handle it was given at
643        // construction.
644        let metrics = Arc::new(metrics::Metrics::new(database.clone()));
645        // Opened over an empty map and republished immediately below, so the
646        // handle the signers capture is the one every later generation writes
647        // into.
648        let (notifiers_tx, notifiers) = notify::notifiers_channel(notify::DispatcherMap::new());
649
650        let assembly = Self {
651            database,
652            jobs,
653            metrics,
654            notifiers,
655            notifiers_tx,
656            signers: std::sync::Mutex::new(signer::SignerSet::default()),
657        };
658        let parts = assembly.build_parts(resolved, config)?;
659        // The first generation's map has to reach the handle before anything
660        // dispatches through it; every later one goes through `publish` in the
661        // reload's own synchronous run.
662        assembly.publish_notifiers(parts.dispatchers.clone());
663        assembly.publish_signers(parts.signers.clone());
664        Ok((assembly, parts))
665    }
666
667    /// Builds one generation's egress, dispatchers and signer backends, without
668    /// publishing any of them.
669    ///
670    /// Separate from [`publish_notifiers`](Self::publish_notifiers) and
671    /// [`publish_signers`](Self::publish_signers) because a reload must be able
672    /// to fail *after* building all three and still leave the running generation
673    /// untouched. Everything fallible is here; everything published is there.
674    ///
675    /// May block: `RelaySigner::from_config` contacts the upstream the first
676    /// time it is built for an account with no `kid` sidecar yet, which is why
677    /// `cli::supervise_reloads` runs this on a blocking thread.
678    pub fn build_parts(
679        &self,
680        resolved: &[config::ProfileConfig],
681        config: &Config,
682    ) -> anyhow::Result<GenerationParts> {
683        // Resolved before anything can dial: a proxy URL that cannot be
684        // understood must stop the process, and the `relay` backend below makes
685        // a real network call on its very first startup.
686        let egress = Arc::new(Egress::from_config(config)?);
687        // Built before the signer backends: the `relay` backend's background
688        // completion task has no `Profile`/`AppState` to reach a notifier
689        // through (it outlives any single request, the same reason it is handed
690        // `database`), so it is instead handed the whole `profile name ->
691        // dispatcher` map and looks up the right one by `Order.profile` once an
692        // issuance settles.
693        let dispatchers = notify::build_registry(resolved, egress.outbound(), &self.jobs)?;
694        let previous = self
695            .signers
696            .lock()
697            .unwrap_or_else(std::sync::PoisonError::into_inner)
698            .clone();
699        let signers = signer::build_backends(
700            resolved,
701            &signer::SignerParts {
702                database: self.database.clone(),
703                notifiers: self.notifiers.clone(),
704                metrics: self.metrics.clone(),
705                egress: egress.clone(),
706                jobs: self.jobs.clone(),
707            },
708            &previous,
709        )?;
710
711        Ok(GenerationParts {
712            egress,
713            dispatchers,
714            signers,
715        })
716    }
717
718    /// Makes `dispatchers` the generation every long-lived reader sees.
719    ///
720    /// Synchronous, and deliberately: it is one of the sends a reload makes
721    /// back-to-back so no task can observe a half-swapped generation.
722    pub fn publish_notifiers(&self, dispatchers: notify::DispatcherMap) {
723        self.notifiers_tx.send_replace(Arc::new(dispatchers));
724    }
725
726    /// Records `signers` as what the *next* reload compares against, and drops
727    /// whatever the generation before it held.
728    ///
729    /// That drop is the point at which a backend nobody references any more —
730    /// an unmounted profile's, or the instance a `[signer]` edit replaced — is
731    /// finally released. Deliberately after its replacement was built and has
732    /// adopted its state, never before.
733    pub fn publish_signers(&self, signers: signer::SignerSet) {
734        *self
735            .signers
736            .lock()
737            .unwrap_or_else(std::sync::PoisonError::into_inner) = signers;
738    }
739}
740
741/// Refuses a `server.request_timeout_ms` shorter than the work the server does
742/// *inside* a request.
743///
744/// Two hooks run inline in a handler rather than in the background: challenge
745/// validation (`post_challenge` awaits `challenges.validate`) and the `custom`
746/// signer's script (`post_finalize` awaits it). If the request deadline is the
747/// shorter of the two budgets, a validation that was going to succeed is cut
748/// off and the client is told the server failed — a misconfiguration that would
749/// look like an intermittent CA outage and be miserable to diagnose. Cheaper to
750/// refuse to start and say which two numbers disagree.
751fn check_request_timeout(
752    config: &Config,
753    name: &str,
754    sections: &config::ProfileSections,
755) -> anyhow::Result<()> {
756    let deadline = config.server.request_timeout_ms;
757    let inline = [
758        ("challenge.timeout_ms", sections.challenge.timeout_ms),
759        (
760            "signer.custom.timeout_ms",
761            // Only when that backend is the one actually installed; an unused
762            // `[signer.custom]` section says nothing about this profile.
763            if sections.signer.backend == "custom" {
764                sections.signer.custom.timeout_ms
765            } else {
766                0
767            },
768        ),
769    ];
770
771    for (key, budget) in inline {
772        anyhow::ensure!(
773            deadline > budget,
774            "profile `{name}`: server.request_timeout_ms ({deadline}) must exceed {key} \
775             ({budget}) — that hook runs inside the request, so a shorter deadline would cut \
776             off work that was going to succeed and report it to the client as a server failure",
777        );
778    }
779    Ok(())
780}
781
782/// Shared application state handed to every route via `State<AppState>`.
783#[derive(Clone)]
784pub struct AppState {
785    pub database: Arc<Database>,
786    /// Process-wide configuration only — `server`, `nonce`, `dns`, `logging`.
787    /// Anything an endpoint can differ on is on [`AppState::profile`].
788    pub config: Arc<Config>,
789    pub profile: Arc<Profile>,
790    /// The CA's audit trail. Beside `config` rather than on the profile,
791    /// because `[audit]` is process-wide: the trail describes the CA, and the
792    /// web admin writes to the same one across every endpoint it can revoke on.
793    pub audit: Arc<audit::Auditor>,
794}
795
796/// Every distinct `http-01` token store across the mounted profiles.
797///
798/// Deduplicated by pointer: [`signer::build_backends`] already shares one
799/// backend instance between profiles with identical `[signer]` sections, so
800/// several profiles usually contribute the *same* store. Two profiles relaying
801/// to two different upstreams contribute two, and the route consults both —
802/// there is nothing to isolate, because the token is the upstream's own random
803/// value and is itself the secret (RFC 8555 §8.3), so one merged view cannot
804/// answer the wrong challenge.
805fn http01_stores(profiles: &[Arc<Profile>]) -> Vec<Arc<dyn signer::Http01TokenStore>> {
806    let mut stores: Vec<Arc<dyn signer::Http01TokenStore>> = Vec::new();
807    for profile in profiles {
808        if let Some(store) = profile.signer.http01_tokens()
809            && !stores.iter().any(|existing| Arc::ptr_eq(existing, &store))
810        {
811            stores.push(store);
812        }
813    }
814    stores
815}
816
817/// Builds the whole HTTP service: the server-level routes at the root, and one
818/// ACME router per profile under `/profile/<name>`.
819/// The three response-hardening headers **both** listeners apply.
820///
821/// A shared constructor rather than two copies: the admin router is not nested
822/// inside [`build_app`] and so inherits none of its layers, but these three are
823/// a security control, and two hand-written copies of one are a control that
824/// drifts. Everything genuinely per-listener — the admin's `Cache-Control`,
825/// `Referrer-Policy` and CSP, this one's admission and nonce layers — stays at
826/// its own call site.
827///
828/// A tuple because `tower` implements [`Layer`](tower::Layer) for one, so the
829/// three still apply as three separate layers rather than being collapsed into
830/// a wrapper type. They set distinct headers, so their order among themselves
831/// carries no meaning.
832pub(crate) fn security_headers() -> (
833    SetResponseHeaderLayer<HeaderValue>,
834    SetResponseHeaderLayer<HeaderValue>,
835    SetResponseHeaderLayer<HeaderValue>,
836) {
837    (
838        SetResponseHeaderLayer::overriding(
839            header::STRICT_TRANSPORT_SECURITY,
840            HeaderValue::from_static("max-age=31536000; includeSubDomains"),
841        ),
842        SetResponseHeaderLayer::overriding(
843            header::X_CONTENT_TYPE_OPTIONS,
844            HeaderValue::from_static("nosniff"),
845        ),
846        SetResponseHeaderLayer::overriding(
847            header::X_FRAME_OPTIONS,
848            HeaderValue::from_static("DENY"),
849        ),
850    )
851}
852
853pub fn build_app(
854    database: Arc<Database>,
855    config: Arc<Config>,
856    profiles: Vec<Arc<Profile>>,
857    audit: Arc<audit::Auditor>,
858    metrics: Arc<metrics::Metrics>,
859) -> Router {
860    // Server-level routes. Deliberately *outside* the admission limit below: a
861    // health probe is asked for precisely when the server is saturated, and
862    // inside the limit it was starved exactly when it mattered — a load
863    // balancer would go on reporting the server healthy right up to the point
864    // where the probe itself could no longer get a slot.
865    let mut root = Router::new()
866        .route("/", get(|| async { Redirect::temporary("/health") }))
867        .route("/health", get(handlers::get_health_check));
868
869    // The `http-01` responder for the *upstream's* challenge, mounted only when
870    // a signer backend has tokens to serve — which today means `relay`
871    // with `challenge_strategy = "http01"`. Here beside `/health` rather than
872    // inside a profile: RFC 8555 §8.3 fixes this path at the root of the name
873    // being certified, and the CA fetching it holds no account at this server,
874    // so it must not meet a filter chain, a nonce or an ACME 404.
875    let stores = http01_stores(&profiles);
876    if !stores.is_empty() {
877        info!(
878            event = "http_01_responder_mounted",
879            outcome = "advisory",
880            path = challenge::http_01::WELL_KNOWN_PREFIX,
881            stores = stores.len(),
882            "a reverse proxy must forward or redirect \
883             http://<identifier>:80/.well-known/acme-challenge/ here for the upstream to reach it"
884        );
885        root = root.merge(
886            Router::new()
887                .route(
888                    &format!("{}{{token}}", challenge::http_01::WELL_KNOWN_PREFIX),
889                    get(handlers::get_challenge_file),
890                )
891                .with_state(handlers::Http01Stores(Arc::new(stores))),
892        );
893    }
894
895    let mut acme = Router::new();
896    for profile in &profiles {
897        let path = profile.path.clone();
898        acme = acme.nest(
899            &path,
900            build_router(
901                database.clone(),
902                config.clone(),
903                profile.clone(),
904                audit.clone(),
905            ),
906        );
907    }
908
909    let server = &config.server;
910    let acme = acme
911        .layer(middleware::from_fn_with_state(
912            middlewares::admission::Admission::new(
913                server.max_concurrent_requests,
914                server.admission_wait_ms,
915                server.request_timeout_ms,
916            ),
917            middlewares::admission::admission_middleware,
918        ))
919        // Innermost of the two, so it is in force by the time
920        // `String::from_request` reads the JWS body in `verify_jws`. Without it
921        // the ceiling is axum's implicit 2 MiB, which every concurrent request
922        // may buffer and then hand to `serde_json` — for a body that is a JWS
923        // carrying at most a CSR.
924        .layer(DefaultBodyLimit::max(server.max_body_bytes));
925
926    // Server-wide layers, applied once rather than once per profile. The
927    // filter and nonce layers are deliberately *not* here: both are ACME
928    // concerns and live inside each profile's own router.
929    let app = root.merge(acme);
930
931    // Counting sits here even though the exposition is served on a *different*
932    // socket (see `metrics_app`): this is the only router that sees an ACME
933    // request, and the registry both share is an `Arc`. On the merged router
934    // rather than inside a profile, because `Router::layer` applies per route
935    // *and* to the fallback — so a request that matched nothing is counted too,
936    // under `ROUTE_UNMATCHED`. It also runs after routing, which is what makes
937    // `MatchedPath` present: the label has to be the route *pattern*
938    // (`/order/{id}`), never the URI, or every order ever finalized would be
939    // its own series for as long as the scraper retained it.
940    //
941    // Added only when the listener exists, so an operator who has not asked for
942    // metrics pays neither the lock nor the allocation per request.
943    let app = if config.metrics.enabled {
944        app.layer(middleware::from_fn_with_state(
945            metrics,
946            middlewares::metrics::record_request,
947        ))
948    } else {
949        app
950    };
951
952    app.layer(security_headers())
953        // Outermost of everything, so the `request` span it opens — and the
954        // `x-request-id` it echoes — covers every route, the admission layer
955        // and the two hardening layers alike. Nothing below it is allowed to
956        // log without an id.
957        .layer(middleware::from_fn(
958            middlewares::access::add_access_middleware,
959        ))
960}
961
962/// Builds the metrics listener's router: `GET /metrics` and nothing else.
963///
964/// A **third socket**, not a route on either of the other two. The port is the
965/// access control — see [`crate::config::MetricsConfig`] — which is why there
966/// is no session extractor here and no filter chain, and why the exposition can
967/// name every profile without that being a decision about the public listener.
968///
969/// Deliberately none of `build_app`'s layers. There is no admission control (a
970/// scrape is wanted *most* when the server is saturated, the reason `/health`
971/// sits outside it too), no `Replay-Nonce`, no `Link: rel="index"`, no
972/// `DefaultBodyLimit` (a `GET` with no body), and no security headers — those
973/// exist for a browser, and nothing renders this. It keeps only the access
974/// middleware, so a scrape is a `request_completed` line like everything else
975/// and its `x-request-id` correlates with whatever it was measuring.
976///
977/// This router is **not** behind a [`reload`] swap cell, unlike the other two.
978/// It has one route, and its only state is the registry — which by design is
979/// carried across generations rather than rebuilt (see [`Assembly`]), so there
980/// is nothing a reload could put in a new one. `metrics.enabled` and
981/// `metrics.bind_address` are frozen for the reason every bind address is: the
982/// socket cannot move under a running listener.
983pub fn metrics_app(metrics: Arc<metrics::Metrics>) -> Router {
984    Router::new()
985        .route("/metrics", get(handlers::get_metrics))
986        .with_state(handlers::MetricsState(metrics))
987        .layer(middleware::from_fn(
988            middlewares::access::add_access_middleware,
989        ))
990}
991
992/// Builds one profile's ACME router: every RFC 8555 resource, plus the two
993/// layers that are per-endpoint (its filter chain) or ACME-specific (the
994/// `Replay-Nonce` minting).
995///
996/// Paths here are relative to the mount point — `axum::Router::nest` strips
997/// the prefix before this router sees a request, which is also what makes
998/// `verify_jws`'s `base_url + path` reconstruction correct.
999pub fn build_router(
1000    database: Arc<Database>,
1001    config: Arc<Config>,
1002    profile: Arc<Profile>,
1003    audit: Arc<audit::Auditor>,
1004) -> Router {
1005    let filter = profile.filter.clone();
1006    let state = AppState {
1007        database: database.clone(),
1008        config,
1009        profile: profile.clone(),
1010        audit,
1011    };
1012
1013    let profile_name = profile.name.clone();
1014
1015    // RFC 8555 §7.1 — the `index` link every resource but the directory carries.
1016    // Built once here rather than per response; an invalid header value is
1017    // impossible for a URL that already passed config validation, but falling
1018    // back to skipping the layer beats panicking a whole endpoint over it.
1019    let index_link =
1020        HeaderValue::from_str(&format!("<{}/directory>;rel=\"index\"", profile.base_url));
1021
1022    let router = Router::<AppState>::new()
1023        // §6.3: the directory and newNonce MUST answer a plain GET *and* a
1024        // POST-as-GET. The extra methods chain onto one `MethodRouter` —
1025        // registering the same path twice would replace the first route.
1026        .route(
1027            routes::DIRECTORY,
1028            get(handlers::get_directory).post(handlers::post_directory),
1029        )
1030        .route(
1031            routes::NEW_NONCE,
1032            get(handlers::get_new_nonce)
1033                .head(handlers::head_new_nonce)
1034                .post(handlers::post_new_nonce),
1035        )
1036        .route(routes::NEW_ACCOUNT, post(handlers::post_new_account))
1037        .route("/acct/{id}", post(handlers::post_account))
1038        .route("/acct/{id}/orders", post(handlers::post_account_orders))
1039        .route(routes::KEY_CHANGE, post(handlers::post_key_change))
1040        .route(routes::NEW_ORDER, post(handlers::post_new_order))
1041        .route("/order/{id}", post(handlers::post_order))
1042        .route("/order/{id}/finalize", post(handlers::post_finalize))
1043        .route("/authz/{id}", post(handlers::post_authz))
1044        .route("/chall/{id}", post(handlers::post_challenge))
1045        .route("/certificate/{id}", post(handlers::post_certificate))
1046        .route(routes::REVOKE_CERT, post(handlers::post_revoke_cert))
1047        .route(
1048            &format!("{}/{{id}}", routes::RENEWAL_INFO),
1049            get(handlers::get_renewal_info),
1050        )
1051        .route(routes::CRL, get(handlers::get_crl))
1052        .route(routes::CA_CHAIN, get(handlers::get_ca_chain))
1053        // §6.3: "if the server receives a GET request, it MUST return an error
1054        // with status code 405 (Method Not Allowed) and type `malformed`".
1055        // axum's own default gets the status right but sends an empty body, so
1056        // these two fallbacks supply the problem document — for a wrong method
1057        // and, in the same spirit, for a path that routes nowhere.
1058        .method_not_allowed_fallback(|| async {
1059            Problem::method_not_allowed("This resource must be read with POST-as-GET")
1060        })
1061        .fallback(|| async { Problem::not_found("No such resource") })
1062        .with_state(state)
1063        .layer(middleware::from_fn_with_state(
1064            filter,
1065            middlewares::filter::add_filter_middleware,
1066        ))
1067        .layer(middleware::from_fn_with_state(
1068            database.clone(),
1069            middlewares::nonce::add_nonce_middleware,
1070        ));
1071
1072    // Outermost of the profile's layers that touch a response, so the link
1073    // reaches every one of them — including the two fallbacks above and
1074    // anything a filter refuses. (The `profile` recorder below wraps this, but
1075    // only writes to the tracing span.)
1076    let router = match index_link {
1077        Ok(value) => router.layer(middleware::from_fn_with_state(
1078            value,
1079            middlewares::index_link::add_index_link_middleware,
1080        )),
1081        Err(error) => {
1082            tracing::error!(
1083                event = "request_index_link_header_invalid",
1084                outcome = "failure",
1085                base_url = %profile.base_url,
1086                error = %error,
1087            );
1088            router
1089        }
1090    };
1091
1092    // `profile` is declared `field::Empty` on the server-wide `request` span
1093    // (`middlewares::access`) and filled in here — the first layer that knows
1094    // which endpoint the request landed on, since the name comes from the
1095    // `/profile/<name>` mount point `Router::nest` has already stripped.
1096    // Ahead of every other layer of this router so a request a filter refuses
1097    // still says *which* endpoint refused it.
1098    router.layer(middleware::from_fn(
1099        move |request: Request<Body>, next: Next| {
1100            let name = profile_name.clone();
1101            async move {
1102                Span::current().record("profile", &*name);
1103                next.run(request).await
1104            }
1105        },
1106    ))
1107}
1108
1109#[cfg(test)]
1110mod tests {
1111    use super::*;
1112
1113    /// Loads a whole configuration file, the only way profile resolution can be
1114    /// exercised (it reads the raw sources — see `Config::resolve_profiles`).
1115    ///
1116    /// Holds the crate-wide `ENV_LOCK` while it does: this points
1117    /// `ACME_PROXY_CONFIG` at its own file, and the environment is process-wide.
1118    fn config_from(body: &str) -> Config {
1119        let _lock = crate::config::ENV_LOCK
1120            .lock()
1121            .unwrap_or_else(std::sync::PoisonError::into_inner);
1122        let dir = crate::testutil::TempDir::new("lib");
1123        std::fs::write(dir.join("config.toml"), body).unwrap();
1124        // SAFETY: single-threaded test; the variable is removed before return.
1125        unsafe {
1126            std::env::set_var("ACME_PROXY_CONFIG", dir.join("config").to_str().unwrap());
1127        }
1128        let config = Config::load().expect("the configuration must load");
1129        unsafe {
1130            std::env::remove_var("ACME_PROXY_CONFIG");
1131        }
1132        config
1133    }
1134
1135    /// A CA-material-free configuration: `local_ca` writes files at startup, so
1136    /// each profile gets its own throwaway directory.
1137    fn two_profiles_config(dir: impl AsRef<std::path::Path>) -> Config {
1138        let dir = dir.as_ref();
1139        let a = dir.join("a");
1140        let b = dir.join("b");
1141        config_from(&format!(
1142            r#"
1143            [challenge]
1144            enabled = ["http-01"]
1145            bypass = true
1146
1147            [profiles.a]
1148            signer.local_ca.cert_path = "{a}.pem"
1149            signer.local_ca.key_path = "{a}.key"
1150            signer.local_ca.crl_path = "{a}.crl"
1151
1152            [profiles.b]
1153            challenge.bypass = false
1154            signer.local_ca.cert_path = "{b}.pem"
1155            signer.local_ca.key_path = "{b}.key"
1156            signer.local_ca.crl_path = "{b}.crl"
1157            "#,
1158            a = a.display(),
1159            b = b.display(),
1160        ))
1161    }
1162
1163    async fn database() -> Arc<Database> {
1164        Arc::new(Database::connect_in_memory().await.unwrap())
1165    }
1166
1167    #[tokio::test]
1168    async fn build_all_assembles_every_endpoint_from_its_own_configuration() {
1169        let dir = crate::testutil::TempDir::new("build");
1170        let config = two_profiles_config(&dir);
1171
1172        let profiles = Profile::build_all(
1173            &config,
1174            database().await,
1175            &crate::testutil::idle_job_queue(database().await),
1176        )
1177        .unwrap();
1178        assert_eq!(profiles.len(), 2);
1179
1180        assert_eq!(profiles[0].name, "a");
1181        assert_eq!(profiles[0].path, "/profile/a");
1182        assert_eq!(profiles[0].base_url, "http://localhost:3000/profile/a");
1183        // `a` inherits the global challenge section wholesale…
1184        assert!(profiles[0].challenges.is_bypassed());
1185        // …while `b` overrides one key of it and keeps the rest.
1186        assert!(!profiles[1].challenges.is_bypassed());
1187        assert_eq!(profiles[1].challenges.enabled_types(), ["http-01"]);
1188    }
1189
1190    #[tokio::test]
1191    async fn build_all_refuses_a_configuration_that_mounts_nothing() {
1192        let config = config_from("[server]\nbase_url = \"http://acme.test\"\n");
1193        let error = match Profile::build_all(
1194            &config,
1195            database().await,
1196            &crate::testutil::idle_job_queue(database().await),
1197        ) {
1198            Err(error) => error.to_string(),
1199            Ok(_) => panic!("a server with no endpoint must not start"),
1200        };
1201        assert!(error.contains("[profiles.default]"), "{error}");
1202    }
1203
1204    /// A subsystem that cannot be built names the endpoint it belongs to —
1205    /// with several mounted, "unknown challenge type" alone would not say where.
1206    #[tokio::test]
1207    async fn build_all_names_the_profile_a_failure_came_from() {
1208        let config = config_from(
1209            r#"
1210            [profiles.le]
1211            challenge.enabled = ["not-a-challenge"]
1212            "#,
1213        );
1214        let error = match Profile::build_all(
1215            &config,
1216            database().await,
1217            &crate::testutil::idle_job_queue(database().await),
1218        ) {
1219            Err(error) => error.to_string(),
1220            Ok(_) => panic!("an unknown challenge type is a startup error"),
1221        };
1222        assert!(error.contains("profile `le`"), "{error}");
1223        assert!(error.contains("not-a-challenge"), "{error}");
1224    }
1225
1226    /// A request deadline shorter than a hook that runs inside the request is a
1227    /// misconfiguration that would look like an intermittent CA outage: a
1228    /// validation that was going to succeed gets cut off and reported to the
1229    /// client as a server failure. Refuse to start and name both numbers.
1230    #[tokio::test]
1231    async fn build_all_refuses_a_deadline_shorter_than_an_inline_hook() {
1232        let config = config_from(
1233            r#"
1234            [server]
1235            request_timeout_ms = 1000
1236
1237            [profiles.le]
1238            challenge.timeout_ms = 5000
1239            "#,
1240        );
1241        let error = match Profile::build_all(
1242            &config,
1243            database().await,
1244            &crate::testutil::idle_job_queue(database().await),
1245        ) {
1246            Err(error) => error.to_string(),
1247            Ok(_) => panic!("a deadline below challenge.timeout_ms is a startup error"),
1248        };
1249        assert!(error.contains("profile `le`"), "{error}");
1250        assert!(error.contains("request_timeout_ms"), "{error}");
1251        assert!(error.contains("challenge.timeout_ms"), "{error}");
1252    }
1253
1254    /// The same check must not fire on `signer.custom.timeout_ms` when that
1255    /// backend is not the one installed — an unused `[signer.custom]` section
1256    /// says nothing about how long this profile's requests take.
1257    #[tokio::test]
1258    async fn an_unused_custom_signer_timeout_does_not_constrain_the_deadline() {
1259        let config = config_from(
1260            r#"
1261            [server]
1262            request_timeout_ms = 2000
1263
1264            [signer.custom]
1265            script_path = "/bin/true"
1266            timeout_ms = 30000
1267
1268            [profiles.le]
1269            challenge.timeout_ms = 1000
1270            "#,
1271        );
1272        assert!(
1273            Profile::build_all(
1274                &config,
1275                database().await,
1276                &crate::testutil::idle_job_queue(database().await)
1277            )
1278            .is_ok()
1279        );
1280    }
1281}