feat(targets): enhance webhook TLS support with custom CA and skip-verify (#1994)

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>
Co-authored-by: heihutu <heihutu@gmail.com>
This commit is contained in:
houseme
2026-02-27 21:24:49 +08:00
committed by GitHub
parent bdb2a9e9b7
commit d17d2083d4
19 changed files with 182 additions and 83 deletions

View File

@@ -1,8 +1,9 @@
## —— Tests and e2e test --------------------------------------------------------------------------- ## —— Tests and e2e test ---------------------------------------------------------------------------
TEST_THREADS ?= 1
.PHONY: test .PHONY: test
test: core-deps test-deps ## Run all tests test: core-deps test-deps ## Run all tests
TEST_THREADS ?= 1
@echo "🧪 Running tests..." @echo "🧪 Running tests..."
@if command -v cargo-nextest >/dev/null 2>&1; then \ @if command -v cargo-nextest >/dev/null 2>&1; then \
cargo nextest run --all --exclude e2e_test; \ cargo nextest run --all --exclude e2e_test; \

3
Cargo.lock generated
View File

@@ -3106,7 +3106,6 @@ dependencies = [
"rustfs-madmin", "rustfs-madmin",
"rustfs-protos", "rustfs-protos",
"rustls", "rustls",
"rustls-pemfile",
"serde", "serde",
"serde_json", "serde_json",
"serial_test", "serial_test",
@@ -7202,7 +7201,6 @@ dependencies = [
"rustfs-utils", "rustfs-utils",
"rustfs-zip", "rustfs-zip",
"rustls", "rustls",
"rustls-pemfile",
"s3s", "s3s",
"serde", "serde",
"serde_json", "serde_json",
@@ -7902,7 +7900,6 @@ dependencies = [
"rustfs-config", "rustfs-config",
"rustix 1.1.4", "rustix 1.1.4",
"rustls", "rustls",
"rustls-pemfile",
"rustls-pki-types", "rustls-pki-types",
"s3s", "s3s",
"serde", "serde",

View File

@@ -160,7 +160,6 @@ openidconnect = { version = "4.0", default-features = false }
pbkdf2 = "0.13.0-rc.9" pbkdf2 = "0.13.0-rc.9"
rsa = { version = "0.10.0-rc.15" } rsa = { version = "0.10.0-rc.15" }
rustls = { version = "0.23.37", default-features = false, features = ["aws-lc-rs", "logging", "tls12", "prefer-post-quantum", "std"] } rustls = { version = "0.23.37", default-features = false, features = ["aws-lc-rs", "logging", "tls12", "prefer-post-quantum", "std"] }
rustls-pemfile = "2.2.0"
rustls-pki-types = "1.14.0" rustls-pki-types = "1.14.0"
sha1 = "0.11.0-rc.5" sha1 = "0.11.0-rc.5"
sha2 = "0.11.0-rc.5" sha2 = "0.11.0-rc.5"

View File

@@ -19,8 +19,9 @@ use rumqttc::QoS;
use rustfs_config::audit::{AUDIT_MQTT_KEYS, AUDIT_WEBHOOK_KEYS, ENV_AUDIT_MQTT_KEYS, ENV_AUDIT_WEBHOOK_KEYS}; use rustfs_config::audit::{AUDIT_MQTT_KEYS, AUDIT_WEBHOOK_KEYS, ENV_AUDIT_MQTT_KEYS, ENV_AUDIT_WEBHOOK_KEYS};
use rustfs_config::{ use rustfs_config::{
AUDIT_DEFAULT_DIR, DEFAULT_LIMIT, MQTT_BROKER, MQTT_KEEP_ALIVE_INTERVAL, MQTT_PASSWORD, MQTT_QOS, MQTT_QUEUE_DIR, AUDIT_DEFAULT_DIR, DEFAULT_LIMIT, MQTT_BROKER, MQTT_KEEP_ALIVE_INTERVAL, MQTT_PASSWORD, MQTT_QOS, MQTT_QUEUE_DIR,
MQTT_QUEUE_LIMIT, MQTT_RECONNECT_INTERVAL, MQTT_TOPIC, MQTT_USERNAME, WEBHOOK_AUTH_TOKEN, WEBHOOK_CLIENT_CERT, MQTT_QUEUE_LIMIT, MQTT_RECONNECT_INTERVAL, MQTT_TOPIC, MQTT_USERNAME, RUSTFS_WEBHOOK_SKIP_TLS_VERIFY_DEFAULT,
WEBHOOK_CLIENT_KEY, WEBHOOK_ENDPOINT, WEBHOOK_QUEUE_DIR, WEBHOOK_QUEUE_LIMIT, WEBHOOK_AUTH_TOKEN, WEBHOOK_CLIENT_CA, WEBHOOK_CLIENT_CERT, WEBHOOK_CLIENT_KEY, WEBHOOK_ENDPOINT, WEBHOOK_QUEUE_DIR,
WEBHOOK_QUEUE_LIMIT, WEBHOOK_SKIP_TLS_VERIFY,
}; };
use rustfs_ecstore::config::KVS; use rustfs_ecstore::config::KVS;
use rustfs_targets::{ use rustfs_targets::{
@@ -75,6 +76,11 @@ impl TargetFactory for WebhookTargetFactory {
.unwrap_or(DEFAULT_LIMIT), .unwrap_or(DEFAULT_LIMIT),
client_cert: config.lookup(WEBHOOK_CLIENT_CERT).unwrap_or_default(), client_cert: config.lookup(WEBHOOK_CLIENT_CERT).unwrap_or_default(),
client_key: config.lookup(WEBHOOK_CLIENT_KEY).unwrap_or_default(), client_key: config.lookup(WEBHOOK_CLIENT_KEY).unwrap_or_default(),
client_ca: config.lookup(WEBHOOK_CLIENT_CA).unwrap_or_default(),
skip_tls_verify: config
.lookup(WEBHOOK_SKIP_TLS_VERIFY)
.and_then(|v| v.parse::<bool>().ok())
.unwrap_or(RUSTFS_WEBHOOK_SKIP_TLS_VERIFY_DEFAULT),
target_type: rustfs_targets::target::TargetType::AuditLog, target_type: rustfs_targets::target::TargetType::AuditLog,
}; };

View File

@@ -20,9 +20,11 @@ pub const ENV_AUDIT_WEBHOOK_QUEUE_LIMIT: &str = "RUSTFS_AUDIT_WEBHOOK_QUEUE_LIMI
pub const ENV_AUDIT_WEBHOOK_QUEUE_DIR: &str = "RUSTFS_AUDIT_WEBHOOK_QUEUE_DIR"; pub const ENV_AUDIT_WEBHOOK_QUEUE_DIR: &str = "RUSTFS_AUDIT_WEBHOOK_QUEUE_DIR";
pub const ENV_AUDIT_WEBHOOK_CLIENT_CERT: &str = "RUSTFS_AUDIT_WEBHOOK_CLIENT_CERT"; pub const ENV_AUDIT_WEBHOOK_CLIENT_CERT: &str = "RUSTFS_AUDIT_WEBHOOK_CLIENT_CERT";
pub const ENV_AUDIT_WEBHOOK_CLIENT_KEY: &str = "RUSTFS_AUDIT_WEBHOOK_CLIENT_KEY"; pub const ENV_AUDIT_WEBHOOK_CLIENT_KEY: &str = "RUSTFS_AUDIT_WEBHOOK_CLIENT_KEY";
pub const ENV_AUDIT_WEBHOOK_CLIENT_CA: &str = "RUSTFS_AUDIT_WEBHOOK_CLIENT_CA";
pub const ENV_AUDIT_WEBHOOK_SKIP_TLS_VERIFY: &str = "RUSTFS_AUDIT_WEBHOOK_SKIP_TLS_VERIFY";
/// List of all environment variable keys for a webhook target. /// List of all environment variable keys for a webhook target.
pub const ENV_AUDIT_WEBHOOK_KEYS: &[&str; 7] = &[ pub const ENV_AUDIT_WEBHOOK_KEYS: &[&str; 9] = &[
ENV_AUDIT_WEBHOOK_ENABLE, ENV_AUDIT_WEBHOOK_ENABLE,
ENV_AUDIT_WEBHOOK_ENDPOINT, ENV_AUDIT_WEBHOOK_ENDPOINT,
ENV_AUDIT_WEBHOOK_AUTH_TOKEN, ENV_AUDIT_WEBHOOK_AUTH_TOKEN,
@@ -30,6 +32,8 @@ pub const ENV_AUDIT_WEBHOOK_KEYS: &[&str; 7] = &[
ENV_AUDIT_WEBHOOK_QUEUE_DIR, ENV_AUDIT_WEBHOOK_QUEUE_DIR,
ENV_AUDIT_WEBHOOK_CLIENT_CERT, ENV_AUDIT_WEBHOOK_CLIENT_CERT,
ENV_AUDIT_WEBHOOK_CLIENT_KEY, ENV_AUDIT_WEBHOOK_CLIENT_KEY,
ENV_AUDIT_WEBHOOK_CLIENT_CA,
ENV_AUDIT_WEBHOOK_SKIP_TLS_VERIFY,
]; ];
/// A list of all valid configuration keys for a webhook target. /// A list of all valid configuration keys for a webhook target.
@@ -42,4 +46,6 @@ pub const AUDIT_WEBHOOK_KEYS: &[&str] = &[
crate::WEBHOOK_CLIENT_CERT, crate::WEBHOOK_CLIENT_CERT,
crate::WEBHOOK_CLIENT_KEY, crate::WEBHOOK_CLIENT_KEY,
crate::COMMENT_KEY, crate::COMMENT_KEY,
crate::WEBHOOK_CLIENT_CA,
crate::WEBHOOK_SKIP_TLS_VERIFY,
]; ];

View File

@@ -20,6 +20,8 @@ pub const EVENT_DEFAULT_DIR: &str = "/opt/rustfs/events"; // Default directory f
pub const AUDIT_DEFAULT_DIR: &str = "/opt/rustfs/audit"; // Default directory for audit store pub const AUDIT_DEFAULT_DIR: &str = "/opt/rustfs/audit"; // Default directory for audit store
pub const DEFAULT_LIMIT: u64 = 100000; // Default store limit pub const DEFAULT_LIMIT: u64 = 100000; // Default store limit
pub const RUSTFS_WEBHOOK_SKIP_TLS_VERIFY_DEFAULT: bool = false;
/// Standard config keys and values. /// Standard config keys and values.
pub const ENABLE_KEY: &str = "enable"; pub const ENABLE_KEY: &str = "enable";
pub const COMMENT_KEY: &str = "comment"; pub const COMMENT_KEY: &str = "comment";

View File

@@ -16,6 +16,8 @@ pub const WEBHOOK_ENDPOINT: &str = "endpoint";
pub const WEBHOOK_AUTH_TOKEN: &str = "auth_token"; pub const WEBHOOK_AUTH_TOKEN: &str = "auth_token";
pub const WEBHOOK_CLIENT_CERT: &str = "client_cert"; pub const WEBHOOK_CLIENT_CERT: &str = "client_cert";
pub const WEBHOOK_CLIENT_KEY: &str = "client_key"; pub const WEBHOOK_CLIENT_KEY: &str = "client_key";
pub const WEBHOOK_CLIENT_CA: &str = "client_ca";
pub const WEBHOOK_SKIP_TLS_VERIFY: &str = "skip_tls_verify";
pub const WEBHOOK_BATCH_SIZE: &str = "batch_size"; pub const WEBHOOK_BATCH_SIZE: &str = "batch_size";
pub const WEBHOOK_QUEUE_LIMIT: &str = "queue_limit"; pub const WEBHOOK_QUEUE_LIMIT: &str = "queue_limit";
pub const WEBHOOK_QUEUE_DIR: &str = "queue_dir"; pub const WEBHOOK_QUEUE_DIR: &str = "queue_dir";

View File

@@ -22,6 +22,8 @@ pub const NOTIFY_WEBHOOK_KEYS: &[&str] = &[
crate::WEBHOOK_CLIENT_CERT, crate::WEBHOOK_CLIENT_CERT,
crate::WEBHOOK_CLIENT_KEY, crate::WEBHOOK_CLIENT_KEY,
crate::COMMENT_KEY, crate::COMMENT_KEY,
crate::WEBHOOK_CLIENT_CA,
crate::WEBHOOK_SKIP_TLS_VERIFY,
]; ];
// Webhook Environment Variables // Webhook Environment Variables
@@ -32,8 +34,10 @@ pub const ENV_NOTIFY_WEBHOOK_QUEUE_LIMIT: &str = "RUSTFS_NOTIFY_WEBHOOK_QUEUE_LI
pub const ENV_NOTIFY_WEBHOOK_QUEUE_DIR: &str = "RUSTFS_NOTIFY_WEBHOOK_QUEUE_DIR"; pub const ENV_NOTIFY_WEBHOOK_QUEUE_DIR: &str = "RUSTFS_NOTIFY_WEBHOOK_QUEUE_DIR";
pub const ENV_NOTIFY_WEBHOOK_CLIENT_CERT: &str = "RUSTFS_NOTIFY_WEBHOOK_CLIENT_CERT"; pub const ENV_NOTIFY_WEBHOOK_CLIENT_CERT: &str = "RUSTFS_NOTIFY_WEBHOOK_CLIENT_CERT";
pub const ENV_NOTIFY_WEBHOOK_CLIENT_KEY: &str = "RUSTFS_NOTIFY_WEBHOOK_CLIENT_KEY"; pub const ENV_NOTIFY_WEBHOOK_CLIENT_KEY: &str = "RUSTFS_NOTIFY_WEBHOOK_CLIENT_KEY";
pub const ENV_NOTIFY_WEBHOOK_CLIENT_CA: &str = "RUSTFS_NOTIFY_WEBHOOK_CLIENT_CA";
pub const ENV_NOTIFY_WEBHOOK_SKIP_TLS_VERIFY: &str = "RUSTFS_NOTIFY_WEBHOOK_SKIP_TLS_VERIFY";
pub const ENV_NOTIFY_WEBHOOK_KEYS: &[&str; 7] = &[ pub const ENV_NOTIFY_WEBHOOK_KEYS: &[&str; 9] = &[
ENV_NOTIFY_WEBHOOK_ENABLE, ENV_NOTIFY_WEBHOOK_ENABLE,
ENV_NOTIFY_WEBHOOK_ENDPOINT, ENV_NOTIFY_WEBHOOK_ENDPOINT,
ENV_NOTIFY_WEBHOOK_AUTH_TOKEN, ENV_NOTIFY_WEBHOOK_AUTH_TOKEN,
@@ -41,4 +45,6 @@ pub const ENV_NOTIFY_WEBHOOK_KEYS: &[&str; 7] = &[
ENV_NOTIFY_WEBHOOK_QUEUE_DIR, ENV_NOTIFY_WEBHOOK_QUEUE_DIR,
ENV_NOTIFY_WEBHOOK_CLIENT_CERT, ENV_NOTIFY_WEBHOOK_CLIENT_CERT,
ENV_NOTIFY_WEBHOOK_CLIENT_KEY, ENV_NOTIFY_WEBHOOK_CLIENT_KEY,
ENV_NOTIFY_WEBHOOK_CLIENT_CA,
ENV_NOTIFY_WEBHOOK_SKIP_TLS_VERIFY,
]; ];

View File

@@ -59,5 +59,4 @@ sha2 = { workspace = true }
suppaftp = { workspace = true, features = ["tokio", "rustls-aws-lc-rs"] } suppaftp = { workspace = true, features = ["tokio", "rustls-aws-lc-rs"] }
rcgen.workspace = true rcgen.workspace = true
anyhow.workspace = true anyhow.workspace = true
rustls.workspace = true rustls.workspace = true
rustls-pemfile.workspace = true

View File

@@ -18,8 +18,7 @@ use crate::common::rustfs_binary_path;
use crate::protocols::test_env::{DEFAULT_ACCESS_KEY, DEFAULT_SECRET_KEY, ProtocolTestEnvironment}; use crate::protocols::test_env::{DEFAULT_ACCESS_KEY, DEFAULT_SECRET_KEY, ProtocolTestEnvironment};
use anyhow::Result; use anyhow::Result;
use rcgen::generate_simple_self_signed; use rcgen::generate_simple_self_signed;
use rustls::crypto::aws_lc_rs::default_provider; use rustls::{ClientConfig, RootCertStore, pki_types::CertificateDer, pki_types::pem::PemObject};
use rustls::{ClientConfig, RootCertStore};
use std::io::Cursor; use std::io::Cursor;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
@@ -74,8 +73,8 @@ pub async fn test_ftps_core_operations() -> Result<()> {
.await .await
.map_err(|e| anyhow::anyhow!("{}", e))?; .map_err(|e| anyhow::anyhow!("{}", e))?;
// Install the aws-lc-rs crypto provider // Build ServerConfig with SNI support
default_provider() rustls::crypto::aws_lc_rs::default_provider()
.install_default() .install_default()
.map_err(|e| anyhow::anyhow!("Failed to install crypto provider: {:?}", e))?; .map_err(|e| anyhow::anyhow!("Failed to install crypto provider: {:?}", e))?;
@@ -84,7 +83,7 @@ pub async fn test_ftps_core_operations() -> Result<()> {
// Add the self-signed certificate to the trust store for e2e // Add the self-signed certificate to the trust store for e2e
// Note: In a real environment, you'd use proper root certificates // Note: In a real environment, you'd use proper root certificates
let cert_pem = default_cert.cert.pem(); let cert_pem = default_cert.cert.pem();
let cert_der = rustls_pemfile::certs(&mut Cursor::new(cert_pem)) let cert_der = CertificateDer::pem_reader_iter(&mut Cursor::new(cert_pem))
.collect::<Result<Vec<_>, _>>() .collect::<Result<Vec<_>, _>>()
.map_err(|e| anyhow::anyhow!("Failed to parse cert: {}", e))?; .map_err(|e| anyhow::anyhow!("Failed to parse cert: {}", e))?;

View File

@@ -165,7 +165,6 @@ impl TransitionClient {
async fn private_new(endpoint: &str, opts: Options, tier_type: &str) -> Result<TransitionClient, std::io::Error> { async fn private_new(endpoint: &str, opts: Options, tier_type: &str) -> Result<TransitionClient, std::io::Error> {
let endpoint_url = get_endpoint_url(endpoint, opts.secure)?; let endpoint_url = get_endpoint_url(endpoint, opts.secure)?;
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
let scheme = endpoint_url.scheme(); let scheme = endpoint_url.scheme();
let client; let client;
let tls = if let Some(store) = load_root_store_from_tls_path() { let tls = if let Some(store) = load_root_store_from_tls_path() {
@@ -292,7 +291,7 @@ impl TransitionClient {
let _ = hc_duration; let _ = hc_duration;
} }
fn dump_http(&self, req: &http::Request<s3s::Body>, resp: &http::Response<Incoming>) -> Result<(), std::io::Error> { fn dump_http(&self, req: &Request<s3s::Body>, resp: &Response<Incoming>) -> Result<(), std::io::Error> {
let mut resp_trace: Vec<u8>; let mut resp_trace: Vec<u8>;
//info!("{}{}", self.trace_output, "---------BEGIN-HTTP---------"); //info!("{}{}", self.trace_output, "---------BEGIN-HTTP---------");
@@ -301,7 +300,7 @@ impl TransitionClient {
Ok(()) Ok(())
} }
pub async fn doit(&self, req: http::Request<s3s::Body>) -> Result<http::Response<Incoming>, std::io::Error> { pub async fn doit(&self, req: Request<s3s::Body>) -> Result<Response<Incoming>, std::io::Error> {
let req_method; let req_method;
let req_uri; let req_uri;
let req_headers; let req_headers;
@@ -353,7 +352,7 @@ impl TransitionClient {
&self, &self,
method: http::Method, method: http::Method,
metadata: &mut RequestMetadata, metadata: &mut RequestMetadata,
) -> Result<http::Response<Incoming>, std::io::Error> { ) -> Result<Response<Incoming>, std::io::Error> {
if self.is_offline() { if self.is_offline() {
let mut s = self.endpoint_url.to_string(); let mut s = self.endpoint_url.to_string();
s.push_str(" is offline."); s.push_str(" is offline.");
@@ -363,7 +362,7 @@ impl TransitionClient {
let retryable: bool; let retryable: bool;
//let mut body_seeker: BufferReader; //let mut body_seeker: BufferReader;
let mut req_retry = self.max_retries; let mut req_retry = self.max_retries;
let mut resp: http::Response<Incoming>; let mut resp: Response<Incoming>;
//if metadata.content_body != nil { //if metadata.content_body != nil {
//body_seeker = BufferReader::new(metadata.content_body.read_all().await?); //body_seeker = BufferReader::new(metadata.content_body.read_all().await?);
@@ -401,10 +400,10 @@ impl TransitionClient {
err_response.message = format!("remote tier error: {}", err_response.message); err_response.message = format!("remote tier error: {}", err_response.message);
if self.region == "" { if self.region == "" {
match err_response.code { return match err_response.code {
S3ErrorCode::AuthorizationHeaderMalformed | S3ErrorCode::InvalidArgument /*S3ErrorCode::InvalidRegion*/ => { S3ErrorCode::AuthorizationHeaderMalformed | S3ErrorCode::InvalidArgument /*S3ErrorCode::InvalidRegion*/ => {
//break; //break;
return Err(std::io::Error::other(err_response)); Err(std::io::Error::other(err_response))
} }
S3ErrorCode::AccessDenied => { S3ErrorCode::AccessDenied => {
if err_response.region == "" { if err_response.region == "" {
@@ -421,12 +420,12 @@ impl TransitionClient {
metadata.bucket_location = err_response.region.clone(); metadata.bucket_location = err_response.region.clone();
//continue; //continue;
} }
return Err(std::io::Error::other(err_response)); Err(std::io::Error::other(err_response))
} }
_ => { _ => {
return Err(std::io::Error::other(err_response)); Err(std::io::Error::other(err_response))
} }
} };
} }
if is_s3code_retryable(err_response.code.as_str()) { if is_s3code_retryable(err_response.code.as_str()) {
@@ -447,7 +446,7 @@ impl TransitionClient {
&self, &self,
method: &http::Method, method: &http::Method,
metadata: &mut RequestMetadata, metadata: &mut RequestMetadata,
) -> Result<http::Request<s3s::Body>, std::io::Error> { ) -> Result<Request<s3s::Body>, std::io::Error> {
let mut location = metadata.bucket_location.clone(); let mut location = metadata.bucket_location.clone();
if location == "" && metadata.bucket_name != "" { if location == "" && metadata.bucket_name != "" {
location = self.get_bucket_location(&metadata.bucket_name).await?; location = self.get_bucket_location(&metadata.bucket_name).await?;

View File

@@ -19,8 +19,9 @@ use rumqttc::QoS;
use rustfs_config::notify::{ENV_NOTIFY_MQTT_KEYS, ENV_NOTIFY_WEBHOOK_KEYS, NOTIFY_MQTT_KEYS, NOTIFY_WEBHOOK_KEYS}; use rustfs_config::notify::{ENV_NOTIFY_MQTT_KEYS, ENV_NOTIFY_WEBHOOK_KEYS, NOTIFY_MQTT_KEYS, NOTIFY_WEBHOOK_KEYS};
use rustfs_config::{ use rustfs_config::{
DEFAULT_LIMIT, EVENT_DEFAULT_DIR, MQTT_BROKER, MQTT_KEEP_ALIVE_INTERVAL, MQTT_PASSWORD, MQTT_QOS, MQTT_QUEUE_DIR, DEFAULT_LIMIT, EVENT_DEFAULT_DIR, MQTT_BROKER, MQTT_KEEP_ALIVE_INTERVAL, MQTT_PASSWORD, MQTT_QOS, MQTT_QUEUE_DIR,
MQTT_QUEUE_LIMIT, MQTT_RECONNECT_INTERVAL, MQTT_TOPIC, MQTT_USERNAME, WEBHOOK_AUTH_TOKEN, WEBHOOK_CLIENT_CERT, MQTT_QUEUE_LIMIT, MQTT_RECONNECT_INTERVAL, MQTT_TOPIC, MQTT_USERNAME, RUSTFS_WEBHOOK_SKIP_TLS_VERIFY_DEFAULT,
WEBHOOK_CLIENT_KEY, WEBHOOK_ENDPOINT, WEBHOOK_QUEUE_DIR, WEBHOOK_QUEUE_LIMIT, WEBHOOK_AUTH_TOKEN, WEBHOOK_CLIENT_CA, WEBHOOK_CLIENT_CERT, WEBHOOK_CLIENT_KEY, WEBHOOK_ENDPOINT, WEBHOOK_QUEUE_DIR,
WEBHOOK_QUEUE_LIMIT, WEBHOOK_SKIP_TLS_VERIFY,
}; };
use rustfs_ecstore::config::KVS; use rustfs_ecstore::config::KVS;
use rustfs_targets::{ use rustfs_targets::{
@@ -75,6 +76,11 @@ impl TargetFactory for WebhookTargetFactory {
.unwrap_or(DEFAULT_LIMIT), .unwrap_or(DEFAULT_LIMIT),
client_cert: config.lookup(WEBHOOK_CLIENT_CERT).unwrap_or_default(), client_cert: config.lookup(WEBHOOK_CLIENT_CERT).unwrap_or_default(),
client_key: config.lookup(WEBHOOK_CLIENT_KEY).unwrap_or_default(), client_key: config.lookup(WEBHOOK_CLIENT_KEY).unwrap_or_default(),
client_ca: config.lookup(WEBHOOK_CLIENT_CA).unwrap_or_default(),
skip_tls_verify: config
.lookup(WEBHOOK_SKIP_TLS_VERIFY)
.and_then(|v| v.parse::<bool>().ok())
.unwrap_or(RUSTFS_WEBHOOK_SKIP_TLS_VERIFY_DEFAULT),
target_type: rustfs_targets::target::TargetType::NotifyEvent, target_type: rustfs_targets::target::TargetType::NotifyEvent,
}; };

View File

@@ -64,7 +64,7 @@ pub struct FtpsServer<S> {
impl<S> FtpsServer<S> impl<S> FtpsServer<S>
where where
S: StorageBackend + Clone + Send + Sync + 'static + std::fmt::Debug, S: StorageBackend + Clone + Send + Sync + 'static + Debug,
{ {
/// Create a new FTPS server /// Create a new FTPS server
pub async fn new(config: FtpsConfig, storage: S) -> Result<Self, FtpsInitError> { pub async fn new(config: FtpsConfig, storage: S) -> Result<Self, FtpsInitError> {
@@ -130,9 +130,9 @@ where
let server_config = rustls::ServerConfig::builder() let server_config = rustls::ServerConfig::builder()
.with_no_client_auth() .with_no_client_auth()
.with_cert_resolver(std::sync::Arc::new(resolver)); .with_cert_resolver(Arc::new(resolver));
server_builder = server_builder.ftps_manual::<std::path::PathBuf>(std::sync::Arc::new(server_config)); server_builder = server_builder.ftps_manual::<std::path::PathBuf>(Arc::new(server_config));
if self.config.ftps_required { if self.config.ftps_required {
info!("FTPS is explicitly required for all connections"); info!("FTPS is explicitly required for all connections");

View File

@@ -35,7 +35,7 @@ use std::{
}; };
use tokio::net::lookup_host; use tokio::net::lookup_host;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tracing::{debug, error, info, instrument}; use tracing::{debug, error, info, instrument, warn};
/// Arguments for configuring a Webhook target /// Arguments for configuring a Webhook target
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -54,6 +54,10 @@ pub struct WebhookArgs {
pub client_cert: String, pub client_cert: String,
/// The client key for TLS (PEM format) /// The client key for TLS (PEM format)
pub client_key: String, pub client_key: String,
/// The path to a custom client root CA certificate file (PEM format) to trust the server.
pub client_ca: String,
/// Skip TLS certificate verification. DANGEROUS: for testing only.
pub skip_tls_verify: bool,
/// the target type /// the target type
pub target_type: TargetType, pub target_type: TargetType,
} }
@@ -82,6 +86,12 @@ impl WebhookArgs {
return Err(TargetError::Configuration("cert and key must be specified as a pair".to_string())); return Err(TargetError::Configuration("cert and key must be specified as a pair".to_string()));
} }
if self.skip_tls_verify && !self.client_ca.is_empty() {
return Err(TargetError::Configuration(
"skip_tls_verify and client_ca are mutually exclusive; remove client_ca or disable skip_tls_verify".to_string(),
));
}
Ok(()) Ok(())
} }
} }
@@ -125,29 +135,9 @@ where
args.validate()?; args.validate()?;
// Create a TargetID // Create a TargetID
let target_id = TargetID::new(id, ChannelTargetType::Webhook.as_str().to_string()); let target_id = TargetID::new(id, ChannelTargetType::Webhook.as_str().to_string());
// Build HTTP client
let mut client_builder = Client::builder()
.timeout(Duration::from_secs(30))
.user_agent(rustfs_utils::get_user_agent(rustfs_utils::ServiceType::Basis));
// Supplementary certificate processing logic // Build HTTP client using the helper function
if !args.client_cert.is_empty() && !args.client_key.is_empty() { let http_client = Arc::new(Self::build_http_client(&args)?);
// Add client certificate
let cert = std::fs::read(&args.client_cert)
.map_err(|e| TargetError::Configuration(format!("Failed to read client cert: {e}")))?;
let key = std::fs::read(&args.client_key)
.map_err(|e| TargetError::Configuration(format!("Failed to read client key: {e}")))?;
let identity = reqwest::Identity::from_pem(&[cert, key].concat())
.map_err(|e| TargetError::Configuration(format!("Failed to create identity: {e}")))?;
client_builder = client_builder.identity(identity);
}
let http_client = Arc::new(
client_builder
.build()
.map_err(|e| TargetError::Configuration(format!("Failed to build HTTP client: {e}")))?,
);
// Build storage // Build storage
let queue_store = if !args.queue_dir.is_empty() { let queue_store = if !args.queue_dir.is_empty() {
@@ -196,6 +186,46 @@ where
}) })
} }
fn build_http_client(args: &WebhookArgs) -> Result<Client, TargetError> {
let mut client_builder = Client::builder()
.timeout(Duration::from_secs(30))
.user_agent(rustfs_utils::get_user_agent(rustfs_utils::ServiceType::Basis));
// 1. Configure server certificate verification
if args.skip_tls_verify {
// DANGEROUS: For testing only, skip all certificate verification
client_builder = client_builder.danger_accept_invalid_certs(true);
warn!(
"Webhook target '{}' is configured to skip TLS verification. This is insecure and should not be used in production.",
args.endpoint
);
} else if !args.client_ca.is_empty() {
// Use user-provided custom CA certificate
let ca_cert_pem = std::fs::read(&args.client_ca)
.map_err(|e| TargetError::Configuration(format!("Failed to read root CA cert: {e}")))?;
let ca_cert = reqwest::Certificate::from_pem(&ca_cert_pem)
.map_err(|e| TargetError::Configuration(format!("Failed to parse root CA cert: {e}")))?;
client_builder = client_builder.add_root_certificate(ca_cert);
}
// If neither is set, use the system's default trust store
// 2. Configure client certificate (mTLS)
if !args.client_cert.is_empty() && !args.client_key.is_empty() {
let cert = std::fs::read(&args.client_cert)
.map_err(|e| TargetError::Configuration(format!("Failed to read client cert: {e}")))?;
let key = std::fs::read(&args.client_key)
.map_err(|e| TargetError::Configuration(format!("Failed to read client key: {e}")))?;
let identity = reqwest::Identity::from_pem(&[cert, key].concat())
.map_err(|e| TargetError::Configuration(format!("Failed to create identity for mTLS: {e}")))?;
client_builder = client_builder.identity(identity);
}
client_builder
.build()
.map_err(|e| TargetError::Configuration(format!("Failed to build HTTP client: {e}")))
}
async fn init(&self) -> Result<(), TargetError> { async fn init(&self) -> Result<(), TargetError> {
// Use CAS operations to ensure thread-safe initialization // Use CAS operations to ensure thread-safe initialization
if !self.initialized.load(Ordering::SeqCst) { if !self.initialized.load(Ordering::SeqCst) {
@@ -423,9 +453,60 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::target::decode_object_name; use super::WebhookArgs;
use crate::target::{TargetType, decode_object_name};
use url::Url;
use url::form_urlencoded; use url::form_urlencoded;
fn base_args() -> WebhookArgs {
WebhookArgs {
enable: true,
endpoint: Url::parse("https://example.com/hook").unwrap(),
auth_token: String::new(),
queue_dir: String::new(),
queue_limit: 0,
client_cert: String::new(),
client_key: String::new(),
client_ca: String::new(),
skip_tls_verify: false,
target_type: TargetType::NotifyEvent,
}
}
#[test]
fn test_validate_skip_tls_verify_and_client_ca_mutually_exclusive() {
let args = WebhookArgs {
skip_tls_verify: true,
client_ca: "/path/to/ca.pem".to_string(),
..base_args()
};
let result = args.validate();
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("skip_tls_verify") && err_msg.contains("client_ca"),
"Error message should mention both fields, got: {err_msg}"
);
}
#[test]
fn test_validate_skip_tls_verify_without_client_ca_is_ok() {
let args = WebhookArgs {
skip_tls_verify: true,
..base_args()
};
assert!(args.validate().is_ok());
}
#[test]
fn test_validate_client_ca_without_skip_tls_verify_is_ok() {
let args = WebhookArgs {
client_ca: "/path/to/ca.pem".to_string(),
..base_args()
};
assert!(args.validate().is_ok());
}
#[test] #[test]
fn test_decode_object_name_with_spaces() { fn test_decode_object_name_with_spaces() {
// Test case from the issue: "greeting file (2).csv" // Test case from the issue: "greeting file (2).csv"

View File

@@ -48,7 +48,6 @@ regex = { workspace = true, optional = true }
rustix = { workspace = true, optional = true } rustix = { workspace = true, optional = true }
rustfs-config = { workspace = true, features = ["constants"] } rustfs-config = { workspace = true, features = ["constants"] }
rustls = { workspace = true, optional = true } rustls = { workspace = true, optional = true }
rustls-pemfile = { workspace = true, optional = true }
rustls-pki-types = { workspace = true, optional = true } rustls-pki-types = { workspace = true, optional = true }
s3s = { workspace = true, optional = true } s3s = { workspace = true, optional = true }
serde = { workspace = true, optional = true } serde = { workspace = true, optional = true }
@@ -80,7 +79,7 @@ workspace = true
[features] [features]
default = ["ip"] # features that are enabled by default default = ["ip"] # features that are enabled by default
ip = ["dep:local-ip-address"] # ip characteristics and their dependencies ip = ["dep:local-ip-address"] # ip characteristics and their dependencies
tls = ["dep:rustls", "dep:rustls-pemfile", "dep:rustls-pki-types"] # tls characteristics and their dependencies tls = ["dep:rustls", "dep:rustls-pki-types"] # tls characteristics and their dependencies
net = ["ip", "dep:url", "dep:netif", "dep:futures", "dep:transform-stream", "dep:bytes", "dep:s3s", "dep:hyper", "dep:thiserror", "dep:tokio"] # network features with DNS resolver net = ["ip", "dep:url", "dep:netif", "dep:futures", "dep:transform-stream", "dep:bytes", "dep:s3s", "dep:hyper", "dep:thiserror", "dep:tokio"] # network features with DNS resolver
io = ["dep:tokio"] io = ["dep:tokio"]
path = [] # path manipulation features path = [] # path manipulation features

View File

@@ -15,11 +15,11 @@
use crate::get_env_bool; use crate::get_env_bool;
use rustfs_config::{RUSTFS_TLS_CERT, RUSTFS_TLS_KEY}; use rustfs_config::{RUSTFS_TLS_CERT, RUSTFS_TLS_KEY};
use rustls::RootCertStore; use rustls::RootCertStore;
use rustls::server::danger::ClientCertVerifier; use rustls::server::{
use rustls::server::{ClientHello, ResolvesServerCert, ResolvesServerCertUsingSni, WebPkiClientVerifier}; ClientHello, ResolvesServerCert, ResolvesServerCertUsingSni, WebPkiClientVerifier, danger::ClientCertVerifier,
};
use rustls::sign::CertifiedKey; use rustls::sign::CertifiedKey;
use rustls_pemfile::{certs, private_key}; use rustls_pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject};
use rustls_pki_types::{CertificateDer, PrivateKeyDer};
use std::collections::HashMap; use std::collections::HashMap;
use std::io::Error; use std::io::Error;
use std::path::Path; use std::path::Path;
@@ -42,7 +42,7 @@ pub fn load_certs(filename: &str) -> io::Result<Vec<CertificateDer<'static>>> {
let mut reader = io::BufReader::new(cert_file); let mut reader = io::BufReader::new(cert_file);
// Load and return certificate. // Load and return certificate.
let certs = certs(&mut reader) let certs = CertificateDer::pem_reader_iter(&mut reader)
.collect::<Result<Vec<_>, _>>() .collect::<Result<Vec<_>, _>>()
.map_err(|e| certs_error(format!("certificate file {filename} format error:{e:?}")))?; .map_err(|e| certs_error(format!("certificate file {filename} format error:{e:?}")))?;
if certs.is_empty() { if certs.is_empty() {
@@ -65,7 +65,7 @@ pub fn load_cert_bundle_der_bytes(path: &str) -> io::Result<Vec<Vec<u8>>> {
let pem = fs::read(path)?; let pem = fs::read(path)?;
let mut reader = io::BufReader::new(&pem[..]); let mut reader = io::BufReader::new(&pem[..]);
let certs = certs(&mut reader) let certs = CertificateDer::pem_reader_iter(&mut reader)
.collect::<Result<Vec<_>, _>>() .collect::<Result<Vec<_>, _>>()
.map_err(|e| certs_error(format!("Failed to parse PEM certs from {path}: {e}")))?; .map_err(|e| certs_error(format!("Failed to parse PEM certs from {path}: {e}")))?;
@@ -139,7 +139,8 @@ pub fn load_private_key(filename: &str) -> io::Result<PrivateKeyDer<'static>> {
let mut reader = io::BufReader::new(keyfile); let mut reader = io::BufReader::new(keyfile);
// Load and return a single private key. // Load and return a single private key.
private_key(&mut reader)?.ok_or_else(|| certs_error(format!("no private key found in {filename}"))) PrivateKeyDer::from_pem_reader(&mut reader)
.map_err(|e| certs_error(format!("failed to parse private key in {filename}: {e}")))
} }
/// error function /// error function
@@ -319,13 +320,14 @@ pub fn create_multi_cert_resolver(
/// * A boolean indicating whether TLS key logging is enabled based on the `RUSTFS_TLS_KEYLOG` environment variable. /// * A boolean indicating whether TLS key logging is enabled based on the `RUSTFS_TLS_KEYLOG` environment variable.
/// ///
pub fn tls_key_log() -> bool { pub fn tls_key_log() -> bool {
crate::get_env_bool(rustfs_config::ENV_TLS_KEYLOG, rustfs_config::DEFAULT_TLS_KEYLOG) get_env_bool(rustfs_config::ENV_TLS_KEYLOG, rustfs_config::DEFAULT_TLS_KEYLOG)
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use std::fs; use std::fs;
use std::io::ErrorKind;
use tempfile::TempDir; use tempfile::TempDir;
#[test] #[test]
@@ -333,7 +335,7 @@ mod tests {
let error_msg = "Test error message"; let error_msg = "Test error message";
let error = certs_error(error_msg.to_string()); let error = certs_error(error_msg.to_string());
assert_eq!(error.kind(), std::io::ErrorKind::Other); assert_eq!(error.kind(), ErrorKind::Other);
assert_eq!(error.to_string(), error_msg); assert_eq!(error.to_string(), error_msg);
} }
@@ -343,7 +345,7 @@ mod tests {
assert!(result.is_err()); assert!(result.is_err());
let error = result.unwrap_err(); let error = result.unwrap_err();
assert_eq!(error.kind(), std::io::ErrorKind::Other); assert_eq!(error.kind(), ErrorKind::Other);
assert!(error.to_string().contains("failed to open")); assert!(error.to_string().contains("failed to open"));
} }
@@ -353,7 +355,7 @@ mod tests {
assert!(result.is_err()); assert!(result.is_err());
let error = result.unwrap_err(); let error = result.unwrap_err();
assert_eq!(error.kind(), std::io::ErrorKind::Other); assert_eq!(error.kind(), ErrorKind::Other);
assert!(error.to_string().contains("failed to open")); assert!(error.to_string().contains("failed to open"));
} }
@@ -393,7 +395,7 @@ mod tests {
assert!(result.is_err()); assert!(result.is_err());
let error = result.unwrap_err(); let error = result.unwrap_err();
assert!(error.to_string().contains("no private key found")); assert!(error.to_string().contains("failed to parse private key in"));
} }
#[test] #[test]
@@ -406,7 +408,7 @@ mod tests {
assert!(result.is_err()); assert!(result.is_err());
let error = result.unwrap_err(); let error = result.unwrap_err();
assert!(error.to_string().contains("no private key found")); assert!(error.to_string().contains("failed to parse private key in"));
} }
#[test] #[test]
@@ -585,11 +587,8 @@ mod tests {
#[test] #[test]
fn test_memory_efficiency() { fn test_memory_efficiency() {
// Test that error types are reasonably sized
use std::mem;
let error = certs_error("test".to_string()); let error = certs_error("test".to_string());
let error_size = mem::size_of_val(&error); let error_size = std::mem::size_of_val(&error);
// Error should not be excessively large // Error should not be excessively large
assert!(error_size < 1024, "Error size should be reasonable, got {error_size} bytes"); assert!(error_size < 1024, "Error size should be reasonable, got {error_size} bytes");

View File

@@ -99,7 +99,6 @@ serde_urlencoded = { workspace = true }
# Cryptography and Security # Cryptography and Security
rustls = { workspace = true } rustls = { workspace = true }
subtle = { workspace = true } subtle = { workspace = true }
rustls-pemfile = { workspace = true }
jiff = { workspace = true } jiff = { workspace = true }
time = { workspace = true, features = ["parsing", "formatting", "serde"] } time = { workspace = true, features = ["parsing", "formatting", "serde"] }

View File

@@ -14,7 +14,7 @@
use rustfs_common::{MtlsIdentityPem, set_global_mtls_identity, set_global_root_cert}; use rustfs_common::{MtlsIdentityPem, set_global_mtls_identity, set_global_root_cert};
use rustfs_config::{RUSTFS_CA_CERT, RUSTFS_PUBLIC_CERT, RUSTFS_TLS_CERT}; use rustfs_config::{RUSTFS_CA_CERT, RUSTFS_PUBLIC_CERT, RUSTFS_TLS_CERT};
use rustls::pki_types::{CertificateDer, PrivateKeyDer}; use rustls::pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use tracing::{debug, info}; use tracing::{debug, info};
@@ -47,7 +47,7 @@ impl std::error::Error for RustFSError {}
fn parse_pem_certs(pem: &[u8]) -> Result<Vec<CertificateDer<'static>>, RustFSError> { fn parse_pem_certs(pem: &[u8]) -> Result<Vec<CertificateDer<'static>>, RustFSError> {
let mut out = Vec::new(); let mut out = Vec::new();
let mut reader = std::io::Cursor::new(pem); let mut reader = std::io::Cursor::new(pem);
for item in rustls_pemfile::certs(&mut reader) { for item in CertificateDer::pem_reader_iter(&mut reader) {
let c = item.map_err(|e| RustFSError::Cert(format!("parse cert pem: {e}")))?; let c = item.map_err(|e| RustFSError::Cert(format!("parse cert pem: {e}")))?;
out.push(c); out.push(c);
} }
@@ -67,8 +67,7 @@ fn parse_pem_certs(pem: &[u8]) -> Result<Vec<CertificateDer<'static>>, RustFSErr
/// Returns `RustFSError` if parsing fails or no key is found. /// Returns `RustFSError` if parsing fails or no key is found.
fn parse_pem_private_key(pem: &[u8]) -> Result<PrivateKeyDer<'static>, RustFSError> { fn parse_pem_private_key(pem: &[u8]) -> Result<PrivateKeyDer<'static>, RustFSError> {
let mut reader = std::io::Cursor::new(pem); let mut reader = std::io::Cursor::new(pem);
let key = rustls_pemfile::private_key(&mut reader).map_err(|e| RustFSError::Cert(format!("parse private key pem: {e}")))?; PrivateKeyDer::from_pem_reader(&mut reader).map_err(|e| RustFSError::Cert(format!("parse private key pem: {e}")))
key.ok_or_else(|| RustFSError::Cert("no private key found in PEM".into()))
} }
/// Helper function to read a file and return its contents. /// Helper function to read a file and return its contents.
@@ -103,6 +102,9 @@ pub(crate) async fn init_cert(tls_path: &str) -> Result<(), RustFSError> {
info!("No TLS path configured; skipping certificate initialization"); info!("No TLS path configured; skipping certificate initialization");
return Ok(()); return Ok(());
} }
// Make sure to use a modern encryption suite
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
let tls_dir = PathBuf::from(tls_path); let tls_dir = PathBuf::from(tls_path);
// Load root certificates // Load root certificates

View File

@@ -441,10 +441,7 @@ async fn setup_tls_acceptor(tls_path: &str) -> Result<Option<TlsAcceptor>> {
} }
debug!("Found TLS directory, checking for certificates"); debug!("Found TLS directory, checking for certificates");
// Make sure to use a modern encryption suite
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
let mtls_verifier = rustfs_utils::build_webpki_client_verifier(tls_path)?; let mtls_verifier = rustfs_utils::build_webpki_client_verifier(tls_path)?;
// 1. Attempt to load all certificates in the directory (multi-certificate support, for SNI) // 1. Attempt to load all certificates in the directory (multi-certificate support, for SNI)
if let Ok(cert_key_pairs) = rustfs_utils::load_all_certs_from_directory(tls_path) if let Ok(cert_key_pairs) = rustfs_utils::load_all_certs_from_directory(tls_path)
&& !cert_key_pairs.is_empty() && !cert_key_pairs.is_empty()