Skip to main content

smg_mesh/
mtls.rs

1//! mTLS (mutual TLS) support for mesh cluster communication
2//!
3//! Provides optional mTLS encryption for gRPC mesh connections using rustls.
4//! Supports certificate rotation without restart.
5
6use std::{
7    path::{Path, PathBuf},
8    sync::Arc,
9    time::Duration,
10};
11
12use anyhow::Result;
13use rustls::{
14    pki_types::{CertificateDer, PrivateKeyDer},
15    ClientConfig, RootCertStore, ServerConfig,
16};
17use rustls_pemfile::{certs, pkcs8_private_keys};
18use tokio::{fs, sync::RwLock};
19use tonic::transport::Certificate;
20use tracing::{info, warn};
21
22/// mTLS configuration
23#[derive(Debug, Clone)]
24pub struct MTLSConfig {
25    /// Path to CA certificate file
26    pub ca_cert_path: PathBuf,
27    /// Path to server certificate file
28    pub server_cert_path: PathBuf,
29    /// Path to server private key file
30    pub server_key_path: PathBuf,
31    /// Whether to require client certificates
32    pub require_client_cert: bool,
33    /// Certificate rotation check interval
34    pub rotation_check_interval: Duration,
35}
36
37impl Default for MTLSConfig {
38    fn default() -> Self {
39        Self {
40            ca_cert_path: PathBuf::from("/etc/ssl/certs/ca-certificates.crt"),
41            server_cert_path: PathBuf::from("/etc/ssl/certs/server.crt"),
42            server_key_path: PathBuf::from("/etc/ssl/private/server.key"),
43            require_client_cert: true,
44            rotation_check_interval: Duration::from_secs(300), // 5 minutes
45        }
46    }
47}
48
49/// mTLS certificate manager
50#[derive(Debug)]
51pub struct MTLSManager {
52    config: MTLSConfig,
53    server_config: Arc<RwLock<Option<Arc<ServerConfig>>>>,
54    client_config: Arc<RwLock<Option<Arc<ClientConfig>>>>,
55}
56
57impl MTLSManager {
58    /// Create a new mTLS manager
59    pub fn new(config: MTLSConfig) -> Self {
60        Self {
61            config,
62            server_config: Arc::new(RwLock::new(None)),
63            client_config: Arc::new(RwLock::new(None)),
64        }
65    }
66
67    /// Load server TLS configuration
68    pub async fn load_server_config(&self) -> Result<Arc<ServerConfig>> {
69        let certs = self.load_certs(&self.config.server_cert_path).await?;
70        let key = self.load_private_key(&self.config.server_key_path).await?;
71
72        let mut server_config = ServerConfig::builder()
73            .with_no_client_auth()
74            .with_single_cert(certs, key)?;
75
76        // Enable ALPN for HTTP/2
77        server_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
78
79        let config = Arc::new(server_config);
80        *self.server_config.write().await = Some(config.clone());
81        Ok(config)
82    }
83
84    /// Load client TLS configuration
85    pub async fn load_client_config(&self) -> Result<Arc<ClientConfig>> {
86        let mut root_store = RootCertStore::empty();
87
88        // Load CA certificate
89        let ca_certs = self.load_certs(&self.config.ca_cert_path).await?;
90        for cert in ca_certs {
91            root_store.add(cert)?;
92        }
93
94        let mut client_config = ClientConfig::builder()
95            .with_root_certificates(root_store)
96            .with_no_client_auth();
97
98        // Enable ALPN for HTTP/2
99        client_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
100
101        let config = Arc::new(client_config);
102        *self.client_config.write().await = Some(config.clone());
103        Ok(config)
104    }
105
106    /// Load CA certificate for tonic client TLS configuration
107    pub async fn load_ca_certificate(&self) -> Result<Certificate> {
108        let ca_cert = fs::read(&self.config.ca_cert_path).await?;
109        Ok(Certificate::from_pem(ca_cert))
110    }
111
112    /// Load certificates from file
113    async fn load_certs(&self, path: &Path) -> Result<Vec<CertificateDer<'static>>> {
114        let cert_data = fs::read(path).await?;
115        let certs = certs(&mut cert_data.as_slice()).collect::<Result<Vec<_>, _>>()?;
116        Ok(certs)
117    }
118
119    /// Load private key from file
120    async fn load_private_key(&self, path: &Path) -> Result<PrivateKeyDer<'static>> {
121        let key_data = fs::read(path).await?;
122        let mut keys =
123            pkcs8_private_keys(&mut key_data.as_slice()).collect::<Result<Vec<_>, _>>()?;
124
125        if keys.is_empty() {
126            return Err(anyhow::anyhow!("No private key found in file"));
127        }
128
129        Ok(PrivateKeyDer::Pkcs8(keys.remove(0)))
130    }
131
132    /// Start certificate rotation monitoring
133    #[expect(
134        clippy::disallowed_methods,
135        reason = "fire-and-forget background monitor; rotation runs for the process lifetime and does not need explicit join"
136    )]
137    pub fn start_rotation_monitor(&self) {
138        let config = self.config.clone();
139        let server_config = self.server_config.clone();
140        let client_config = self.client_config.clone();
141
142        tokio::spawn(async move {
143            let mut interval = tokio::time::interval(config.rotation_check_interval);
144            loop {
145                interval.tick().await;
146
147                // Check if certificates have changed
148                if let Err(e) =
149                    Self::check_and_reload_certs(&config, &server_config, &client_config).await
150                {
151                    warn!("Error checking certificate rotation: {}", e);
152                }
153            }
154        });
155    }
156
157    /// Check and reload certificates if they have changed
158    async fn check_and_reload_certs(
159        config: &MTLSConfig,
160        _server_config: &Arc<RwLock<Option<Arc<ServerConfig>>>>,
161        _client_config: &Arc<RwLock<Option<Arc<ClientConfig>>>>,
162    ) -> Result<()> {
163        // Get file modification times
164        let server_cert_mtime = fs::metadata(&config.server_cert_path).await?.modified()?;
165        let server_key_mtime = fs::metadata(&config.server_key_path).await?.modified()?;
166        let ca_cert_mtime = fs::metadata(&config.ca_cert_path).await?.modified()?;
167
168        // TODO: Compare with cached modification times
169        // For now, we'll just log that rotation monitoring is active
170        info!(
171            "Certificate rotation check: server_cert={:?}, server_key={:?}, ca_cert={:?}",
172            server_cert_mtime, server_key_mtime, ca_cert_mtime
173        );
174
175        // Reload if certificates have changed
176        // This is a simplified version - in production, you'd compare mtimes
177        Ok(())
178    }
179
180    /// Get current server config (for use with tonic)
181    pub async fn get_server_config(&self) -> Option<Arc<ServerConfig>> {
182        self.server_config.read().await.clone()
183    }
184
185    /// Get current client config (for use with tonic)
186    pub async fn get_client_config(&self) -> Option<Arc<ClientConfig>> {
187        self.client_config.read().await.clone()
188    }
189}
190
191/// Optional mTLS manager
192pub type OptionalMTLSManager = Option<Arc<MTLSManager>>;