Skip to main content

acme_proxy/
lib.rs

1//! ACME (RFC 8555) Server Implementation
2//!
3//! This is a server-side implementation of the ACME protocol (RFC 8555) for
4//! issuing and managing SSL/TLS certificates. It serves as a backend for
5//! certificate clients like certbot and acme.sh.
6//!
7//! ## Features
8//!
9//! - The full RFC 8555 flow: directory, newNonce, newAccount, account
10//!   lookup/update and deactivation, newOrder, authorizations and challenges,
11//!   finalize, certificate retrieval via signed POST-as-GET, and revocation
12//! - JWS signature verification for EC (ES256) and RSA (RS256) keys
13//! - Automatic nonce management with replay protection
14//! - Challenge validation behind pluggable validators (`http-01`, `dns-01`,
15//!   `tls-alpn-01`), with a configurable bypass
16//! - Certificate issuance behind a pluggable signer backend: a local CA (whose
17//!   key may live in a PKCS#11 token), a relay to an upstream ACME CA, or an
18//!   operator-supplied script
19//! - **Profiles** — several independent ACME endpoints in one process, each with
20//!   its own signer, filters, challenge validators and EAB policy
21//! - External Account Binding (§7.3.4), account key rollover (§7.3.5) and
22//!   Renewal Information (RFC 9773)
23//! - Access control behind a policy engine of named checks combined by boolean
24//!   rules, including an IPAM lookup (NetBox, phpIPAM or a script) asking the
25//!   inventory whether the client's own address owns the names it is requesting
26//! - An append-only audit trail of every issuance *and every refusal*
27//! - An optional web admin listener, and admin subcommands in the same binary
28//! - Optional Prometheus metrics on a third listener of their own
29//! - A durable job queue, so work the server owes itself survives a restart and
30//!   an upstream blip is retried rather than invalidating a client's order
31//! - Configuration reload on `SIGHUP` — a rebuild and a swap, with
32//!   `database.url` the only key that still needs a restart
33//! - `SQLite` or `PostgreSQL` persistence for accounts, nonces, orders and the
34//!   audit trail, and `acme-proxy transfer` to move between them
35//! - Configurable via TOML, environment variables, or defaults
36//!
37//! ## Architecture
38//!
39//! One binary over a workspace of library crates, each naming only the crates
40//! beneath it — so the layering is the compiler's to enforce. Bottom-up:
41//!
42//! - [`acme_proxy_core`] - Configuration, the ACME wire types (identifiers,
43//!   JWS, problem documents, routes), certificate parsing, EAB and key-change
44//!   verification, and the audit trail's vocabulary
45//! - [`acme_proxy_store`] - The storage layer over `SQLite` or `PostgreSQL`, one
46//!   module per table, and both embedded migration sets
47//! - [`acme_proxy_net`] - DNS, outbound HTTP and forward proxies, TLS, the
48//!   listeners, and the challenge validators (http-01, dns-01, tls-alpn-01)
49//! - [`acme_proxy_policy`] - The filter engine (who may ask for what) and the
50//!   IPAM inventories one of its checks consults
51//! - [`acme_proxy_jobs`] - The durable job queue, the notifications delivered
52//!   through it, the audit writer and the Prometheus metrics
53//! - [`acme_proxy_signer`] - The signing backends (local CA, ACME relay, custom
54//!   script) and their read side
55//! - [`acme_proxy_protocol`] - The ACME services, the extractors that verify a
56//!   signed request before a handler runs, the handlers, the middlewares, and
57//!   the routers
58//! - [`acme_proxy_admin`] - The operation layer both front ends dispatch to,
59//!   and the web admin panel over it
60//! - [`acme_proxy_server`] - The runtime: role processes, listeners,
61//!   configuration reload and logging
62//!
63//! This crate is the binary and its terminal front end: [`cli`], the `clap`
64//! command tree.
65//!
66//! ## Usage
67//!
68//! The main entry point is `router::build_app()`, which mounts one ACME router
69//! per configured profile under `/profile/<name>` and serves the server-level
70//! routes (`/health`) at the root.
71//!
72//! ```rust,no_run
73//! use std::net::SocketAddr;
74//! use std::sync::Arc;
75//! use acme_proxy_protocol::profile::{Profile, ProfileParts};
76//! use acme_proxy_protocol::router::build_app;
77//! use acme_proxy_store::db::Database;
78//! use acme_proxy_signer as signer;
79//! use acme_proxy_jobs::{jobs, notify};
80//! use acme_proxy_policy::{filter, ipam};
81//! use acme_proxy_net::challenge;
82//! use acme_proxy_core::config::Config;
83//!
84//! #[tokio::main]
85//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
86//!     let config = Arc::new(Config::load()?);
87//!     let database = Arc::new(Database::connect_and_migrate(&config.database.url).await?);
88//!
89//!     let resolved = config.resolve_profiles()?;
90//!     // The resolver and the proxy policy, resolved before anything can dial:
91//!     // a proxy URL that cannot be understood must stop the process rather
92//!     // than leave egress elsewhere, and `dns.resolver` governs every outbound
93//!     // connection this server makes, not just challenge lookups. Bundled,
94//!     // because every outbound client takes them together — and because the
95//!     // rendering beside them is what tells a reload whether a signer backend
96//!     // has to be rebuilt.
97//!     let egress = Arc::new(acme_proxy_net::egress::Egress::from_config(&config)?);
98//!     let outbound = egress.outbound();
99//!     // The enqueue side of the durable queue, built first because everything
100//!     // below queues into it. A backend that defers issuance (`relay`) is
101//!     // handed one at construction, and so is every notify dispatcher — a
102//!     // notification is a job row too. The runner that drains it is started
103//!     // separately, below.
104//!     let job_queue = jobs::JobQueue::new(database.clone(), &config.jobs);
105//!     // Built once, up front: an asynchronous signer backend (`relay`)
106//!     // has no `Profile` to reach a notifier through from its background
107//!     // completion task, so it is handed this whole map instead — and so is
108//!     // the `NotifyJob` that performs the deliveries.
109//!     let mut notifiers = std::collections::HashMap::new();
110//!     for profile in &resolved {
111//!         notifiers.insert(
112//!             profile.name.clone(),
113//!             notify::from_config(
114//!                 &profile.name,
115//!                 &profile.sections.notify,
116//!                 outbound.clone(),
117//!                 &job_queue,
118//!             )?,
119//!         );
120//!     }
121//!     let notifiers = Arc::new(notifiers);
122//!     // The Prometheus counters. Built here rather than per generation, so a
123//!     // `SIGHUP` does not reset every counter to zero — see `Assembly`.
124//!     let metrics = Arc::new(acme_proxy_jobs::metrics::Metrics::new(database.clone()));
125//!
126//!     let mut profiles = Vec::new();
127//!     // Each profile's signer in two halves: the backend, which holds the key
128//!     // and is handed only to the job handlers below, and its read side, which
129//!     // is all a profile — and so a request — ever sees.
130//!     let mut backends = Vec::new();
131//!     for profile in &resolved {
132//!         let sections = &profile.sections;
133//!         let backend = signer::from_config(
134//!             &sections.signer,
135//!             &signer::SignerParts {
136//!                 database: database.clone(),
137//!                 notifiers: notifiers.clone().into(),
138//!                 metrics: metrics.clone(),
139//!                 egress: egress.clone(),
140//!                 jobs: job_queue.clone(),
141//!             },
142//!         )?;
143//!         backends.push((profile.name.clone(), backend.clone()));
144//!         profiles.push(Arc::new(Profile::new(
145//!             &profile.name,
146//!             &config.server.base_url,
147//!             ProfileParts {
148//!                 signer_info: backend.info(),
149//!                 filter: filter::from_config(
150//!                     &sections.filter,
151//!                     &config.dns,
152//!                     ipam::from_config(&sections.ipam, outbound.clone())?,
153//!                     sections.eab.enabled,
154//!                 )?,
155//!                 challenges: challenge::from_config(
156//!                     &sections.challenge,
157//!                     &config.dns,
158//!                     egress.proxies.clone(),
159//!                 )?,
160//!                 order: sections.order.clone(),
161//!                 eab: sections.eab.clone(),
162//!                 meta: sections.meta.clone(),
163//!                 notify: notifiers[&profile.name].clone(),
164//!             },
165//!         )));
166//!     }
167//!     // Process-wide, like `[audit]` itself: one trail for the whole CA,
168//!     // shared by every profile's router and by the web admin listener.
169//!     // The registry is a parameter rather than a builder step, so a serving
170//!     // process cannot build an auditor that counts into nothing. The counters
171//!     // come off the same `AuditRecord` the trail is written from, so the two
172//!     // can never disagree.
173//!     let audit = Arc::new(acme_proxy_jobs::auditor::Auditor::from_config(
174//!         &config.audit,
175//!         &config.dns,
176//!         database.clone(),
177//!         metrics.clone(),
178//!     )?);
179//!     // The queue goes in too: `POST /chall/{id}` claims a challenge and
180//!     // queues its validation rather than performing it inside the request.
181//!     let app = build_app(
182//!         database.clone(),
183//!         config.clone(),
184//!         profiles,
185//!         audit.clone(),
186//!         metrics.clone(),
187//!         job_queue.clone(),
188//!     );
189//!
190//!     // One runner drains the queue for the process. Every handler comes from
191//!     // a subsystem that has background work — relayed issuance, notification
192//!     // delivery, the periodic table sweeps — and the runner calls `recover`
193//!     // on each before it claims anything, which is how work a previous run
194//!     // left in flight is picked back up, and how each sweep's single row gets
195//!     // queued. **One handler per kind, never per backend**: a handler covers
196//!     // every profile or backend of its kind and picks the right one per row,
197//!     // since `register` refuses a second handler for a kind it already has.
198//!     let mut registry = jobs::JobRegistry::new();
199//!     // `finalize` queues the signing: this is the one handler that asks a
200//!     // backend to issue, and the one place a backend is handed out.
201//!     registry.register(Arc::new(acme_proxy_protocol::acme::issue::SignerIssueJob::new(
202//!         database.clone(),
203//!         audit,
204//!         backends,
205//!         notifiers.clone().into(),
206//!     )))?;
207//!     registry.register(Arc::new(notify::NotifyJob::new(notifiers)))?;
208//!     registry.register(Arc::new(jobs::SweepJob::nonces(
209//!         database.clone(),
210//!         std::time::Duration::from_secs(config.nonce.ttl_seconds),
211//!     )))?;
212//!     let (_shutdown, shutdown_rx) = tokio::sync::watch::channel(false);
213//!     jobs::spawn_runner(job_queue, Arc::new(registry), &config.jobs, shutdown_rx);
214//!
215//!     let listener = tokio::net::TcpListener::bind(&config.server.bind_address).await?;
216//!     axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>()).await?;
217//!
218//!     Ok(())
219//! }
220//! ```
221
222pub mod cli;
223
224// Re-export name shape helpers for backwards compatibility