diff --git a/seaweed-volume/Cargo.lock b/seaweed-volume/Cargo.lock index 7d1cd0c75..ffc7d31f3 100644 --- a/seaweed-volume/Cargo.lock +++ b/seaweed-volume/Cargo.lock @@ -86,7 +86,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -97,7 +97,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -106,6 +106,45 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "asn1-rs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -1046,6 +1085,12 @@ dependencies = [ "parking_lot_core 0.9.12", ] +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + [[package]] name = "debugid" version = "0.8.0" @@ -1076,6 +1121,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der-parser" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "deranged" version = "0.5.8" @@ -1264,7 +1323,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2309,6 +2368,12 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2397,6 +2462,16 @@ dependencies = [ "libc", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "ntapi" version = "0.4.3" @@ -2412,7 +2487,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2486,6 +2561,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "oid-registry" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -3337,6 +3421,15 @@ dependencies = [ "semver", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + [[package]] name = "rustix" version = "0.38.44" @@ -3360,7 +3453,7 @@ dependencies = [ "errno 0.3.14", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3670,7 +3763,7 @@ version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ - "errno 0.2.8", + "errno 0.3.14", "libc", ] @@ -3747,7 +3840,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3902,7 +3995,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4598,6 +4691,7 @@ name = "weed-volume" version = "0.1.0" dependencies = [ "anyhow", + "async-stream", "async-trait", "aws-config", "aws-credential-types", @@ -4655,6 +4749,7 @@ dependencies = [ "tracing", "tracing-subscriber", "uuid", + "x509-parser", ] [[package]] @@ -5076,6 +5171,23 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "x509-parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + [[package]] name = "xmlparser" version = "0.13.6" diff --git a/seaweed-volume/Cargo.toml b/seaweed-volume/Cargo.toml index 327c08239..d33563467 100644 --- a/seaweed-volume/Cargo.toml +++ b/seaweed-volume/Cargo.toml @@ -113,6 +113,8 @@ thiserror = "1" anyhow = "1" async-trait = "0.1" futures = "0.3" +async-stream = "0.3" +x509-parser = "0.16" # Disk space checking sysinfo = "0.31" diff --git a/seaweed-volume/src/config.rs b/seaweed-volume/src/config.rs index fac3b8135..34e06c0d3 100644 --- a/seaweed-volume/src/config.rs +++ b/seaweed-volume/src/config.rs @@ -247,6 +247,8 @@ pub struct VolumeServerConfig { pub grpc_cert_file: String, pub grpc_key_file: String, pub grpc_ca_file: String, + pub grpc_allowed_wildcard_domain: String, + pub grpc_volume_allowed_common_names: Vec, pub tls_policy: TlsPolicy, /// Enable batched write queue for improved throughput under load. pub enable_write_queue: bool, @@ -787,6 +789,8 @@ fn resolve_config(cli: Cli) -> VolumeServerConfig { grpc_cert_file: sec.grpc_cert_file, grpc_key_file: sec.grpc_key_file, grpc_ca_file: sec.grpc_ca_file, + grpc_allowed_wildcard_domain: sec.grpc_allowed_wildcard_domain, + grpc_volume_allowed_common_names: sec.grpc_volume_allowed_common_names, tls_policy: sec.tls_policy, enable_write_queue: std::env::var("SEAWEED_WRITE_QUEUE") .map(|v| v == "1" || v == "true") @@ -816,6 +820,8 @@ pub struct SecurityConfig { pub grpc_cert_file: String, pub grpc_key_file: String, pub grpc_ca_file: String, + pub grpc_allowed_wildcard_domain: String, + pub grpc_volume_allowed_common_names: Vec, pub tls_policy: TlsPolicy, pub access_ui: bool, /// IPs from [guard] white_list in security.toml @@ -853,10 +859,12 @@ const SECURITY_CONFIG_FILE_NAME: &str = "security.toml"; /// /// [grpc] /// ca = "/path/to/ca.pem" +/// allowed_wildcard_domain = ".example.com" /// /// [grpc.volume] /// cert = "/path/to/cert.pem" /// key = "/path/to/key.pem" +/// allowed_commonNames = "volume-a.internal,volume-b.internal" /// ``` pub fn parse_security_config(path: &str) -> SecurityConfig { let Some(config_path) = resolve_security_config_path(path) else { @@ -963,6 +971,9 @@ pub fn parse_security_config(path: &str) -> SecurityConfig { }, Section::Grpc => match key { "ca" => cfg.grpc_ca_file = value.to_string(), + "allowed_wildcard_domain" => { + cfg.grpc_allowed_wildcard_domain = value.to_string() + } _ => {} }, Section::HttpsVolume => match key { @@ -975,6 +986,10 @@ pub fn parse_security_config(path: &str) -> SecurityConfig { "cert" => cfg.grpc_cert_file = value.to_string(), "key" => cfg.grpc_key_file = value.to_string(), "ca" => cfg.grpc_ca_file = value.to_string(), + "allowed_commonNames" => { + cfg.grpc_volume_allowed_common_names = + value.split(',').map(|name| name.to_string()).collect(); + } _ => {} }, Section::Tls => match key { @@ -1097,6 +1112,12 @@ fn apply_env_overrides(cfg: &mut SecurityConfig) { } else if let Ok(v) = std::env::var("WEED_GRPC_VOLUME_CA") { cfg.grpc_ca_file = v; } + if let Ok(v) = std::env::var("WEED_GRPC_ALLOWED_WILDCARD_DOMAIN") { + cfg.grpc_allowed_wildcard_domain = v; + } + if let Ok(v) = std::env::var("WEED_GRPC_VOLUME_ALLOWED_COMMONNAMES") { + cfg.grpc_volume_allowed_common_names = v.split(',').map(|name| name.to_string()).collect(); + } if let Ok(v) = std::env::var("WEED_TLS_MIN_VERSION") { cfg.tls_policy.min_version = v; } @@ -1185,6 +1206,8 @@ mod tests { "WEED_GRPC_VOLUME_KEY", "WEED_GRPC_CA", "WEED_GRPC_VOLUME_CA", + "WEED_GRPC_ALLOWED_WILDCARD_DOMAIN", + "WEED_GRPC_VOLUME_ALLOWED_COMMONNAMES", "WEED_TLS_MIN_VERSION", "WEED_TLS_MAX_VERSION", "WEED_TLS_CIPHER_SUITES", @@ -1433,6 +1456,35 @@ key = "/etc/seaweedfs/volume-key.pem" }); } + #[test] + fn test_parse_security_config_uses_grpc_peer_name_policy() { + let _guard = process_state_lock(); + let tmp = tempfile::NamedTempFile::new().unwrap(); + std::fs::write( + tmp.path(), + r#" +[grpc] +allowed_wildcard_domain = ".example.com" + +[grpc.volume] +allowed_commonNames = "volume-a.internal,volume-b.internal" +"#, + ) + .unwrap(); + + with_cleared_security_env(|| { + let cfg = parse_security_config(tmp.path().to_str().unwrap()); + assert_eq!(cfg.grpc_allowed_wildcard_domain, ".example.com"); + assert_eq!( + cfg.grpc_volume_allowed_common_names, + vec![ + String::from("volume-a.internal"), + String::from("volume-b.internal") + ] + ); + }); + } + #[test] fn test_parse_security_config_uses_https_client_settings() { let _guard = process_state_lock(); diff --git a/seaweed-volume/src/main.rs b/seaweed-volume/src/main.rs index 1d8257813..750e39457 100644 --- a/seaweed-volume/src/main.rs +++ b/seaweed-volume/src/main.rs @@ -5,7 +5,10 @@ use tracing::{error, info, warn}; use seaweed_volume::config::{self, VolumeServerConfig}; use seaweed_volume::metrics; use seaweed_volume::pb::volume_server_pb::volume_server_server::VolumeServerServer; -use seaweed_volume::security::tls::build_rustls_server_config; +use seaweed_volume::security::tls::{ + build_rustls_server_config, build_rustls_server_config_with_grpc_client_auth, + GrpcClientAuthPolicy, TlsPolicy, +}; use seaweed_volume::security::{Guard, SigningKey}; use seaweed_volume::server::debug::build_debug_router; use seaweed_volume::server::grpc_client::load_outgoing_grpc_tls; @@ -121,43 +124,36 @@ fn build_outgoing_http_client( Ok((builder.build()?, scheme.to_string())) } -fn build_grpc_server_tls_config( +fn build_grpc_server_tls_acceptor( cert_path: &str, key_path: &str, ca_path: &str, -) -> Option { + tls_policy: &TlsPolicy, + allowed_wildcard_domain: &str, + allowed_common_names: &[String], +) -> Option { if cert_path.is_empty() || key_path.is_empty() || ca_path.is_empty() { return None; } - - let cert = match std::fs::read_to_string(cert_path) { - Ok(cert) => cert, - Err(e) => { - warn!("Failed to read gRPC cert '{}': {}", cert_path, e); - return None; - } - }; - let key = match std::fs::read_to_string(key_path) { - Ok(key) => key, - Err(e) => { - warn!("Failed to read gRPC key '{}': {}", key_path, e); - return None; - } + let client_auth_policy = GrpcClientAuthPolicy { + allowed_common_names: allowed_common_names.to_vec(), + allowed_wildcard_domain: allowed_wildcard_domain.to_string(), }; - let ca_cert = match std::fs::read_to_string(ca_path) { - Ok(ca_cert) => ca_cert, + let mut server_config = match build_rustls_server_config_with_grpc_client_auth( + cert_path, + key_path, + ca_path, + tls_policy, + &client_auth_policy, + ) { + Ok(server_config) => server_config, Err(e) => { - warn!("Failed to read gRPC CA cert '{}': {}", ca_path, e); + warn!("Failed to build gRPC TLS config: {}", e); return None; } }; - - let identity = tonic::transport::Identity::from_pem(cert, key); - Some( - tonic::transport::ServerTlsConfig::new() - .identity(identity) - .client_ca_root(tonic::transport::Certificate::from_pem(ca_cert)), - ) + server_config.alpn_protocols = vec![b"h2".to_vec()]; + Some(TlsAcceptor::from(Arc::new(server_config))) } async fn run(config: VolumeServerConfig) -> Result<(), Box> { @@ -305,11 +301,15 @@ async fn run(config: VolumeServerConfig) -> Result<(), Box Result<(), Box impl tokio_stream::Stream< + Item = Result, std::io::Error>, +> { + async_stream::stream! { + loop { + match listener.accept().await { + Ok((tcp_stream, remote_addr)) => match tls_acceptor.accept(tcp_stream).await { + Ok(tls_stream) => yield Ok(tls_stream), + Err(e) => { + tracing::debug!("gRPC TLS handshake failed from {}: {}", remote_addr, e); + } + }, + Err(e) => { + yield Err(e); + break; + } + } + } + } +} + /// Serve an axum Router over TLS using tokio-rustls. /// Accepts TCP connections, performs TLS handshake, then serves HTTP over the encrypted stream. async fn serve_https( @@ -713,7 +740,8 @@ async fn serve_https( #[cfg(test)] mod tests { - use super::build_grpc_server_tls_config; + use super::build_grpc_server_tls_acceptor; + use seaweed_volume::security::tls::TlsPolicy; fn write_pem(dir: &tempfile::TempDir, name: &str, body: &str) -> String { let path = dir.path().join(name); @@ -735,15 +763,55 @@ mod tests { "-----BEGIN PRIVATE KEY-----\nZmFrZQ==\n-----END PRIVATE KEY-----\n", ); - assert!(build_grpc_server_tls_config(&cert, &key, "").is_none()); + assert!( + build_grpc_server_tls_acceptor(&cert, &key, "", &TlsPolicy::default(), "", &[]) + .is_none() + ); } #[test] fn test_grpc_server_tls_returns_none_when_files_are_missing() { - assert!(build_grpc_server_tls_config( + assert!(build_grpc_server_tls_acceptor( "/missing/server.crt", "/missing/server.key", "/missing/ca.crt", + &TlsPolicy::default(), + "", + &[], + ) + .is_none()); + } + + #[test] + fn test_grpc_server_tls_disables_on_unsupported_tls_policy() { + let dir = tempfile::tempdir().unwrap(); + let cert = write_pem( + &dir, + "server.crt", + "-----BEGIN CERTIFICATE-----\nZmFrZQ==\n-----END CERTIFICATE-----\n", + ); + let key = write_pem( + &dir, + "server.key", + "-----BEGIN PRIVATE KEY-----\nZmFrZQ==\n-----END PRIVATE KEY-----\n", + ); + let ca = write_pem( + &dir, + "ca.crt", + "-----BEGIN CERTIFICATE-----\nZmFrZQ==\n-----END CERTIFICATE-----\n", + ); + + assert!(build_grpc_server_tls_acceptor( + &cert, + &key, + &ca, + &TlsPolicy { + min_version: "TLS 1.0".to_string(), + max_version: "TLS 1.1".to_string(), + cipher_suites: String::new(), + }, + "", + &[], ) .is_none()); } diff --git a/seaweed-volume/src/security/tls.rs b/seaweed-volume/src/security/tls.rs index 0b2664652..8f8cb2403 100644 --- a/seaweed-volume/src/security/tls.rs +++ b/seaweed-volume/src/security/tls.rs @@ -1,13 +1,19 @@ +use std::collections::HashSet; use std::fmt; use std::sync::Arc; +use rustls::client::danger::HandshakeSignatureValid; use rustls::crypto::aws_lc_rs; use rustls::crypto::CryptoProvider; +use rustls::pki_types::UnixTime; use rustls::pki_types::{CertificateDer, PrivateKeyDer}; +use rustls::server::danger::{ClientCertVerified, ClientCertVerifier}; use rustls::server::WebPkiClientVerifier; use rustls::{ - CipherSuite, RootCertStore, ServerConfig, SupportedCipherSuite, SupportedProtocolVersion, + CipherSuite, DigitallySignedStruct, DistinguishedName, RootCertStore, ServerConfig, + SignatureScheme, SupportedCipherSuite, SupportedProtocolVersion, }; +use x509_parser::prelude::{FromDer, X509Certificate}; #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct TlsPolicy { @@ -16,6 +22,12 @@ pub struct TlsPolicy { pub cipher_suites: String, } +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct GrpcClientAuthPolicy { + pub allowed_common_names: Vec, + pub allowed_wildcard_domain: String, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct TlsPolicyError(String); @@ -36,11 +48,107 @@ enum GoTlsVersion { Tls13, } +#[derive(Debug)] +struct CommonNameVerifier { + inner: Arc, + allowed_common_names: HashSet, + allowed_wildcard_domain: String, +} + +impl ClientCertVerifier for CommonNameVerifier { + fn offer_client_auth(&self) -> bool { + self.inner.offer_client_auth() + } + + fn client_auth_mandatory(&self) -> bool { + self.inner.client_auth_mandatory() + } + + fn root_hint_subjects(&self) -> &[DistinguishedName] { + self.inner.root_hint_subjects() + } + + fn verify_client_cert( + &self, + end_entity: &CertificateDer<'_>, + intermediates: &[CertificateDer<'_>], + now: UnixTime, + ) -> Result { + self.inner + .verify_client_cert(end_entity, intermediates, now)?; + let common_name = parse_common_name(end_entity).map_err(|e| { + rustls::Error::General(format!( + "parse client certificate common name failed: {}", + e + )) + })?; + if common_name_is_allowed( + &common_name, + &self.allowed_common_names, + &self.allowed_wildcard_domain, + ) { + return Ok(ClientCertVerified::assertion()); + } + Err(rustls::Error::General(format!( + "Authenticate: invalid subject client common name: {}", + common_name + ))) + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + self.inner.verify_tls12_signature(message, cert, dss) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + self.inner.verify_tls13_signature(message, cert, dss) + } + + fn supported_verify_schemes(&self) -> Vec { + self.inner.supported_verify_schemes() + } +} + pub fn build_rustls_server_config( cert_path: &str, key_path: &str, ca_path: &str, policy: &TlsPolicy, +) -> Result { + build_rustls_server_config_with_client_auth(cert_path, key_path, ca_path, policy, None) +} + +pub fn build_rustls_server_config_with_grpc_client_auth( + cert_path: &str, + key_path: &str, + ca_path: &str, + policy: &TlsPolicy, + client_auth_policy: &GrpcClientAuthPolicy, +) -> Result { + build_rustls_server_config_with_client_auth( + cert_path, + key_path, + ca_path, + policy, + Some(client_auth_policy), + ) +} + +fn build_rustls_server_config_with_client_auth( + cert_path: &str, + key_path: &str, + ca_path: &str, + policy: &TlsPolicy, + client_auth_policy: Option<&GrpcClientAuthPolicy>, ) -> Result { let cert_chain = read_cert_chain(cert_path)?; let private_key = read_private_key(key_path)?; @@ -55,9 +163,27 @@ pub fn build_rustls_server_config( builder.with_no_client_auth() } else { let roots = read_root_store(ca_path)?; - let verifier = WebPkiClientVerifier::builder_with_provider(Arc::new(roots), provider) - .build() - .map_err(|e| TlsPolicyError(format!("build client verifier failed: {}", e)))?; + let verifier = + WebPkiClientVerifier::builder_with_provider(Arc::new(roots), provider.clone()) + .build() + .map_err(|e| TlsPolicyError(format!("build client verifier failed: {}", e)))?; + let verifier: Arc = if let Some(client_auth_policy) = + client_auth_policy.filter(|policy| { + !policy.allowed_common_names.is_empty() + || !policy.allowed_wildcard_domain.is_empty() + }) { + Arc::new(CommonNameVerifier { + inner: verifier, + allowed_common_names: client_auth_policy + .allowed_common_names + .iter() + .cloned() + .collect(), + allowed_wildcard_domain: client_auth_policy.allowed_wildcard_domain.clone(), + }) + } else { + verifier + }; builder.with_client_cert_verifier(verifier) }; @@ -209,6 +335,30 @@ fn parse_cipher_suite_name(value: &str) -> Result { } } +fn parse_common_name(cert: &CertificateDer<'_>) -> Result { + let (_, certificate) = X509Certificate::from_der(cert.as_ref()) + .map_err(|e| TlsPolicyError(format!("parse X.509 certificate failed: {}", e)))?; + let common_name = certificate + .subject() + .iter_common_name() + .next() + .and_then(|common_name| common_name.as_str().ok()) + .map(str::to_string); + match common_name { + Some(common_name) => Ok(common_name), + None => Ok(String::new()), + } +} + +fn common_name_is_allowed( + common_name: &str, + allowed_common_names: &HashSet, + allowed_wildcard_domain: &str, +) -> bool { + (!allowed_wildcard_domain.is_empty() && common_name.ends_with(allowed_wildcard_domain)) + || allowed_common_names.contains(common_name) +} + fn go_tls_version_for_supported(version: &SupportedProtocolVersion) -> GoTlsVersion { match version.version { rustls::ProtocolVersion::TLSv1_2 => GoTlsVersion::Tls12, @@ -219,8 +369,9 @@ fn go_tls_version_for_supported(version: &SupportedProtocolVersion) -> GoTlsVers #[cfg(test)] mod tests { - use super::{build_supported_versions, parse_cipher_suites, TlsPolicy}; + use super::{build_supported_versions, common_name_is_allowed, parse_cipher_suites, TlsPolicy}; use rustls::crypto::aws_lc_rs; + use std::collections::HashSet; #[test] fn test_build_supported_versions_defaults_to_tls12_and_tls13() { @@ -262,4 +413,25 @@ mod tests { .unwrap(); assert_eq!(cipher_suites.len(), 2); } + + #[test] + fn test_common_name_is_allowed_matches_exact_and_wildcard() { + let allowed_common_names = + HashSet::from([String::from("volume-a.internal"), String::from("worker-7")]); + assert!(common_name_is_allowed( + "volume-a.internal", + &allowed_common_names, + "", + )); + assert!(common_name_is_allowed( + "node.prod.example.com", + &allowed_common_names, + ".example.com", + )); + assert!(!common_name_is_allowed( + "node.prod.other.net", + &allowed_common_names, + ".example.com", + )); + } } diff --git a/seaweed-volume/src/server/grpc_client.rs b/seaweed-volume/src/server/grpc_client.rs index 0bf668d5d..bdc372b42 100644 --- a/seaweed-volume/src/server/grpc_client.rs +++ b/seaweed-volume/src/server/grpc_client.rs @@ -156,6 +156,8 @@ mod tests { grpc_cert_file: String::new(), grpc_key_file: String::new(), grpc_ca_file: String::new(), + grpc_allowed_wildcard_domain: String::new(), + grpc_volume_allowed_common_names: vec![], tls_policy: TlsPolicy::default(), enable_write_queue: false, security_file: String::new(),