acme_proxy/config/types/ipam.rs
1//! `[ipam]` — the IP address management inventory this endpoint consults.
2//!
3//! Re-exported flat from [`super`], so nothing outside this directory names
4//! the submodule.
5
6use serde::Deserialize;
7
8use super::empty_string_is_no_values;
9
10/// Which inventory answers "which names does this address own?", and how long
11/// it is given to answer.
12///
13/// Per-profile, so two endpoints may consult different inventories — but the
14/// *policy* built on the answer is the `ipam` filter's, not this section's.
15/// Nothing here is read unless `ipam` appears in `filter.enabled`.
16#[derive(Debug, Clone, Deserialize)]
17#[serde(default)]
18pub struct IpamConfig {
19 /// `netbox`, `phpipam`, `custom`, or empty for no inventory at all.
20 /// Anything else is a startup error rather than a silent fallback.
21 pub backend: String,
22 /// Budget for one whole lookup, however many requests the backend makes to
23 /// answer it. Applied by the registry rather than by each backend, so a
24 /// backend added later cannot forget it. This runs inline in `newOrder`
25 /// and `finalize`, so it is part of those requests' worst case.
26 pub timeout_ms: u64,
27 pub netbox: NetboxConfig,
28 pub phpipam: PhpIpamConfig,
29 pub custom: CustomIpamConfig,
30}
31
32impl Default for IpamConfig {
33 fn default() -> Self {
34 Self {
35 backend: String::new(),
36 timeout_ms: 5000,
37 netbox: NetboxConfig::default(),
38 phpipam: PhpIpamConfig::default(),
39 custom: CustomIpamConfig::default(),
40 }
41 }
42}
43
44/// The default `sources` both backends ship with: the address object's own
45/// name, the custom field on it, and that same field on the machine it is
46/// assigned to.
47///
48/// Exactly the behaviour of the `netbox` filter before it became an IPAM
49/// backend, so an existing deployment that moves its section across sees no
50/// change. The two service-address sources are deliberately *not* here: both
51/// widen what a client may certify, and widening is opted into.
52fn default_sources() -> Vec<String> {
53 vec![
54 "dns_name".to_string(),
55 "custom_field".to_string(),
56 "device".to_string(),
57 ]
58}
59
60/// Configuration for the `netbox` IPAM backend.
61///
62/// `token` is a secret, so it belongs in `ACME_PROXY_IPAM__NETBOX__TOKEN`
63/// rather than in a file on disk — the same advice
64/// [`super::signer::Rfc2136Config::tsig_key_secret`] carries.
65#[derive(Debug, Clone, Deserialize)]
66#[serde(default)]
67pub struct NetboxConfig {
68 /// Base URL of the NetBox instance, e.g. `https://netbox.example.com`. Any
69 /// path is kept, so an instance served under a subpath works.
70 pub url: String,
71 /// NetBox API token. Both generations are accepted and the scheme
72 /// follows the token itself: a v2 one (`nbt_<key>.<secret>`, the
73 /// default since NetBox 4.5) is sent as `Authorization: Bearer …`,
74 /// a legacy v1 one as `Authorization: Token …`.
75 pub token: String,
76 /// Custom field, on the IP address or on its device/VM, holding the extra
77 /// names that address may have certified.
78 pub custom_field: String,
79 /// Which places a permitted name may come from. Empty, or an unknown
80 /// entry, is a startup error; order is meaningless, since the result is a
81 /// union. See [`crate::ipam::Source`].
82 #[serde(deserialize_with = "empty_string_is_no_values")]
83 pub sources: Vec<String>,
84 /// Which NetBox address roles count as a service address for the `vip`
85 /// source. Read only when `vip` is in `sources`, so this is *which* roles
86 /// rather than whether to look at all.
87 #[serde(deserialize_with = "empty_string_is_no_values")]
88 pub vip_roles: Vec<String>,
89 /// Extra CA certificates (PEM) to trust on top of the public roots, for a
90 /// NetBox behind an internal PKI. Ignored when `insecure_skip_verify` is on.
91 pub ca_cert_path: String,
92 /// Skip verification of NetBox's TLS certificate entirely.
93 ///
94 /// Off by default and meant as a temporary way out of an expired NetBox
95 /// certificate: with it on, the answers this backend trusts could come from
96 /// anyone able to intercept the connection. Startup logs a warning for as
97 /// long as it is set.
98 pub insecure_skip_verify: bool,
99}
100
101impl Default for NetboxConfig {
102 fn default() -> Self {
103 Self {
104 url: String::new(),
105 token: String::new(),
106 custom_field: "acme_domains".to_string(),
107 sources: default_sources(),
108 // The roles NetBox itself offers for a shared address. Listing them
109 // all is not a widening: nothing is queried at all unless `vip` is
110 // in `sources`.
111 vip_roles: vec![
112 "vip".to_string(),
113 "vrrp".to_string(),
114 "hsrp".to_string(),
115 "glbp".to_string(),
116 "carp".to_string(),
117 "anycast".to_string(),
118 ],
119 ca_cert_path: String::new(),
120 insecure_skip_verify: false,
121 }
122 }
123}
124
125/// Configuration for the `phpipam` IPAM backend.
126///
127/// `token` is the application's static app code and is a secret, so it belongs
128/// in `ACME_PROXY_IPAM__PHPIPAM__TOKEN`.
129#[derive(Debug, Clone, Deserialize)]
130#[serde(default)]
131pub struct PhpIpamConfig {
132 /// Base URL of the phpIPAM instance, e.g. `https://ipam.example.com`. Any
133 /// path is kept, so an instance served under a subpath works.
134 pub url: String,
135 /// The API application's identifier, the `<app_id>` in every phpIPAM API
136 /// path. Created in phpIPAM under Administration → API.
137 pub app_id: String,
138 /// The application's app code, sent as a `token` header.
139 pub token: String,
140 /// Custom field on the address (and, for the `device` source, on the
141 /// device) holding the extra names it may have certified. phpIPAM prefixes
142 /// custom columns with `custom_`, so the default carries that prefix.
143 pub custom_field: String,
144 /// Which places a permitted name may come from. phpIPAM records no
145 /// redundancy groups, so `vip` and `fhrp` are refused here by name rather
146 /// than quietly ignored.
147 #[serde(deserialize_with = "empty_string_is_no_values")]
148 pub sources: Vec<String>,
149 /// Extra CA certificates (PEM) to trust on top of the public roots.
150 /// Ignored when `insecure_skip_verify` is on.
151 pub ca_cert_path: String,
152 /// Skip verification of phpIPAM's TLS certificate entirely. The
153 /// counterpart of [`NetboxConfig::insecure_skip_verify`], warned about at
154 /// startup for as long as it is set.
155 pub insecure_skip_verify: bool,
156}
157
158impl Default for PhpIpamConfig {
159 fn default() -> Self {
160 Self {
161 url: String::new(),
162 app_id: "acme".to_string(),
163 token: String::new(),
164 custom_field: "custom_acme_domains".to_string(),
165 sources: default_sources(),
166 ca_cert_path: String::new(),
167 insecure_skip_verify: false,
168 }
169 }
170}
171
172/// Configuration for the `custom` IPAM backend.
173///
174/// The one backend with no URL, no credential and no `sources`: the script is
175/// the inventory, and it decides for itself where its answer comes from. It
176/// also takes no `timeout_ms` of its own — [`IpamConfig::timeout_ms`] is the
177/// budget the whole lookup runs under, and a second one here would contradict
178/// the "one budget however many requests it takes" rule the other two follow.
179#[derive(Debug, Clone, Default, Deserialize)]
180#[serde(default)]
181pub struct CustomIpamConfig {
182 /// Path to the executable answering "which names does this address own?".
183 /// Empty while `backend = "custom"` is a startup error.
184 pub script_path: String,
185 /// Fixed arguments passed to the script before it is told anything about
186 /// the request, which travels in the environment and on stdin.
187 #[serde(deserialize_with = "empty_string_is_no_values")]
188 pub args: Vec<String>,
189}