Browse Source

Apply Go tls policy to HTTPS server

rust-volume-server
Chris Lu 1 week ago
parent
commit
1a33c0b1fb
  1. 60
      seaweed-volume/src/config.rs
  2. 50
      seaweed-volume/src/main.rs
  3. 2
      seaweed-volume/src/security.rs
  4. 265
      seaweed-volume/src/security/tls.rs
  5. 2
      seaweed-volume/src/server/grpc_client.rs

60
seaweed-volume/src/config.rs

@ -2,6 +2,8 @@ use clap::Parser;
use std::net::UdpSocket;
use std::path::{Path, PathBuf};
use crate::security::tls::TlsPolicy;
/// SeaweedFS Volume Server (Rust implementation)
///
/// Start a volume server to provide storage spaces.
@ -245,6 +247,7 @@ pub struct VolumeServerConfig {
pub grpc_cert_file: String,
pub grpc_key_file: String,
pub grpc_ca_file: String,
pub tls_policy: TlsPolicy,
/// Enable batched write queue for improved throughput under load.
pub enable_write_queue: bool,
/// Path to security.toml — stored for SIGHUP reload.
@ -784,6 +787,7 @@ 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,
tls_policy: sec.tls_policy,
enable_write_queue: std::env::var("SEAWEED_WRITE_QUEUE")
.map(|v| v == "1" || v == "true")
.unwrap_or(false),
@ -812,6 +816,7 @@ pub struct SecurityConfig {
pub grpc_cert_file: String,
pub grpc_key_file: String,
pub grpc_ca_file: String,
pub tls_policy: TlsPolicy,
pub access_ui: bool,
/// IPs from [guard] white_list in security.toml
pub guard_white_list: Vec<String>,
@ -833,6 +838,12 @@ const SECURITY_CONFIG_FILE_NAME: &str = "security.toml";
/// [https.volume]
/// cert = "/path/to/cert.pem"
/// key = "/path/to/key.pem"
/// ca = "/path/to/ca.pem"
///
/// [tls]
/// min_version = "TLS 1.2"
/// max_version = "TLS 1.3"
/// cipher_suites = "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
///
/// [https.client]
/// enabled = true
@ -874,6 +885,7 @@ pub fn parse_security_config(path: &str) -> SecurityConfig {
Grpc,
HttpsVolume,
GrpcVolume,
Tls,
Guard,
Access,
}
@ -909,6 +921,10 @@ pub fn parse_security_config(path: &str) -> SecurityConfig {
section = Section::GrpcVolume;
continue;
}
if trimmed == "[tls]" {
section = Section::Tls;
continue;
}
if trimmed == "[guard]" {
section = Section::Guard;
continue;
@ -961,6 +977,12 @@ pub fn parse_security_config(path: &str) -> SecurityConfig {
"ca" => cfg.grpc_ca_file = value.to_string(),
_ => {}
},
Section::Tls => match key {
"min_version" => cfg.tls_policy.min_version = value.to_string(),
"max_version" => cfg.tls_policy.max_version = value.to_string(),
"cipher_suites" => cfg.tls_policy.cipher_suites = value.to_string(),
_ => {}
},
Section::Guard => match key {
"white_list" => {
cfg.guard_white_list = value
@ -1075,6 +1097,15 @@ 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_TLS_MIN_VERSION") {
cfg.tls_policy.min_version = v;
}
if let Ok(v) = std::env::var("WEED_TLS_MAX_VERSION") {
cfg.tls_policy.max_version = v;
}
if let Ok(v) = std::env::var("WEED_TLS_CIPHER_SUITES") {
cfg.tls_policy.cipher_suites = v;
}
if let Ok(v) = std::env::var("WEED_GUARD_WHITE_LIST") {
cfg.guard_white_list = v
.split(',')
@ -1154,6 +1185,9 @@ mod tests {
"WEED_GRPC_VOLUME_KEY",
"WEED_GRPC_CA",
"WEED_GRPC_VOLUME_CA",
"WEED_TLS_MIN_VERSION",
"WEED_TLS_MAX_VERSION",
"WEED_TLS_CIPHER_SUITES",
"WEED_GUARD_WHITE_LIST",
"WEED_ACCESS_UI",
];
@ -1424,6 +1458,32 @@ ca = "/etc/seaweedfs/client-ca.pem"
});
}
#[test]
fn test_parse_security_config_uses_tls_policy_settings() {
let _guard = process_state_lock();
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::write(
tmp.path(),
r#"
[tls]
min_version = "TLS 1.2"
max_version = "TLS 1.3"
cipher_suites = "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
"#,
)
.unwrap();
with_cleared_security_env(|| {
let cfg = parse_security_config(tmp.path().to_str().unwrap());
assert_eq!(cfg.tls_policy.min_version, "TLS 1.2");
assert_eq!(cfg.tls_policy.max_version, "TLS 1.3");
assert_eq!(
cfg.tls_policy.cipher_suites,
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
);
});
}
#[test]
fn test_merge_options_file_basic() {
let tmp = tempfile::NamedTempFile::new().unwrap();

50
seaweed-volume/src/main.rs

@ -5,6 +5,7 @@ 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::{Guard, SigningKey};
use seaweed_volume::server::debug::build_debug_router;
use seaweed_volume::server::grpc_client::load_outgoing_grpc_tls;
@ -48,50 +49,6 @@ fn main() {
}
}
/// 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)
.unwrap_or_else(|e| panic!("Failed to read TLS key file '{}': {}", key_path, e));
let certs = rustls_pemfile::certs(&mut &cert_pem[..])
.collect::<Result<Vec<_>, _>>()
.expect("Failed to parse TLS certificate PEM");
let key = rustls_pemfile::private_key(&mut &key_pem[..])
.expect("Failed to parse TLS private key PEM")
.expect("No private key found in PEM file");
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::<Result<Vec<_>, _>>()
.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")
}
}
fn build_outgoing_http_client(
config: &VolumeServerConfig,
) -> Result<(reqwest::Client, String), Box<dyn std::error::Error>> {
@ -439,11 +396,12 @@ async fn run(config: VolumeServerConfig) -> Result<(), Box<dyn std::error::Error
"TLS enabled for HTTP server (cert={}, key={})",
config.https_cert_file, config.https_key_file
);
let tls_config = load_rustls_config(
let tls_config = build_rustls_server_config(
&config.https_cert_file,
&config.https_key_file,
&config.https_ca_file,
);
&config.tls_policy,
)?;
Some(TlsAcceptor::from(Arc::new(tls_config)))
} else {
None

2
seaweed-volume/src/security.rs

@ -4,6 +4,8 @@
//! - Guard: combines whitelist IP checking with JWT token validation
//! - JWT: HS256 HMAC signing with file-id claims
pub mod tls;
use std::collections::HashSet;
use std::net::IpAddr;
use std::time::{SystemTime, UNIX_EPOCH};

265
seaweed-volume/src/security/tls.rs

@ -0,0 +1,265 @@
use std::fmt;
use std::sync::Arc;
use rustls::crypto::aws_lc_rs;
use rustls::crypto::CryptoProvider;
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
use rustls::server::WebPkiClientVerifier;
use rustls::{
CipherSuite, RootCertStore, ServerConfig, SupportedCipherSuite, SupportedProtocolVersion,
};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct TlsPolicy {
pub min_version: String,
pub max_version: String,
pub cipher_suites: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TlsPolicyError(String);
impl fmt::Display for TlsPolicyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for TlsPolicyError {}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
enum GoTlsVersion {
Ssl3,
Tls10,
Tls11,
Tls12,
Tls13,
}
pub fn build_rustls_server_config(
cert_path: &str,
key_path: &str,
ca_path: &str,
policy: &TlsPolicy,
) -> Result<ServerConfig, TlsPolicyError> {
let cert_chain = read_cert_chain(cert_path)?;
let private_key = read_private_key(key_path)?;
let provider = build_crypto_provider(policy)?;
let versions = build_supported_versions(policy)?;
let builder = ServerConfig::builder_with_provider(provider.clone())
.with_protocol_versions(&versions)
.map_err(|e| TlsPolicyError(format!("invalid TLS version policy: {}", e)))?;
let builder = if ca_path.is_empty() {
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)))?;
builder.with_client_cert_verifier(verifier)
};
builder
.with_single_cert(cert_chain, private_key)
.map_err(|e| TlsPolicyError(format!("build rustls server config failed: {}", e)))
}
fn read_cert_chain(cert_path: &str) -> Result<Vec<CertificateDer<'static>>, TlsPolicyError> {
let cert_pem = std::fs::read(cert_path).map_err(|e| {
TlsPolicyError(format!(
"Failed to read TLS cert file '{}': {}",
cert_path, e
))
})?;
rustls_pemfile::certs(&mut &cert_pem[..])
.collect::<Result<Vec<_>, _>>()
.map_err(|e| {
TlsPolicyError(format!(
"Failed to parse TLS cert PEM '{}': {}",
cert_path, e
))
})
}
fn read_private_key(key_path: &str) -> Result<PrivateKeyDer<'static>, TlsPolicyError> {
let key_pem = std::fs::read(key_path).map_err(|e| {
TlsPolicyError(format!("Failed to read TLS key file '{}': {}", key_path, e))
})?;
rustls_pemfile::private_key(&mut &key_pem[..])
.map_err(|e| TlsPolicyError(format!("Failed to parse TLS key PEM '{}': {}", key_path, e)))?
.ok_or_else(|| TlsPolicyError(format!("No private key found in '{}'", key_path)))
}
fn read_root_store(ca_path: &str) -> Result<RootCertStore, TlsPolicyError> {
let ca_pem = std::fs::read(ca_path)
.map_err(|e| TlsPolicyError(format!("Failed to read TLS CA file '{}': {}", ca_path, e)))?;
let ca_certs = rustls_pemfile::certs(&mut &ca_pem[..])
.collect::<Result<Vec<_>, _>>()
.map_err(|e| TlsPolicyError(format!("Failed to parse TLS CA PEM '{}': {}", ca_path, e)))?;
let mut roots = RootCertStore::empty();
for cert in ca_certs {
roots
.add(cert)
.map_err(|e| TlsPolicyError(format!("Failed to add CA cert '{}': {}", ca_path, e)))?;
}
Ok(roots)
}
fn build_crypto_provider(policy: &TlsPolicy) -> Result<Arc<CryptoProvider>, TlsPolicyError> {
let mut provider = aws_lc_rs::default_provider();
let cipher_suites = parse_cipher_suites(&provider.cipher_suites, &policy.cipher_suites)?;
if !cipher_suites.is_empty() {
provider.cipher_suites = cipher_suites;
}
Ok(Arc::new(provider))
}
pub fn build_supported_versions(
policy: &TlsPolicy,
) -> Result<Vec<&'static SupportedProtocolVersion>, TlsPolicyError> {
let min_version = parse_go_tls_version(&policy.min_version)?;
let max_version = parse_go_tls_version(&policy.max_version)?;
let versions = [&rustls::version::TLS13, &rustls::version::TLS12]
.into_iter()
.filter(|version| {
let current = go_tls_version_for_supported(version);
min_version.map(|min| current >= min).unwrap_or(true)
&& max_version.map(|max| current <= max).unwrap_or(true)
})
.collect::<Vec<_>>();
if versions.is_empty() {
return Err(TlsPolicyError(format!(
"TLS version range min='{}' max='{}' is unsupported by rustls",
policy.min_version, policy.max_version
)));
}
Ok(versions)
}
fn parse_go_tls_version(value: &str) -> Result<Option<GoTlsVersion>, TlsPolicyError> {
match value.trim() {
"" => Ok(None),
"SSLv3" => Ok(Some(GoTlsVersion::Ssl3)),
"TLS 1.0" => Ok(Some(GoTlsVersion::Tls10)),
"TLS 1.1" => Ok(Some(GoTlsVersion::Tls11)),
"TLS 1.2" => Ok(Some(GoTlsVersion::Tls12)),
"TLS 1.3" => Ok(Some(GoTlsVersion::Tls13)),
other => Err(TlsPolicyError(format!("invalid TLS version {}", other))),
}
}
fn parse_cipher_suites(
available: &[SupportedCipherSuite],
value: &str,
) -> Result<Vec<SupportedCipherSuite>, TlsPolicyError> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Ok(Vec::new());
}
trimmed
.split(',')
.map(|name| {
let suite = parse_cipher_suite_name(name.trim())?;
available
.iter()
.copied()
.find(|candidate| candidate.suite() == suite)
.ok_or_else(|| {
TlsPolicyError(format!(
"TLS cipher suite '{}' is unsupported by the Rust implementation",
name.trim()
))
})
})
.collect()
}
fn parse_cipher_suite_name(value: &str) -> Result<CipherSuite, TlsPolicyError> {
match value {
"TLS_AES_128_GCM_SHA256" => Ok(CipherSuite::TLS13_AES_128_GCM_SHA256),
"TLS_AES_256_GCM_SHA384" => Ok(CipherSuite::TLS13_AES_256_GCM_SHA384),
"TLS_CHACHA20_POLY1305_SHA256" => Ok(CipherSuite::TLS13_CHACHA20_POLY1305_SHA256),
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" => {
Ok(CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256)
}
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" => {
Ok(CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384)
}
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256" => {
Ok(CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256)
}
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" => {
Ok(CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256)
}
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" => {
Ok(CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384)
}
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256" => {
Ok(CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256)
}
other => Err(TlsPolicyError(format!(
"TLS cipher suite '{}' is unsupported by the Rust implementation",
other
))),
}
}
fn go_tls_version_for_supported(version: &SupportedProtocolVersion) -> GoTlsVersion {
match version.version {
rustls::ProtocolVersion::TLSv1_2 => GoTlsVersion::Tls12,
rustls::ProtocolVersion::TLSv1_3 => GoTlsVersion::Tls13,
_ => unreachable!("rustls only exposes TLS 1.2 and 1.3"),
}
}
#[cfg(test)]
mod tests {
use super::{build_supported_versions, parse_cipher_suites, TlsPolicy};
use rustls::crypto::aws_lc_rs;
#[test]
fn test_build_supported_versions_defaults_to_tls12_and_tls13() {
let versions = build_supported_versions(&TlsPolicy::default()).unwrap();
assert_eq!(
versions,
vec![&rustls::version::TLS13, &rustls::version::TLS12]
);
}
#[test]
fn test_build_supported_versions_filters_to_tls13() {
let versions = build_supported_versions(&TlsPolicy {
min_version: "TLS 1.3".to_string(),
max_version: "TLS 1.3".to_string(),
cipher_suites: String::new(),
})
.unwrap();
assert_eq!(versions, vec![&rustls::version::TLS13]);
}
#[test]
fn test_build_supported_versions_rejects_unsupported_legacy_range() {
let err = build_supported_versions(&TlsPolicy {
min_version: "TLS 1.0".to_string(),
max_version: "TLS 1.1".to_string(),
cipher_suites: String::new(),
})
.unwrap_err();
assert!(err.to_string().contains("unsupported by rustls"));
}
#[test]
fn test_parse_cipher_suites_accepts_go_names() {
let cipher_suites = parse_cipher_suites(
&aws_lc_rs::default_provider().cipher_suites,
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_AES_128_GCM_SHA256",
)
.unwrap();
assert_eq!(cipher_suites.len(), 2);
}
}

2
seaweed-volume/src/server/grpc_client.rs

@ -99,6 +99,7 @@ pub fn build_grpc_endpoint(
mod tests {
use super::{build_grpc_endpoint, grpc_endpoint_uri, load_outgoing_grpc_tls};
use crate::config::{NeedleMapKind, ReadMode, VolumeServerConfig};
use crate::security::tls::TlsPolicy;
fn sample_config() -> VolumeServerConfig {
VolumeServerConfig {
@ -155,6 +156,7 @@ mod tests {
grpc_cert_file: String::new(),
grpc_key_file: String::new(),
grpc_ca_file: String::new(),
tls_policy: TlsPolicy::default(),
enable_write_queue: false,
security_file: String::new(),
}

Loading…
Cancel
Save