Skip to main content

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`, or empty for no inventory at all. Anything else is
20    /// 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}
30
31impl Default for IpamConfig {
32    fn default() -> Self {
33        Self {
34            backend: String::new(),
35            timeout_ms: 5000,
36            netbox: NetboxConfig::default(),
37            phpipam: PhpIpamConfig::default(),
38        }
39    }
40}
41
42/// The default `sources` both backends ship with: the address object's own
43/// name, the custom field on it, and that same field on the machine it is
44/// assigned to.
45///
46/// Exactly the behaviour of the `netbox` filter before it became an IPAM
47/// backend, so an existing deployment that moves its section across sees no
48/// change. The two service-address sources are deliberately *not* here: both
49/// widen what a client may certify, and widening is opted into.
50fn default_sources() -> Vec<String> {
51    vec![
52        "dns_name".to_string(),
53        "custom_field".to_string(),
54        "device".to_string(),
55    ]
56}
57
58/// Configuration for the `netbox` IPAM backend.
59///
60/// `token` is a secret, so it belongs in `ACME_PROXY_IPAM__NETBOX__TOKEN`
61/// rather than in a file on disk — the same advice
62/// [`super::signer::Rfc2136Config::tsig_key_secret`] carries.
63#[derive(Debug, Clone, Deserialize)]
64#[serde(default)]
65pub struct NetboxConfig {
66    /// Base URL of the NetBox instance, e.g. `https://netbox.example.com`. Any
67    /// path is kept, so an instance served under a subpath works.
68    pub url: String,
69    /// NetBox API token, sent as `Authorization: Token <token>`.
70    pub token: String,
71    /// Custom field, on the IP address or on its device/VM, holding the extra
72    /// names that address may have certified.
73    pub custom_field: String,
74    /// Which places a permitted name may come from. Empty, or an unknown
75    /// entry, is a startup error; order is meaningless, since the result is a
76    /// union. See [`crate::ipam::Source`].
77    #[serde(deserialize_with = "empty_string_is_no_values")]
78    pub sources: Vec<String>,
79    /// Which NetBox address roles count as a service address for the `vip`
80    /// source. Read only when `vip` is in `sources`, so this is *which* roles
81    /// rather than whether to look at all.
82    #[serde(deserialize_with = "empty_string_is_no_values")]
83    pub vip_roles: Vec<String>,
84    /// Extra CA certificates (PEM) to trust on top of the public roots, for a
85    /// NetBox behind an internal PKI. Ignored when `insecure_skip_verify` is on.
86    pub ca_cert_path: String,
87    /// Skip verification of NetBox's TLS certificate entirely.
88    ///
89    /// Off by default and meant as a temporary way out of an expired NetBox
90    /// certificate: with it on, the answers this backend trusts could come from
91    /// anyone able to intercept the connection. Startup logs a warning for as
92    /// long as it is set.
93    pub insecure_skip_verify: bool,
94}
95
96impl Default for NetboxConfig {
97    fn default() -> Self {
98        Self {
99            url: String::new(),
100            token: String::new(),
101            custom_field: "acme_allowed_names".to_string(),
102            sources: default_sources(),
103            // The roles NetBox itself offers for a shared address. Listing them
104            // all is not a widening: nothing is queried at all unless `vip` is
105            // in `sources`.
106            vip_roles: vec![
107                "vip".to_string(),
108                "vrrp".to_string(),
109                "hsrp".to_string(),
110                "glbp".to_string(),
111                "carp".to_string(),
112                "anycast".to_string(),
113            ],
114            ca_cert_path: String::new(),
115            insecure_skip_verify: false,
116        }
117    }
118}
119
120/// Configuration for the `phpipam` IPAM backend.
121///
122/// `token` is the application's static app code and is a secret, so it belongs
123/// in `ACME_PROXY_IPAM__PHPIPAM__TOKEN`.
124#[derive(Debug, Clone, Deserialize)]
125#[serde(default)]
126pub struct PhpIpamConfig {
127    /// Base URL of the phpIPAM instance, e.g. `https://ipam.example.com`. Any
128    /// path is kept, so an instance served under a subpath works.
129    pub url: String,
130    /// The API application's identifier, the `<app_id>` in every phpIPAM API
131    /// path. Created in phpIPAM under Administration → API.
132    pub app_id: String,
133    /// The application's app code, sent as a `token` header.
134    pub token: String,
135    /// Custom field on the address (and, for the `device` source, on the
136    /// device) holding the extra names it may have certified. phpIPAM prefixes
137    /// custom columns with `custom_`, so the default carries that prefix.
138    pub custom_field: String,
139    /// Which places a permitted name may come from. phpIPAM records no
140    /// redundancy groups, so `vip` and `fhrp` are refused here by name rather
141    /// than quietly ignored.
142    #[serde(deserialize_with = "empty_string_is_no_values")]
143    pub sources: Vec<String>,
144    /// Extra CA certificates (PEM) to trust on top of the public roots.
145    /// Ignored when `insecure_skip_verify` is on.
146    pub ca_cert_path: String,
147    /// Skip verification of phpIPAM's TLS certificate entirely. The
148    /// counterpart of [`NetboxConfig::insecure_skip_verify`], warned about at
149    /// startup for as long as it is set.
150    pub insecure_skip_verify: bool,
151}
152
153impl Default for PhpIpamConfig {
154    fn default() -> Self {
155        Self {
156            url: String::new(),
157            app_id: "acme".to_string(),
158            token: String::new(),
159            custom_field: "custom_acme_allowed_names".to_string(),
160            sources: default_sources(),
161            ca_cert_path: String::new(),
162            insecure_skip_verify: false,
163        }
164    }
165}