security: redact IAM and target debug secrets (#3306)

This commit is contained in:
安正超
2026-06-10 09:03:50 +08:00
committed by GitHub
parent 3795d44b86
commit a73c90c811
8 changed files with 342 additions and 25 deletions
+51 -1
View File
@@ -32,6 +32,7 @@ use rustfs_policy::policy::{ClaimLookup, get_claim_case_insensitive};
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::collections::{HashMap, VecDeque};
use std::fmt;
use std::future::Future;
use std::net::IpAddr;
use std::pin::Pin;
@@ -275,8 +276,14 @@ impl<'c> AsyncHttpClient<'c> for ReqwestHttpClient {
// ---- Public types (unchanged API) ----
const REDACTED_SECRET: &str = "***redacted***";
fn redacted_optional_secret(value: Option<&str>) -> &'static str {
value.filter(|secret| !secret.is_empty()).map_or("", |_| REDACTED_SECRET)
}
/// Parsed configuration for a single OIDC provider.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Clone, PartialEq, Eq)]
pub struct OidcProviderConfig {
pub id: String,
pub enabled: bool,
@@ -298,6 +305,31 @@ pub struct OidcProviderConfig {
pub hide_from_ui: bool,
}
impl fmt::Debug for OidcProviderConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("OidcProviderConfig")
.field("id", &self.id)
.field("enabled", &self.enabled)
.field("config_url", &self.config_url)
.field("client_id", &self.client_id)
.field("client_secret", &redacted_optional_secret(self.client_secret.as_deref()))
.field("scopes", &self.scopes)
.field("other_audiences", &self.other_audiences)
.field("redirect_uri", &self.redirect_uri)
.field("redirect_uri_dynamic", &self.redirect_uri_dynamic)
.field("claim_name", &self.claim_name)
.field("claim_prefix", &self.claim_prefix)
.field("role_policy", &self.role_policy)
.field("display_name", &self.display_name)
.field("groups_claim", &self.groups_claim)
.field("roles_claim", &self.roles_claim)
.field("email_claim", &self.email_claim)
.field("username_claim", &self.username_claim)
.field("hide_from_ui", &self.hide_from_ui)
.finish()
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum OidcProviderConfigSource {
@@ -1978,6 +2010,24 @@ mod tests {
}
}
#[test]
fn test_oidc_provider_config_debug_redacts_client_secret() {
let config = OidcProviderConfig {
client_secret: Some("oidc-client-secret".to_string()),
..test_config("default")
};
let sourced = SourcedOidcProviderConfig {
config,
source: OidcProviderConfigSource::Persisted,
};
let rendered = format!("{sourced:?}");
assert!(!rendered.contains("oidc-client-secret"));
assert!(rendered.contains(REDACTED_SECRET));
assert!(rendered.contains("client-id"));
}
#[test]
fn test_parse_enable_state_on() {
assert!(OidcSys::parse_enable_state("on", false, false));
+10
View File
@@ -44,6 +44,16 @@ pub(crate) use crate::runtime::tls::fingerprint::TargetTlsGeneration;
pub(crate) use crate::runtime::tls::fingerprint::TargetTlsState;
pub(crate) use crate::runtime::tls::fingerprint::build_target_tls_fingerprint;
pub(crate) const REDACTED_SECRET: &str = "***redacted***";
pub(crate) fn redacted_secret(value: &str) -> &'static str {
if value.is_empty() { "" } else { REDACTED_SECRET }
}
pub(crate) fn redacted_optional_secret(value: Option<&str>) -> &'static str {
value.filter(|secret| !secret.is_empty()).map_or("", |_| REDACTED_SECRET)
}
/// A read-only snapshot of delivery counters for a target.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TargetDeliverySnapshot {
+67 -4
View File
@@ -24,7 +24,7 @@ use crate::{
target::{
ChannelTargetType, EntityTarget, QueuedPayload, QueuedPayloadMeta, TargetDeliveryCounters, TargetDeliverySnapshot,
TargetType, build_queued_payload_with_records, mark_target_disconnected_on_connectivity_error, open_target_queue_store,
persist_queued_payload_to_store,
persist_queued_payload_to_store, redacted_secret,
},
};
use arc_swap::ArcSwap;
@@ -41,6 +41,7 @@ use rustfs_tls_runtime::{load_certs, load_private_key};
use rustls::ClientConfig;
use serde::Serialize;
use serde::de::DeserializeOwned;
use std::fmt;
use std::sync::Arc;
use std::{
marker::PhantomData,
@@ -78,7 +79,7 @@ impl MQTTTlsPolicy {
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[derive(Clone, Default, PartialEq, Eq)]
pub struct MQTTTlsConfig {
pub policy: Option<MQTTTlsPolicy>,
pub ca_path: String,
@@ -88,6 +89,19 @@ pub struct MQTTTlsConfig {
pub ws_path_allowlist: Vec<String>,
}
impl fmt::Debug for MQTTTlsConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MQTTTlsConfig")
.field("policy", &self.policy)
.field("ca_path", &self.ca_path)
.field("client_cert_path", &self.client_cert_path)
.field("client_key_path", &redacted_secret(&self.client_key_path))
.field("trust_leaf_as_ca", &self.trust_leaf_as_ca)
.field("ws_path_allowlist", &self.ws_path_allowlist)
.finish()
}
}
impl MQTTTlsConfig {
pub fn from_values(
policy: Option<&str>,
@@ -422,7 +436,7 @@ pub(crate) fn build_mqtt_options(
}
/// Arguments for configuring an MQTT target
#[derive(Debug, Clone)]
#[derive(Clone)]
pub struct MQTTArgs {
/// Whether the target is enabled
pub enable: bool,
@@ -450,6 +464,25 @@ pub struct MQTTArgs {
pub target_type: TargetType,
}
impl fmt::Debug for MQTTArgs {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MQTTArgs")
.field("enable", &self.enable)
.field("broker", &self.broker)
.field("topic", &self.topic)
.field("qos", &self.qos)
.field("username", &self.username)
.field("password", &redacted_secret(&self.password))
.field("tls", &self.tls)
.field("max_reconnect_interval", &self.max_reconnect_interval)
.field("keep_alive", &self.keep_alive)
.field("queue_dir", &self.queue_dir)
.field("queue_limit", &self.queue_limit)
.field("target_type", &self.target_type)
.finish()
}
}
impl MQTTArgs {
pub fn validate(&self) -> Result<(), TargetError> {
if !self.enable {
@@ -1087,7 +1120,9 @@ where
#[cfg(test)]
mod tests {
use super::{MQTTTlsConfig, validate_mqtt_broker_url};
use super::{MQTTArgs, MQTTTlsConfig, QoS, validate_mqtt_broker_url};
use crate::target::{REDACTED_SECRET, TargetType};
use std::time::Duration;
use url::Url;
#[test]
@@ -1125,6 +1160,34 @@ mod tests {
assert!(err.to_string().contains("must not embed username or password"));
}
#[test]
fn debug_redacts_mqtt_secret_fields() {
let args = MQTTArgs {
enable: true,
broker: Url::parse("mqtt://broker.example.com:1883").expect("valid broker"),
topic: "rustfs/events".to_string(),
qos: QoS::AtLeastOnce,
username: "mqtt-user".to_string(),
password: "mqtt-password".to_string(),
tls: MQTTTlsConfig {
client_key_path: "/etc/rustfs/mqtt.key".to_string(),
..MQTTTlsConfig::default()
},
max_reconnect_interval: Duration::from_secs(1),
keep_alive: Duration::from_secs(30),
queue_dir: String::new(),
queue_limit: 0,
target_type: TargetType::NotifyEvent,
};
let rendered = format!("{args:?}");
assert!(!rendered.contains("mqtt-password"));
assert!(!rendered.contains("/etc/rustfs/mqtt.key"));
assert!(rendered.contains(REDACTED_SECRET));
assert!(rendered.contains("mqtt-user"));
}
#[test]
fn validate_mqtt_broker_url_requires_explicit_tls_policy_for_secure_scheme() {
let url = Url::parse("mqtts://broker.example.com:8883").expect("valid url");
+63 -3
View File
@@ -24,7 +24,7 @@ use crate::{
target::{
ChannelTargetType, EntityTarget, QueuedPayload, QueuedPayloadMeta, TargetDeliveryCounters, TargetDeliverySnapshot,
TargetType, build_queued_payload, delete_stored_payload, is_connectivity_error, open_target_queue_store,
persist_queued_payload_to_store,
persist_queued_payload_to_store, redacted_secret,
},
};
use async_trait::async_trait;
@@ -33,6 +33,7 @@ use rustfs_config::{MYSQL_TLS_CA, MYSQL_TLS_CLIENT_CERT, MYSQL_TLS_CLIENT_KEY};
use rustfs_tls_runtime::{load_certs, load_private_key};
use serde::Serialize;
use serde::de::DeserializeOwned;
use std::fmt;
use std::marker::PhantomData;
use std::path::{Path, PathBuf};
use std::sync::Arc;
@@ -43,7 +44,7 @@ use tracing::{debug, error, info, warn};
///
/// Contains all configuration values needed to connect to a MySQL/TiDB
/// database and write event notification records.
#[derive(Debug, Clone)]
#[derive(Clone)]
pub struct MySqlArgs {
/// Whether the target is enabled
pub enable: bool,
@@ -69,6 +70,24 @@ pub struct MySqlArgs {
pub target_type: TargetType,
}
impl fmt::Debug for MySqlArgs {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MySqlArgs")
.field("enable", &self.enable)
.field("dsn_string", &redact_mysql_dsn(&self.dsn_string))
.field("table", &self.table)
.field("format", &self.format)
.field("tls_ca", &self.tls_ca)
.field("tls_client_cert", &self.tls_client_cert)
.field("tls_client_key", &redacted_secret(&self.tls_client_key))
.field("queue_dir", &self.queue_dir)
.field("queue_limit", &self.queue_limit)
.field("max_open_connections", &self.max_open_connections)
.field("target_type", &self.target_type)
.finish()
}
}
impl MySqlArgs {
/// Validates the MySQL target configuration.
pub fn validate(&self) -> Result<(), TargetError> {
@@ -122,7 +141,7 @@ impl MySqlArgs {
///
/// Produced by [`MySqlDsn::parse`] and consumed by the MySQL
/// target runtime to build connection options.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Clone, PartialEq, Eq)]
pub struct MySqlDsn {
/// MySQL user name
pub user: String,
@@ -138,6 +157,19 @@ pub struct MySqlDsn {
pub tls: bool,
}
impl fmt::Debug for MySqlDsn {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MySqlDsn")
.field("user", &self.user)
.field("password", &redacted_secret(&self.password))
.field("host", &self.host)
.field("port", &self.port)
.field("database", &self.database)
.field("tls", &self.tls)
.finish()
}
}
impl MySqlDsn {
/// Parses a MySQL DSN string into its components.
///
@@ -931,6 +963,7 @@ where
#[cfg(test)]
mod tests {
use super::*;
use crate::target::REDACTED_SECRET;
fn absolute_test_path(path: &str) -> String {
std::env::temp_dir().join(path).to_string_lossy().into_owned()
@@ -1036,6 +1069,33 @@ mod tests {
assert_eq!(redacted, "root:***@tcp(127.0.0.1:4000)/testdb");
}
#[test]
fn debug_redacts_mysql_secret_fields() {
let args = MySqlArgs {
enable: true,
dsn_string: "rustfs:mysql-password@tcp(127.0.0.1:3306)/db".to_string(),
table: "events".to_string(),
format: "access".to_string(),
tls_ca: String::new(),
tls_client_cert: String::new(),
tls_client_key: "/etc/rustfs/mysql.key".to_string(),
queue_dir: String::new(),
queue_limit: 0,
max_open_connections: 0,
target_type: TargetType::NotifyEvent,
};
let dsn = MySqlDsn::parse(&args.dsn_string).expect("valid DSN");
let rendered_args = format!("{args:?}");
let rendered_dsn = format!("{dsn:?}");
assert!(!rendered_args.contains("mysql-password"));
assert!(!rendered_args.contains("/etc/rustfs/mysql.key"));
assert!(!rendered_dsn.contains("mysql-password"));
assert!(rendered_args.contains("rustfs:***@"));
assert!(rendered_dsn.contains(REDACTED_SECRET));
}
#[test]
fn validate_table_name_accepts_valid_identifier() {
validate_table_name("rustfs_events").expect("valid table name");
+45 -2
View File
@@ -24,13 +24,14 @@ use crate::{
target::{
ChannelTargetType, EntityTarget, QueuedPayload, QueuedPayloadMeta, TargetDeliveryCounters, TargetDeliverySnapshot,
TargetTlsState, TargetType, build_queued_payload_with_records, build_target_tls_fingerprint, open_target_queue_store,
persist_queued_payload_to_store,
persist_queued_payload_to_store, redacted_secret,
},
};
use async_trait::async_trait;
use rustfs_config::{NATS_CREDENTIALS_FILE, NATS_TLS_CA, NATS_TLS_CLIENT_CERT, NATS_TLS_CLIENT_KEY};
use serde::Serialize;
use serde::de::DeserializeOwned;
use std::fmt;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::Arc;
@@ -38,7 +39,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
use tokio::sync::Mutex;
use tracing::{info, instrument, warn};
#[derive(Debug, Clone)]
#[derive(Clone)]
pub struct NATSArgs {
pub enable: bool,
pub address: String,
@@ -56,6 +57,27 @@ pub struct NATSArgs {
pub target_type: TargetType,
}
impl fmt::Debug for NATSArgs {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("NATSArgs")
.field("enable", &self.enable)
.field("address", &self.address)
.field("subject", &self.subject)
.field("username", &self.username)
.field("password", &redacted_secret(&self.password))
.field("token", &redacted_secret(&self.token))
.field("credentials_file", &redacted_secret(&self.credentials_file))
.field("tls_ca", &self.tls_ca)
.field("tls_client_cert", &self.tls_client_cert)
.field("tls_client_key", &redacted_secret(&self.tls_client_key))
.field("tls_required", &self.tls_required)
.field("queue_dir", &self.queue_dir)
.field("queue_limit", &self.queue_limit)
.field("target_type", &self.target_type)
.finish()
}
}
impl NATSArgs {
pub fn validate(&self) -> Result<(), TargetError> {
if !self.enable {
@@ -429,6 +451,7 @@ where
#[cfg(test)]
mod tests {
use super::*;
use crate::target::REDACTED_SECRET;
fn base_args() -> NATSArgs {
NATSArgs {
@@ -449,6 +472,26 @@ mod tests {
}
}
#[test]
fn debug_redacts_nats_secret_fields() {
let args = NATSArgs {
password: "nats-password".to_string(),
token: "nats-token".to_string(),
credentials_file: "/etc/rustfs/nats.creds".to_string(),
tls_client_key: "/etc/rustfs/nats.key".to_string(),
..base_args()
};
let rendered = format!("{args:?}");
assert!(!rendered.contains("nats-password"));
assert!(!rendered.contains("nats-token"));
assert!(!rendered.contains("/etc/rustfs/nats.creds"));
assert!(!rendered.contains("/etc/rustfs/nats.key"));
assert!(rendered.contains(REDACTED_SECRET));
assert!(rendered.contains("rustfs.events"));
}
#[test]
fn validate_nats_rejects_multiple_auth_methods() {
let args = NATSArgs {
+30 -10
View File
@@ -36,7 +36,8 @@ use crate::{
store::{Key, Store},
target::{
ChannelTargetType, EntityTarget, QueuedPayload, QueuedPayloadMeta, TargetDeliveryCounters, TargetDeliverySnapshot,
TargetType, build_queued_payload, open_target_queue_store, persist_queued_payload_to_store,
TargetType, build_queued_payload, open_target_queue_store, persist_queued_payload_to_store, redacted_optional_secret,
redacted_secret,
},
};
use async_trait::async_trait;
@@ -94,7 +95,7 @@ pub fn parse_postgres_format(value: Option<&str>) -> Result<PostgresFormat, Targ
}
/// Parsed representation of a PostgreSQL DSN string.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Clone, PartialEq, Eq)]
pub struct PostgresDsn {
pub host: String,
pub port: u16,
@@ -104,6 +105,19 @@ pub struct PostgresDsn {
pub schema: String,
}
impl fmt::Debug for PostgresDsn {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PostgresDsn")
.field("host", &self.host)
.field("port", &self.port)
.field("user", &self.user)
.field("password", &redacted_optional_secret(self.password.as_deref()))
.field("database", &self.database)
.field("schema", &self.schema)
.finish()
}
}
impl PostgresDsn {
/// Parses and validates PostgreSQL DSN string.
///
@@ -300,14 +314,7 @@ impl fmt::Debug for PostgresArgs {
.field("tls_required", &self.tls_required)
.field("tls_ca", &self.tls_ca)
.field("tls_client_cert", &self.tls_client_cert)
.field(
"tls_client_key",
if self.tls_client_key.is_empty() {
&""
} else {
&"***REDACTED***"
},
)
.field("tls_client_key", &redacted_secret(&self.tls_client_key))
.field("queue_dir", &self.queue_dir)
.field("queue_limit", &self.queue_limit)
.field("target_type", &self.target_type)
@@ -831,6 +838,7 @@ where
#[cfg(test)]
mod tests {
use super::*;
use crate::target::REDACTED_SECRET;
fn base_args() -> PostgresArgs {
PostgresArgs {
@@ -1042,6 +1050,18 @@ mod tests {
assert!(!rendered.contains(":***@"));
}
#[test]
fn debug_redacts_postgres_dsn_password() {
let dsn = PostgresDsn::parse("postgres://postgres:pg-secret@localhost:5432/rustfs_events?search_path=public")
.expect("valid DSN");
let rendered = format!("{dsn:?}");
assert!(!rendered.contains("pg-secret"));
assert!(rendered.contains(REDACTED_SECRET));
assert!(rendered.contains("rustfs_events"));
}
#[test]
fn redact_postgres_dsn_masks_password_query_parameter() {
let redacted = redact_postgres_dsn("postgres://postgres@localhost:5432/db?search_path=public&password=secret");
+39 -2
View File
@@ -24,7 +24,7 @@ use crate::{
target::{
ChannelTargetType, EntityTarget, QueuedPayload, QueuedPayloadMeta, TargetDeliveryCounters, TargetDeliverySnapshot,
TargetTlsState, TargetType, build_queued_payload_with_records, build_target_tls_fingerprint, open_target_queue_store,
persist_queued_payload_to_store,
persist_queued_payload_to_store, redacted_secret,
},
};
use async_trait::async_trait;
@@ -32,6 +32,7 @@ use pulsar::{Authentication, Producer, Pulsar, TokioExecutor};
use rustfs_tls_runtime::load_cert_bundle_der_bytes;
use serde::Serialize;
use serde::de::DeserializeOwned;
use std::fmt;
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
@@ -39,7 +40,7 @@ use tokio::sync::Mutex as AsyncMutex;
use tracing::{info, instrument};
use url::Url;
#[derive(Debug, Clone)]
#[derive(Clone)]
pub struct PulsarArgs {
pub enable: bool,
pub broker: String,
@@ -55,6 +56,25 @@ pub struct PulsarArgs {
pub target_type: TargetType,
}
impl fmt::Debug for PulsarArgs {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PulsarArgs")
.field("enable", &self.enable)
.field("broker", &self.broker)
.field("topic", &self.topic)
.field("auth_token", &redacted_secret(&self.auth_token))
.field("username", &self.username)
.field("password", &redacted_secret(&self.password))
.field("tls_ca", &self.tls_ca)
.field("tls_allow_insecure", &self.tls_allow_insecure)
.field("tls_hostname_verification", &self.tls_hostname_verification)
.field("queue_dir", &self.queue_dir)
.field("queue_limit", &self.queue_limit)
.field("target_type", &self.target_type)
.finish()
}
}
impl PulsarArgs {
pub fn validate(&self) -> Result<(), TargetError> {
if !self.enable {
@@ -463,6 +483,7 @@ where
#[cfg(test)]
mod tests {
use super::*;
use crate::target::REDACTED_SECRET;
fn base_args() -> PulsarArgs {
PulsarArgs {
@@ -481,6 +502,22 @@ mod tests {
}
}
#[test]
fn debug_redacts_pulsar_secret_fields() {
let args = PulsarArgs {
auth_token: "pulsar-token".to_string(),
password: "pulsar-password".to_string(),
..base_args()
};
let rendered = format!("{args:?}");
assert!(!rendered.contains("pulsar-token"));
assert!(!rendered.contains("pulsar-password"));
assert!(rendered.contains(REDACTED_SECRET));
assert!(rendered.contains("persistent://public/default/rustfs-events"));
}
#[test]
fn validate_pulsar_rejects_mixed_auth_methods() {
let args = PulsarArgs {
+37 -3
View File
@@ -24,7 +24,7 @@ use crate::{
target::{
ChannelTargetType, EntityTarget, QueuedPayload, QueuedPayloadMeta, TargetDeliveryCounters, TargetDeliverySnapshot,
TargetTlsState, TargetType, build_queued_payload, build_target_tls_fingerprint, open_target_queue_store,
persist_queued_payload_to_store,
persist_queued_payload_to_store, redacted_secret,
},
};
use async_trait::async_trait;
@@ -34,6 +34,7 @@ use rustfs_tls_runtime::load_cert_bundle_der_bytes;
use serde::Serialize;
use serde::de::DeserializeOwned;
use std::{
fmt,
marker::PhantomData,
sync::{
Arc,
@@ -45,7 +46,7 @@ use tokio::sync::mpsc;
use tracing::{debug, error, info, instrument, warn};
/// Arguments for configuring a Webhook target
#[derive(Debug, Clone)]
#[derive(Clone)]
pub struct WebhookArgs {
/// Whether the target is enabled
pub enable: bool,
@@ -69,6 +70,23 @@ pub struct WebhookArgs {
pub target_type: TargetType,
}
impl fmt::Debug for WebhookArgs {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("WebhookArgs")
.field("enable", &self.enable)
.field("endpoint", &self.endpoint)
.field("auth_token", &redacted_secret(&self.auth_token))
.field("queue_dir", &self.queue_dir)
.field("queue_limit", &self.queue_limit)
.field("client_cert", &self.client_cert)
.field("client_key", &redacted_secret(&self.client_key))
.field("client_ca", &self.client_ca)
.field("skip_tls_verify", &self.skip_tls_verify)
.field("target_type", &self.target_type)
.finish()
}
}
impl WebhookArgs {
/// WebhookArgs verification method
pub fn validate(&self) -> Result<(), TargetError> {
@@ -562,7 +580,7 @@ where
#[cfg(test)]
mod tests {
use super::{WebhookArgs, WebhookTarget};
use crate::target::{Target, TargetType, decode_object_name};
use crate::target::{REDACTED_SECRET, Target, TargetType, decode_object_name};
use tokio::net::TcpListener;
use url::Url;
use url::form_urlencoded;
@@ -582,6 +600,22 @@ mod tests {
}
}
#[test]
fn debug_redacts_webhook_secret_fields() {
let args = WebhookArgs {
auth_token: "webhook-token".to_string(),
client_key: "/etc/rustfs/webhook.key".to_string(),
..base_args()
};
let rendered = format!("{args:?}");
assert!(!rendered.contains("webhook-token"));
assert!(!rendered.contains("/etc/rustfs/webhook.key"));
assert!(rendered.contains(REDACTED_SECRET));
assert!(rendered.contains("WebhookArgs"));
}
#[test]
fn test_validate_skip_tls_verify_and_client_ca_mutually_exclusive() {
let args = WebhookArgs {