diff --git a/seaweed-volume/src/config.rs b/seaweed-volume/src/config.rs index b845c4078..49e2ffa5b 100644 --- a/seaweed-volume/src/config.rs +++ b/seaweed-volume/src/config.rs @@ -231,8 +231,10 @@ pub struct VolumeServerConfig { pub jwt_read_signing_expires_seconds: i64, pub https_cert_file: String, pub https_key_file: String, + pub https_ca_file: String, pub grpc_cert_file: String, pub grpc_key_file: String, + pub grpc_ca_file: String, /// Enable batched write queue for improved throughput under load. pub enable_write_queue: bool, } @@ -639,8 +641,10 @@ fn resolve_config(cli: Cli) -> VolumeServerConfig { jwt_read_signing_expires_seconds: sec.jwt_read_signing_expires, https_cert_file: sec.https_cert_file, https_key_file: sec.https_key_file, + https_ca_file: sec.https_ca_file, grpc_cert_file: sec.grpc_cert_file, grpc_key_file: sec.grpc_key_file, + grpc_ca_file: sec.grpc_ca_file, enable_write_queue: std::env::var("SEAWEED_WRITE_QUEUE") .map(|v| v == "1" || v == "true") .unwrap_or(false), @@ -656,8 +660,10 @@ pub struct SecurityConfig { pub jwt_read_signing_expires: i64, pub https_cert_file: String, pub https_key_file: String, + pub https_ca_file: String, pub grpc_cert_file: String, pub grpc_key_file: String, + pub grpc_ca_file: String, pub access_ui: bool, /// IPs from [guard] white_list in security.toml pub guard_white_list: Vec, @@ -759,11 +765,13 @@ fn parse_security_config(path: &str) -> SecurityConfig { Section::HttpsVolume => match key { "cert" => cfg.https_cert_file = value.to_string(), "key" => cfg.https_key_file = value.to_string(), + "ca" => cfg.https_ca_file = value.to_string(), _ => {} }, Section::GrpcVolume => match key { "cert" => cfg.grpc_cert_file = value.to_string(), "key" => cfg.grpc_key_file = value.to_string(), + "ca" => cfg.grpc_ca_file = value.to_string(), _ => {} }, Section::Guard => match key { diff --git a/seaweed-volume/src/main.rs b/seaweed-volume/src/main.rs index f1d3c5756..5ebfa93ab 100644 --- a/seaweed-volume/src/main.rs +++ b/seaweed-volume/src/main.rs @@ -47,8 +47,9 @@ fn main() { } } -/// Build a rustls ServerConfig from cert and key PEM files. -fn load_rustls_config(cert_path: &str, key_path: &str) -> rustls::ServerConfig { +/// Build a rustls ServerConfig from cert, key, and optional CA PEM files. +/// When `ca_path` is non-empty, enables mTLS (client certificate verification). +fn load_rustls_config(cert_path: &str, key_path: &str, ca_path: &str) -> rustls::ServerConfig { let cert_pem = std::fs::read(cert_path) .unwrap_or_else(|e| panic!("Failed to read TLS cert file '{}': {}", cert_path, e)); let key_pem = std::fs::read(key_path) @@ -61,10 +62,31 @@ fn load_rustls_config(cert_path: &str, key_path: &str) -> rustls::ServerConfig { .expect("Failed to parse TLS private key PEM") .expect("No private key found in PEM file"); - rustls::ServerConfig::builder() - .with_no_client_auth() - .with_single_cert(certs, key) - .expect("Failed to build rustls ServerConfig") + let builder = rustls::ServerConfig::builder(); + if !ca_path.is_empty() { + // mTLS: verify client certificates against the CA + let ca_pem = std::fs::read(ca_path) + .unwrap_or_else(|e| panic!("Failed to read CA cert file '{}': {}", ca_path, e)); + let ca_certs = rustls_pemfile::certs(&mut &ca_pem[..]) + .collect::, _>>() + .expect("Failed to parse CA certificate PEM"); + let mut root_store = rustls::RootCertStore::empty(); + for cert in ca_certs { + root_store.add(cert).expect("Failed to add CA certificate to root store"); + } + let verifier = rustls::server::WebPkiClientVerifier::builder(Arc::new(root_store)) + .build() + .expect("Failed to build client certificate verifier"); + builder + .with_client_cert_verifier(verifier) + .with_single_cert(certs, key) + .expect("Failed to build rustls ServerConfig with mTLS") + } else { + builder + .with_no_client_auth() + .with_single_cert(certs, key) + .expect("Failed to build rustls ServerConfig") + } } async fn run(config: VolumeServerConfig) -> Result<(), Box> { @@ -257,7 +279,7 @@ async fn run(config: VolumeServerConfig) -> Result<(), Box Result<(), Box