Browse Source

Enforce Go gRPC TLS server policy

rust-volume-server
Chris Lu 4 days ago
parent
commit
590dc17331
  1. 128
      seaweed-volume/Cargo.lock
  2. 2
      seaweed-volume/Cargo.toml
  3. 52
      seaweed-volume/src/config.rs
  4. 154
      seaweed-volume/src/main.rs
  5. 182
      seaweed-volume/src/security/tls.rs
  6. 2
      seaweed-volume/src/server/grpc_client.rs

128
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"

2
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"

52
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<String>,
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<String>,
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();

154
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<tonic::transport::ServerTlsConfig> {
tls_policy: &TlsPolicy,
allowed_wildcard_domain: &str,
allowed_common_names: &[String],
) -> Option<TlsAcceptor> {
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<dyn std::error::Error>> {
@ -305,11 +301,15 @@ async fn run(config: VolumeServerConfig) -> Result<(), Box<dyn std::error::Error
let public_port = config.public_port;
let needs_public = public_port != config.port;
// Build gRPC service
let grpc_service = VolumeGrpcService {
state: state.clone(),
};
let grpc_addr = format!("{}:{}", config.bind_ip, config.grpc_port);
let grpc_tls_acceptor = build_grpc_server_tls_acceptor(
&config.grpc_cert_file,
&config.grpc_key_file,
&config.grpc_ca_file,
&config.tls_policy,
&config.grpc_allowed_wildcard_domain,
&config.grpc_volume_allowed_common_names,
);
info!("Starting HTTP server on {}", admin_addr);
info!("Starting gRPC server on {}", grpc_addr);
@ -441,21 +441,24 @@ async fn run(config: VolumeServerConfig) -> Result<(), Box<dyn std::error::Error
};
let grpc_handle = {
let grpc_cert_file = config.grpc_cert_file.clone();
let grpc_key_file = config.grpc_key_file.clone();
let grpc_ca_file = config.grpc_ca_file.clone();
let grpc_state = state.clone();
let grpc_addr = grpc_addr.clone();
let grpc_tls_acceptor = grpc_tls_acceptor.clone();
let mut shutdown_rx = shutdown_tx.subscribe();
tokio::spawn(async move {
let addr = grpc_addr.parse().expect("Invalid gRPC address");
if let Some(tls_config) =
build_grpc_server_tls_config(&grpc_cert_file, &grpc_key_file, &grpc_ca_file)
{
let grpc_service = VolumeGrpcService {
state: grpc_state.clone(),
};
if let Some(tls_acceptor) = grpc_tls_acceptor {
let listener = tokio::net::TcpListener::bind(&grpc_addr)
.await
.unwrap_or_else(|e| panic!("Failed to bind gRPC to {}: {}", grpc_addr, e));
let incoming = grpc_tls_incoming(listener, tls_acceptor);
info!("gRPC server listening on {} (TLS enabled)", addr);
if let Err(e) = tonic::transport::Server::builder()
.tls_config(tls_config)
.expect("Failed to configure gRPC TLS")
.add_service(VolumeServerServer::new(grpc_service))
.serve_with_shutdown(addr, async move {
.serve_with_incoming_shutdown(incoming, async move {
let _ = shutdown_rx.recv().await;
})
.await
@ -656,6 +659,30 @@ async fn run_metrics_push_loop(
}
}
fn grpc_tls_incoming(
listener: tokio::net::TcpListener,
tls_acceptor: TlsAcceptor,
) -> impl tokio_stream::Stream<
Item = Result<tokio_rustls::server::TlsStream<tokio::net::TcpStream>, 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<F>(
@ -713,7 +740,8 @@ async fn serve_https<F>(
#[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());
}

182
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<String>,
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<dyn ClientCertVerifier>,
allowed_common_names: HashSet<String>,
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<ClientCertVerified, rustls::Error> {
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<HandshakeSignatureValid, rustls::Error> {
self.inner.verify_tls12_signature(message, cert, dss)
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
self.inner.verify_tls13_signature(message, cert, dss)
}
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
self.inner.supported_verify_schemes()
}
}
pub fn build_rustls_server_config(
cert_path: &str,
key_path: &str,
ca_path: &str,
policy: &TlsPolicy,
) -> Result<ServerConfig, TlsPolicyError> {
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<ServerConfig, TlsPolicyError> {
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<ServerConfig, TlsPolicyError> {
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<dyn ClientCertVerifier> = 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<CipherSuite, TlsPolicyError> {
}
}
fn parse_common_name(cert: &CertificateDer<'_>) -> Result<String, TlsPolicyError> {
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<String>,
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",
));
}
}

2
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(),

Loading…
Cancel
Save