Skip to main content

greentic_setup/admin/
tls.rs

1//! mTLS configuration for the admin API endpoint.
2//!
3//! Defines `AdminTlsConfig` for loading server and client certificates used
4//! to authenticate admin API consumers. The actual TLS server setup happens
5//! in the consuming crate (e.g. greentic-operator via `axum-server` + `rustls`).
6
7use std::path::PathBuf;
8
9use serde::{Deserialize, Serialize};
10
11/// TLS configuration for the admin API endpoint.
12///
13/// # Example
14///
15/// ```rust
16/// use greentic_setup::admin::AdminTlsConfig;
17///
18/// let config = AdminTlsConfig {
19///     server_cert: "/etc/greentic/admin/server.crt".into(),
20///     server_key: "/etc/greentic/admin/server.key".into(),
21///     client_ca: "/etc/greentic/admin/ca.crt".into(),
22///     allowed_clients: vec!["CN=greentic-admin".to_string()],
23///     port: 8443,
24/// };
25/// ```
26#[derive(Clone, Debug, Serialize, Deserialize)]
27pub struct AdminTlsConfig {
28    /// Path to the server TLS certificate (PEM).
29    pub server_cert: PathBuf,
30    /// Path to the server TLS private key (PEM).
31    pub server_key: PathBuf,
32    /// Path to the CA certificate for verifying client certificates (PEM).
33    pub client_ca: PathBuf,
34    /// Optional list of allowed client CN (Common Name) patterns.
35    ///
36    /// If empty, any client with a certificate signed by the CA is allowed.
37    #[serde(default)]
38    pub allowed_clients: Vec<String>,
39    /// Port to bind the admin API server on.
40    #[serde(default = "default_admin_port")]
41    pub port: u16,
42}
43
44fn default_admin_port() -> u16 {
45    8443
46}
47
48impl Default for AdminTlsConfig {
49    fn default() -> Self {
50        Self {
51            server_cert: PathBuf::from("admin/server.crt"),
52            server_key: PathBuf::from("admin/server.key"),
53            client_ca: PathBuf::from("admin/ca.crt"),
54            allowed_clients: Vec::new(),
55            port: default_admin_port(),
56        }
57    }
58}
59
60impl AdminTlsConfig {
61    /// Validate that all referenced certificate files exist.
62    pub fn validate(&self) -> anyhow::Result<()> {
63        for (label, path) in [
64            ("server_cert", &self.server_cert),
65            ("server_key", &self.server_key),
66            ("client_ca", &self.client_ca),
67        ] {
68            if !path.exists() {
69                return Err(anyhow::anyhow!(
70                    "admin TLS {label} not found: {}",
71                    path.display()
72                ));
73            }
74        }
75        Ok(())
76    }
77
78    /// Check whether a client Common Name is allowed by this config.
79    ///
80    /// Returns `true` if `allowed_clients` is empty (any client allowed)
81    /// or if the CN matches one of the patterns.
82    pub fn is_client_allowed(&self, cn: &str) -> bool {
83        if self.allowed_clients.is_empty() {
84            return true;
85        }
86        self.allowed_clients
87            .iter()
88            .any(|pattern| pattern == cn || pattern == "*")
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn default_port_is_8443() {
98        let config = AdminTlsConfig::default();
99        assert_eq!(config.port, 8443);
100    }
101
102    #[test]
103    fn empty_allowed_clients_allows_anyone() {
104        let config = AdminTlsConfig::default();
105        assert!(config.is_client_allowed("anything"));
106    }
107
108    #[test]
109    fn rejects_unlisted_client() {
110        let config = AdminTlsConfig {
111            allowed_clients: vec!["CN=admin".into()],
112            ..Default::default()
113        };
114        assert!(config.is_client_allowed("CN=admin"));
115        assert!(!config.is_client_allowed("CN=hacker"));
116    }
117
118    #[test]
119    fn wildcard_allows_all() {
120        let config = AdminTlsConfig {
121            allowed_clients: vec!["*".into()],
122            ..Default::default()
123        };
124        assert!(config.is_client_allowed("anyone"));
125    }
126
127    #[test]
128    fn validate_fails_for_missing_certs() {
129        let config = AdminTlsConfig {
130            server_cert: "/nonexistent/server.crt".into(),
131            server_key: "/nonexistent/server.key".into(),
132            client_ca: "/nonexistent/ca.crt".into(),
133            ..Default::default()
134        };
135        assert!(config.validate().is_err());
136    }
137}