Skip to main content

acme_proxy/config/types/
signer.rs

1//! `[signer]` and everything under it: the three backends, the local CA's key
2//! sources, and the relaying backend's upstream.
3//!
4//! Re-exported flat from [`super`], so nothing outside this directory names
5//! the submodule.
6
7use serde::Deserialize;
8
9use super::empty_string_is_no_values;
10
11/// Certificate-issuance signer configuration.
12///
13/// All three backends' tables are always present, whichever `backend` names —
14/// the unselected ones are simply never read, exactly as `local_ca`'s keys
15/// have always been parsed even when unused.
16#[derive(Debug, Clone, Deserialize)]
17#[serde(default)]
18pub struct SignerConfig {
19    pub backend: String,
20    pub local_ca: LocalCaConfig,
21    pub relay: RelayConfig,
22    pub custom: CustomSignerConfig,
23}
24
25impl Default for SignerConfig {
26    fn default() -> Self {
27        Self {
28            backend: "local_ca".to_string(),
29            local_ca: LocalCaConfig::default(),
30            relay: RelayConfig::default(),
31            custom: CustomSignerConfig::default(),
32        }
33    }
34}
35/// Configuration for the `custom` signer backend: issuance/revocation
36/// delegated to an external script (see [`crate::signer::custom`]).
37#[derive(Debug, Clone, Deserialize)]
38#[serde(default)]
39pub struct CustomSignerConfig {
40    pub script_path: String,
41    pub timeout_ms: u64,
42    #[serde(deserialize_with = "empty_string_is_no_values")]
43    pub args: Vec<String>,
44    /// Whether the script answers the `crl` hook (`GET /crl`). Off by
45    /// default: an unset script has nothing useful to say here, and the
46    /// trait's own default (`None`, "no CRL of my own to publish") already
47    /// covers that — same reasoning as `supports_renewal_info` below.
48    pub supports_crl: bool,
49    /// Whether the script answers the `renewal_info` hook (RFC 9773). Off by
50    /// default: the trait's own default (`Ok(None)`, "no opinion") already
51    /// falls back to this server's local ARI estimate, which is normal and
52    /// expected for a backend with nothing better to say.
53    pub supports_renewal_info: bool,
54}
55
56impl Default for CustomSignerConfig {
57    fn default() -> Self {
58        Self {
59            script_path: String::new(),
60            timeout_ms: 5000,
61            args: Vec::new(),
62            supports_crl: false,
63            supports_renewal_info: false,
64        }
65    }
66}
67/// Configuration for the `relay` signer backend: this server relaying
68/// issuance to a real upstream ACME server, of which it becomes a client.
69///
70/// The upstream account itself is provisioned once — either via `eab` below,
71/// or out of band via `acme-proxy upstream register` — and only the account
72/// key and the `kid` registration yields persist afterwards; see
73/// [`RelayEabConfig`].
74#[derive(Debug, Clone, Deserialize)]
75#[serde(default)]
76pub struct RelayConfig {
77    /// The upstream ACME server's directory URL.
78    pub directory_url: String,
79    /// This proxy's own account key at the upstream, generated (P-256) if the
80    /// file is absent. The `kid` the upstream assigns is stored beside it, in
81    /// the same path with its extension replaced by `.kid`.
82    pub account_key_path: String,
83    /// Optional contacts sent with `newAccount`.
84    #[serde(deserialize_with = "empty_string_is_no_values")]
85    pub contact: Vec<String>,
86    /// How this proxy satisfies the upstream's own domain-control checks:
87    /// `bypass` (the upstream validates nothing — a private CA, or another
88    /// acme-proxy with `challenge.bypass`) or `dns01` (publish the TXT record
89    /// the upstream asks for, which is what a public CA requires).
90    pub challenge_strategy: String,
91    /// How often to poll an upstream order/authorization while it resolves.
92    pub poll_interval_ms: u64,
93    /// Total budget for one upstream issuance, after which the local order is
94    /// marked `invalid` rather than left processing forever.
95    pub poll_timeout_secs: u64,
96    pub dns01: Dns01Config,
97    pub eab: RelayEabConfig,
98}
99
100impl Default for RelayConfig {
101    fn default() -> Self {
102        Self {
103            directory_url: String::new(),
104            account_key_path: "upstream_account.key".to_string(),
105            contact: Vec::new(),
106            challenge_strategy: "bypass".to_string(),
107            poll_interval_ms: 2000,
108            poll_timeout_secs: 300,
109            dns01: Dns01Config::default(),
110            eab: RelayEabConfig::default(),
111        }
112    }
113}
114/// This proxy's own upstream External Account Binding credential (RFC 8555
115/// §7.3.4), as an alternative to `acme-proxy upstream register --eab-kid
116/// <kid>`.
117///
118/// Both are the *same* one-shot credential: it authorizes exactly one
119/// `newAccount` call and is useless afterwards — registration itself only
120/// ever runs once, guarded by the `.kid` sidecar next to `account_key_path`
121/// (see [`RelayConfig`]). Putting it here trades away the property that
122/// made `acme-proxy upstream register` the only path (a bootstrap secret
123/// living in configuration for the life of the server) for the convenience
124/// of not needing a separate imperative step — useful when `config.toml` is
125/// already populated by a secrets manager or a templated deployment. Once
126/// registration succeeds, `serve` logs a
127/// `signer_relay_eab_secret_in_config` warning on **every** startup for
128/// as long as `hmac_key` stays non-empty, the same "stays visible for as long
129/// as it lasts" treatment `challenge.bypass` and
130/// `filter.netbox.insecure_skip_verify` get — the fix is to blank it out
131/// (`acme-proxy upstream show` confirms the `kid` is already stored).
132///
133/// Leaving both fields empty (the default) is unchanged from before this
134/// existed: `serve` then requires `acme-proxy upstream register` if the
135/// upstream demands EAB.
136#[derive(Debug, Clone, Default, Deserialize)]
137#[serde(default)]
138pub struct RelayEabConfig {
139    /// The EAB key id the upstream's operator issued. Empty means "no
140    /// config-file credential".
141    pub kid: String,
142    /// SENSITIVE — prefer the environment variable to a file on disk, like
143    /// every other secret in this file. Base64: url-safe, unpadded url-safe,
144    /// or standard (the same three forms `acme-proxy upstream register`
145    /// accepts) — a value that decodes as none of them is a startup error.
146    pub hmac_key: String,
147}
148/// Which DNS provider the `dns01` challenge strategy writes through.
149#[derive(Debug, Clone, Deserialize)]
150#[serde(default)]
151pub struct Dns01Config {
152    pub provider: String,
153    pub rfc2136: Rfc2136Config,
154}
155
156impl Default for Dns01Config {
157    fn default() -> Self {
158        Self {
159            provider: "rfc2136".to_string(),
160            rfc2136: Rfc2136Config::default(),
161        }
162    }
163}
164/// RFC 2136 dynamic DNS update, authenticated with TSIG.
165#[derive(Debug, Clone, Default, Deserialize)]
166#[serde(default)]
167pub struct Rfc2136Config {
168    /// `host:port` of the authoritative server accepting the update.
169    pub server: String,
170    /// The zone to update, e.g. `example.org.`.
171    pub zone: String,
172    pub tsig_key_name: String,
173    /// Base64 TSIG secret. Unlike the EAB secret this *is* long-lived — every
174    /// update needs it — so it legitimately lives in configuration; prefer the
175    /// environment variable over a file on disk.
176    pub tsig_key_secret: String,
177    pub tsig_algorithm: String,
178}
179/// Configuration for the persistent local-CA signer backend.
180#[derive(Debug, Clone, Deserialize)]
181#[serde(default)]
182pub struct LocalCaConfig {
183    pub cert_path: String,
184    pub key_path: String,
185    pub key_type: String,
186    pub leaf_validity_days: u64,
187    pub crl_path: String,
188    /// Where a relying party can fetch the CRL, written into every issued leaf
189    /// as `cRLDistributionPoints` (RFC 5280 §4.2.1.13). Empty (the default)
190    /// emits no extension at all.
191    ///
192    /// Not derived from `server.base_url`: the value is frozen into every
193    /// certificate signed while it is set, so a `base_url` change or a profile
194    /// rename would silently break certificates already issued — and the
195    /// per-profile `/crl` sits behind that profile's filter chain, which an
196    /// address-based policy will refuse to a relying party. Several entries
197    /// mean one CRL reachable at several places, not several CRLs.
198    ///
199    /// Validated (and encoded) in `LocalCa::load_or_generate`, not here: the
200    /// list-key round-trip test feeds placeholder values through every entry of
201    /// `LIST_KEYS`.
202    #[serde(deserialize_with = "empty_string_is_no_values")]
203    pub crl_distribution_points: Vec<String>,
204    /// Where a relying party can fetch this CA's own certificate, written into
205    /// every issued leaf as `authorityInfoAccess` / `caIssuers` (RFC 5280
206    /// §4.2.2.1). Empty (the default) emits no extension at all. Same
207    /// "operator names it, nothing derives it" reasoning as
208    /// `crl_distribution_points` above.
209    #[serde(deserialize_with = "empty_string_is_no_values")]
210    pub ca_issuer_urls: Vec<String>,
211    /// Overrides for the autogenerated CA's own X.509 Subject. Read only
212    /// when the CA is generated (no existing `cert_path`/`key_path`) — an
213    /// already-on-disk CA's Subject is whatever it already has, re-signing
214    /// nothing.
215    pub subject: LocalCaSubjectConfig,
216    /// Where the issuing private key lives: `"file"` (the default, and the
217    /// only behaviour that existed before this key) or `"pkcs11"`.
218    ///
219    /// A selector string rather than a `pkcs11.enabled` flag, matching
220    /// `signer.backend` and `signer.relay.challenge_strategy`: it makes
221    /// "both configured" unrepresentable instead of a precedence rule.
222    pub key_source: String,
223    /// The token to sign with when `key_source = "pkcs11"`. Ignored
224    /// otherwise — `Config` cannot tell an unset table from a defaulted one,
225    /// so validation lives in `LocalCa::load_or_generate`, where the selector
226    /// that makes these fields required is also in scope.
227    pub pkcs11: Pkcs11Config,
228}
229
230impl Default for LocalCaConfig {
231    fn default() -> Self {
232        Self {
233            cert_path: "ca.pem".to_string(),
234            key_path: "ca.key".to_string(),
235            key_type: "ecdsa-p256".to_string(),
236            leaf_validity_days: 90,
237            crl_path: "ca.crl".to_string(),
238            crl_distribution_points: Vec::new(),
239            ca_issuer_urls: Vec::new(),
240            subject: LocalCaSubjectConfig::default(),
241            key_source: "file".to_string(),
242            pkcs11: Pkcs11Config::default(),
243        }
244    }
245}
246/// A PKCS#11 token holding the local CA's issuing key
247/// (`signer.local_ca.key_source = "pkcs11"`, requires `--features hsm`).
248///
249/// The private key never leaves the token: this server sends it the bytes to
250/// be signed and gets a signature back. Consequently the CA is **never
251/// generated** in this mode — `cert_path` must already hold a certificate for
252/// the token's key, and `key_path` is not read or written at all.
253/// Every field defaults to "unset" — unlike its neighbours, none of these has
254/// a useful compiled-in value, so the `Default` is derived rather than written
255/// out. Which of them are *required* depends on `key_source`, and is checked in
256/// `LocalCa::load_or_generate` where that selector is in scope.
257#[derive(Debug, Clone, Default, Deserialize)]
258#[serde(default)]
259pub struct Pkcs11Config {
260    /// The PKCS#11 module to `dlopen`, e.g. `/usr/lib/softhsm/libsofthsm2.so`
261    /// or `/usr/lib/libykcs11.so`. Required once `key_source = "pkcs11"`.
262    pub module_path: String,
263    /// The token to use, by its label. Preferred over `slot_id`, which is not
264    /// stable across reboots or re-plugs on most drivers.
265    pub token_label: String,
266    /// The slot to use, when the token carries no usable label. Consulted
267    /// only if `token_label` is empty.
268    pub slot_id: Option<u64>,
269    /// `CKA_LABEL` of the private key. Required once `key_source = "pkcs11"`.
270    /// Note that on a YubiKey the labels are fixed by `libykcs11` (slot 9c is
271    /// `"Private key for Digital Signature"`), so this is looked up, not
272    /// chosen.
273    pub key_label: String,
274    /// `CKA_ID` as hex, to disambiguate a token carrying several keys under
275    /// one label. Optional; empty means "match on the label alone".
276    pub key_id: String,
277    /// The user PIN. **Secret** — prefer `pin_file`, or the
278    /// `ACME_PROXY_SIGNER__LOCAL_CA__PKCS11__PIN` environment variable, over
279    /// writing it here.
280    pub pin: String,
281    /// A file holding the user PIN, trailing whitespace trimmed. Wins over
282    /// `pin` when both are set.
283    pub pin_file: String,
284}
285/// Overrides for the autogenerated Local CA's X.509 Subject (Distinguished
286/// Name). Every field is optional and, when unset (or set to an empty
287/// string — the `config` crate cannot tell an env var explicitly set empty
288/// from one that's absent), is simply omitted from the Subject — except
289/// `common_name`, which falls back to `"acme-proxy local CA"` so the CA
290/// always carries one.
291#[derive(Debug, Clone, Default, Deserialize)]
292#[serde(default)]
293pub struct LocalCaSubjectConfig {
294    pub common_name: Option<String>,
295    pub organization: Option<String>,
296    pub organizational_unit: Option<String>,
297    pub country: Option<String>,
298    pub state: Option<String>,
299    pub locality: Option<String>,
300}