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}