From 21ef6d505ec5bb4dbbd7e68057d21ae4738b0ba2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jasmine=20Lowen=20=F0=9F=A6=81?= <26892280+JasmineLowen@users.noreply.github.com> Date: Sun, 15 Feb 2026 09:28:52 +0100 Subject: [PATCH] feat(config): allow specifying keys via files (key files) (#1814) Co-authored-by: houseme Co-authored-by: heihutu <30542132+heihutu@users.noreply.github.com> --- rustfs/src/config/config_test.rs | 78 ++++++++++++++ rustfs/src/config/mod.rs | 171 ++++++++++++++++++++++++++++--- rustfs/src/init.rs | 32 +++--- rustfs/src/main.rs | 45 ++++---- rustfs/src/server/http.rs | 26 ++--- 5 files changed, 286 insertions(+), 66 deletions(-) diff --git a/rustfs/src/config/config_test.rs b/rustfs/src/config/config_test.rs index 4e449b04d..44ee41799 100644 --- a/rustfs/src/config/config_test.rs +++ b/rustfs/src/config/config_test.rs @@ -523,4 +523,82 @@ mod tests { assert_eq!(opt.server_domains[1], "127.0.0.1:9000"); assert_eq!(opt.server_domains[2], "localhost"); } + + #[test] + fn test_access_key_arguments_mutually_exclusive_cli() { + // Test that CLI args configuration fails on conflict + let args = vec![ + "rustfs", + "/test/volume", + "--access-key", + "foobar", + "--access-key-file", + "/foo/bar", + ]; + let opt_res = Opt::try_parse_from(args); + + // can't specify both access-key and access-key-file at once + assert!(opt_res.is_err()); + let err = opt_res.err().unwrap(); + assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict); + } + + #[test] + #[serial] + #[allow(unsafe_code)] + fn test_access_key_arguments_mutually_exclusive_env_var() { + // Test that env var args configuration fails on conflict + with_env_var("RUSTFS_VOLUMES", "/data/my disk/vol1", || { + with_env_var("RUSTFS_ACCESS_KEY", "foo", || { + with_env_var("RUSTFS_ACCESS_KEY_FILE", "/foo/bar", || { + let args = vec!["rustfs"]; + let opt_res = Opt::try_parse_from(args); + + // can't specify both RUSTFS_ACCESS_KEY and RUSTFS_ACCESS_KEY_FILE at once + assert!(opt_res.is_err()); + let err = opt_res.err().unwrap(); + assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict); + }); + }); + }); + } + + #[test] + fn test_secret_key_arguments_mutually_exclusive_cli() { + // Test that CLI args configuration fails on conflict + let args = vec![ + "rustfs", + "/test/volume", + "--secret-key", + "foobar", + "--secret-key-file", + "/foo/bar", + ]; + let opt_res = Opt::try_parse_from(args); + + // can't specify both secret-key and secret-key-file at once + assert!(opt_res.is_err()); + let err = opt_res.err().unwrap(); + assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict); + } + + #[test] + #[serial] + #[allow(unsafe_code)] + fn test_secret_key_arguments_mutually_exclusive_env_var() { + // Test that env var args configuration fails on conflict + with_env_var("RUSTFS_VOLUMES", "/data/my disk/vol1", || { + with_env_var("RUSTFS_SECRET_KEY", "foo", || { + with_env_var("RUSTFS_SECRET_KEY_FILE", "/foo/bar", || { + let args = vec!["rustfs"]; + let opt_res = Opt::try_parse_from(args); + + // can't specify both RUSTFS_SECRET_KEY and RUSTFS_SECRET_KEY_FILE at once + assert!(opt_res.is_err()); + let err = opt_res.err().unwrap(); + assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict); + }); + }); + }); + } } diff --git a/rustfs/src/config/mod.rs b/rustfs/src/config/mod.rs index 9a2bb9a40..1705d6aa4 100644 --- a/rustfs/src/config/mod.rs +++ b/rustfs/src/config/mod.rs @@ -15,6 +15,7 @@ use clap::Parser; use clap::builder::NonEmptyStringValueParser; use const_str::concat; +use std::path::PathBuf; use std::string::ToString; shadow_rs::shadow!(build); @@ -77,20 +78,20 @@ pub struct Opt { pub server_domains: Vec, /// Access key used for authentication. - #[arg( - long, - default_value_t = rustfs_credentials::DEFAULT_ACCESS_KEY.to_string(), - env = "RUSTFS_ACCESS_KEY" - )] - pub access_key: String, + #[arg(long, env = "RUSTFS_ACCESS_KEY", group = "access-key")] + pub access_key: Option, + + /// Access key stored in a file used for authentication. + #[arg(long, env = "RUSTFS_ACCESS_KEY_FILE", group = "access-key")] + pub access_key_file: Option, /// Secret key used for authentication. - #[arg( - long, - default_value_t = rustfs_credentials::DEFAULT_SECRET_KEY.to_string(), - env = "RUSTFS_SECRET_KEY" - )] - pub secret_key: String, + #[arg(long, env = "RUSTFS_SECRET_KEY", group = "secret-key")] + pub secret_key: Option, + + /// Secret key stored in a file used for authentication. + #[arg(long, env = "RUSTFS_SECRET_KEY_FILE", group = "secret-key")] + pub secret_key_file: Option, /// Enable console server #[arg( @@ -161,9 +162,151 @@ pub struct Opt { pub buffer_profile: String, } -impl std::fmt::Debug for Opt { +#[derive(Clone)] +pub struct Config { + /// DIR points to a directory on a filesystem. + pub volumes: Vec, + + /// bind to a specific ADDRESS:PORT, ADDRESS can be an IP or hostname + pub address: String, + + /// Domain name used for virtual-hosted-style requests. + pub server_domains: Vec, + + /// Access key used for authentication. + pub access_key: String, + + /// Secret key used for authentication. + pub secret_key: String, + + /// Enable console server + pub console_enable: bool, + + /// Console server bind address + pub console_address: String, + + /// Observability endpoint for trace, metrics and logs,only support grpc mode. + pub obs_endpoint: String, + + /// tls path for rustfs API and console. + pub tls_path: Option, + + pub license: Option, + + pub region: Option, + + /// Enable KMS encryption for server-side encryption + pub kms_enable: bool, + + /// KMS backend type (local or vault) + pub kms_backend: String, + + /// KMS key directory for local backend + pub kms_key_dir: Option, + + /// Vault address for vault backend + pub kms_vault_address: Option, + + /// Vault token for vault backend + pub kms_vault_token: Option, + + /// Default KMS key ID for encryption + pub kms_default_key_id: Option, + + /// Disable adaptive buffer sizing with workload profiles + /// Set this flag to use legacy fixed-size buffer behavior from PR #869 + pub buffer_profile_disable: bool, + + /// Workload profile for adaptive buffer sizing + /// Options: GeneralPurpose, AiTraining, DataAnalytics, WebWorkload, IndustrialIoT, SecureStorage + pub buffer_profile: String, +} + +impl Config { + /// parse the command line arguments and environment arguments from [`Opt`] and convert them + /// into a ready to use [`Config`] + /// + /// This includes some intermediate checks for mutually exclusive options + pub fn parse() -> std::io::Result { + let Opt { + volumes, + address, + server_domains, + access_key, + access_key_file, + secret_key, + secret_key_file, + console_enable, + console_address, + obs_endpoint, + tls_path, + license, + region, + kms_enable, + kms_backend, + kms_key_dir, + kms_vault_address, + kms_vault_token, + kms_default_key_id, + buffer_profile_disable, + buffer_profile, + } = Opt::parse(); + + let access_key = access_key + .map(Ok) + .or_else(|| { + let path = access_key_file.as_ref()?; + Some(std::fs::read_to_string(path)) + }) + .transpose()? + .unwrap_or_else(|| { + // neither argument was specified ... using default + rustfs_credentials::DEFAULT_ACCESS_KEY.to_string() + }) + .trim() + .to_string(); + + let secret_key = secret_key + .map(Ok) + .or_else(|| { + let path = secret_key_file.as_ref()?; + Some(std::fs::read_to_string(path)) + }) + .transpose()? + .unwrap_or_else(|| { + // neither argument was specified ... using default + rustfs_credentials::DEFAULT_SECRET_KEY.to_string() + }) + .trim() + .to_string(); + + Ok(Config { + volumes, + address, + server_domains, + access_key, + secret_key, + console_enable, + console_address, + obs_endpoint, + tls_path, + license, + region, + kms_enable, + kms_backend, + kms_key_dir, + kms_vault_address, + kms_vault_token, + kms_default_key_id, + buffer_profile_disable, + buffer_profile, + }) + } +} + +impl std::fmt::Debug for Config { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Opt") + f.debug_struct("Config") .field("volumes", &self.volumes) .field("address", &self.address) .field("server_domains", &self.server_domains) diff --git a/rustfs/src/init.rs b/rustfs/src/init.rs index 7f6542d36..366100d75 100644 --- a/rustfs/src/init.rs +++ b/rustfs/src/init.rs @@ -163,22 +163,22 @@ pub(crate) async fn add_bucket_notification_configuration(buckets: Vec) /// If not enabled, it attempts to load any persisted KMS configuration from /// cluster storage and starts the service if found. /// # Arguments -/// * `opt` - The application configuration options +/// * `config` - The application configuration options /// /// Returns `std::io::Result<()>` indicating success or failure -#[instrument(skip(opt))] -pub(crate) async fn init_kms_system(opt: &config::Opt) -> std::io::Result<()> { +#[instrument(skip(config))] +pub(crate) async fn init_kms_system(config: &config::Config) -> std::io::Result<()> { // Initialize global KMS service manager (starts in NotConfigured state) let service_manager = rustfs_kms::init_global_kms_service_manager(); // If KMS is enabled in configuration, configure and start the service - if opt.kms_enable { + if config.kms_enable { info!("KMS is enabled via command line, configuring and starting service..."); // Create KMS configuration from command line options - let kms_config = match opt.kms_backend.as_str() { + let kms_config = match config.kms_backend.as_str() { "local" => { - let key_dir = opt + let key_dir = config .kms_key_dir .as_ref() .ok_or_else(|| Error::other("KMS key directory is required for local backend"))?; @@ -190,7 +190,7 @@ pub(crate) async fn init_kms_system(opt: &config::Opt) -> std::io::Result<()> { master_key: None, file_permissions: Some(0o600), }), - default_key_id: opt.kms_default_key_id.clone(), + default_key_id: config.kms_default_key_id.clone(), timeout: std::time::Duration::from_secs(30), retry_attempts: 3, enable_cache: true, @@ -198,11 +198,11 @@ pub(crate) async fn init_kms_system(opt: &config::Opt) -> std::io::Result<()> { } } "vault" => { - let vault_address = opt + let vault_address = config .kms_vault_address .as_ref() .ok_or_else(|| Error::other("Vault address is required for vault backend"))?; - let vault_token = opt + let vault_token = config .kms_vault_token .as_ref() .ok_or_else(|| Error::other("Vault token is required for vault backend"))?; @@ -220,14 +220,14 @@ pub(crate) async fn init_kms_system(opt: &config::Opt) -> std::io::Result<()> { key_path_prefix: "rustfs/kms/keys".to_string(), tls: None, })), - default_key_id: opt.kms_default_key_id.clone(), + default_key_id: config.kms_default_key_id.clone(), timeout: std::time::Duration::from_secs(30), retry_attempts: 3, enable_cache: true, cache_config: rustfs_kms::config::CacheConfig::default(), } } - _ => return Err(Error::other(format!("Unsupported KMS backend: {}", opt.kms_backend))), + _ => return Err(Error::other(format!("Unsupported KMS backend: {}", config.kms_backend))), }; // Configure the KMS service @@ -287,22 +287,22 @@ pub(crate) async fn init_kms_system(opt: &config::Opt) -> std::io::Result<()> { /// - Custom profile: Set via `--buffer-profile` or `RUSTFS_BUFFER_PROFILE` environment variable /// /// # Arguments -/// * `opt` - The application configuration options -pub(crate) fn init_buffer_profile_system(opt: &config::Opt) { +/// * `config` - The application configuration options +pub(crate) fn init_buffer_profile_system(config: &config::Config) { use crate::config::workload_profiles::{ RustFSBufferConfig, WorkloadProfile, init_global_buffer_config, set_buffer_profile_enabled, }; - if opt.buffer_profile_disable { + if config.buffer_profile_disable { // User explicitly disabled buffer profiling - use GeneralPurpose profile in disabled mode info!("Buffer profiling disabled via --buffer-profile-disable, using GeneralPurpose profile"); set_buffer_profile_enabled(false); } else { // Enabled by default: use configured workload profile - info!("Buffer profiling enabled with profile: {}", opt.buffer_profile); + info!("Buffer profiling enabled with profile: {}", config.buffer_profile); // Parse the workload profile from configuration string - let profile = WorkloadProfile::from_name(&opt.buffer_profile); + let profile = WorkloadProfile::from_name(&config.buffer_profile); // Log the selected profile for operational visibility info!("Active buffer profile: {:?}", profile); diff --git a/rustfs/src/main.rs b/rustfs/src/main.rs index ca4ea198b..4702f9ae3 100644 --- a/rustfs/src/main.rs +++ b/rustfs/src/main.rs @@ -38,7 +38,6 @@ use crate::server::{ SHUTDOWN_TIMEOUT, ServiceState, ServiceStateManager, ShutdownSignal, init_cert, init_event_notifier, shutdown_event_notifier, start_audit_system, start_http_server, stop_audit_system, wait_for_shutdown, }; -use clap::Parser; use license::init_license; use rustfs_common::{GlobalReadiness, SystemStage, set_global_addr}; use rustfs_credentials::init_global_action_credentials; @@ -100,13 +99,13 @@ fn main() { } async fn async_main() -> Result<()> { // Parse the obtained parameters - let opt = config::Opt::parse(); + let config = config::Config::parse()?; // Initialize the configuration - init_license(opt.license.clone()); + init_license(config.license.clone()); // Initialize Observability - let guard = match init_obs(Some(opt.clone().obs_endpoint)).await { + let guard = match init_obs(Some(config.clone().obs_endpoint)).await { Ok(g) => g, Err(e) => { println!("Failed to initialize observability: {e}"); @@ -135,7 +134,7 @@ async fn async_main() -> Result<()> { rustfs_trusted_proxies::init(); // Initialize TLS if a certificate path is provided - if let Some(tls_path) = &opt.tls_path { + if let Some(tls_path) = &config.tls_path { match init_cert(tls_path).await { Ok(_) => { info!(target: "rustfs::main", "TLS initialized successfully with certs from {}", tls_path); @@ -148,7 +147,7 @@ async fn async_main() -> Result<()> { } // Run parameters - match run(opt).await { + match run(config).await { Ok(_) => Ok(()), Err(e) => { error!("Server encountered an error and is shutting down: {}", e); @@ -157,17 +156,17 @@ async fn async_main() -> Result<()> { } } -#[instrument(skip(opt))] -async fn run(opt: config::Opt) -> Result<()> { - debug!("opt: {:?}", &opt); +#[instrument(skip(config))] +async fn run(config: config::Config) -> Result<()> { + debug!("config: {:?}", &config); // 1. Initialize global readiness tracker let readiness = Arc::new(GlobalReadiness::new()); - if let Some(region) = &opt.region { + if let Some(region) = &config.region { rustfs_ecstore::global::set_global_region(region.clone()); } - let server_addr = parse_and_resolve_address(opt.address.as_str()).map_err(Error::other)?; + let server_addr = parse_and_resolve_address(config.address.as_str()).map_err(Error::other)?; let server_port = server_addr.port(); let server_address = server_addr.to_string(); @@ -182,7 +181,7 @@ async fn run(opt: config::Opt) -> Result<()> { ); // Set up AK and SK - match init_global_action_credentials(Some(opt.access_key.clone()), Some(opt.secret_key.clone())) { + match init_global_action_credentials(Some(config.access_key.clone()), Some(config.secret_key.clone())) { Ok(_) => { info!(target: "rustfs::main::run", "Global action credentials initialized successfully."); } @@ -195,10 +194,10 @@ async fn run(opt: config::Opt) -> Result<()> { set_global_rustfs_port(server_port); - set_global_addr(&opt.address).await; + set_global_addr(&config.address).await; // For RPC - let (endpoint_pools, setup_type) = EndpointServerPools::from_volumes(server_address.clone().as_str(), opt.volumes.clone()) + let (endpoint_pools, setup_type) = EndpointServerPools::from_volumes(server_address.clone().as_str(), config.volumes.clone()) .await .map_err(Error::other)?; @@ -248,16 +247,16 @@ async fn run(opt: config::Opt) -> Result<()> { state_manager.update(ServiceState::Starting); let s3_shutdown_tx = { - let mut s3_opt = opt.clone(); - s3_opt.console_enable = false; - let s3_shutdown_tx = start_http_server(&s3_opt, state_manager.clone(), readiness.clone()).await?; + let mut s3_config = config.clone(); + s3_config.console_enable = false; + let s3_shutdown_tx = start_http_server(&s3_config, state_manager.clone(), readiness.clone()).await?; Some(s3_shutdown_tx) }; - let console_shutdown_tx = if opt.console_enable && !opt.console_address.is_empty() { - let mut console_opt = opt.clone(); - console_opt.address = console_opt.console_address.clone(); - let console_shutdown_tx = start_http_server(&console_opt, state_manager.clone(), readiness.clone()).await?; + let console_shutdown_tx = if config.console_enable && !config.console_address.is_empty() { + let mut console_config = config.clone(); + console_config.address = console_config.console_address.clone(); + let console_shutdown_tx = start_http_server(&console_config, state_manager.clone(), readiness.clone()).await?; Some(console_shutdown_tx) } else { None @@ -290,7 +289,7 @@ async fn run(opt: config::Opt) -> Result<()> { // init replication_pool init_background_replication(store.clone()).await; // Initialize KMS system if enabled - init_kms_system(&opt).await?; + init_kms_system(&config).await?; // Initialize FTP system if enabled #[cfg(feature = "ftps")] @@ -333,7 +332,7 @@ async fn run(opt: config::Opt) -> Result<()> { let ftps_shutdown_tx: Option> = None; // Initialize buffer profiling system - init_buffer_profile_system(&opt); + init_buffer_profile_system(&config); // Initialize event notifier init_event_notifier().await; diff --git a/rustfs/src/server/http.rs b/rustfs/src/server/http.rs index 8551e95a3..f7ea27b52 100644 --- a/rustfs/src/server/http.rs +++ b/rustfs/src/server/http.rs @@ -60,11 +60,11 @@ use tracing::{Span, debug, error, info, instrument, warn}; use tracing_opentelemetry::OpenTelemetrySpanExt; pub async fn start_http_server( - opt: &config::Opt, + config: &config::Config, worker_state_manager: ServiceStateManager, readiness: Arc, ) -> Result> { - let server_addr = parse_and_resolve_address(opt.address.as_str()).map_err(Error::other)?; + let server_addr = parse_and_resolve_address(config.address.as_str()).map_err(Error::other)?; let server_port = server_addr.port(); // The listening address and port are obtained from the parameters @@ -150,7 +150,7 @@ pub async fn start_http_server( TcpListener::from_std(socket.into())? }; - let tls_acceptor = setup_tls_acceptor(opt.tls_path.as_deref().unwrap_or_default()).await?; + let tls_acceptor = setup_tls_acceptor(config.tls_path.as_deref().unwrap_or_default()).await?; let tls_enabled = tls_acceptor.is_some(); let protocol = if tls_enabled { "https" } else { "http" }; // Obtain the listener address @@ -173,7 +173,7 @@ pub async fn start_http_server( let api_endpoints = format!("{protocol}://{local_ip_str}:{server_port}"); let localhost_endpoint = format!("{protocol}://127.0.0.1:{server_port}"); let now_time = jiff::Zoned::now().strftime("%Y-%m-%d %H:%M:%S").to_string(); - if opt.console_enable { + if config.console_enable { admin::console::init_console_cfg(local_ip, server_port); info!( @@ -193,8 +193,8 @@ pub async fn start_http_server( info!(target: "rustfs::main::startup","RustFS API: {api_endpoints} {localhost_endpoint}"); println!("RustFS Http API: {api_endpoints} {localhost_endpoint}"); println!("RustFS Start Time: {now_time}"); - if rustfs_credentials::DEFAULT_ACCESS_KEY.eq(&opt.access_key) - && rustfs_credentials::DEFAULT_SECRET_KEY.eq(&opt.secret_key) + if rustfs_credentials::DEFAULT_ACCESS_KEY.eq(&config.access_key) + && rustfs_credentials::DEFAULT_SECRET_KEY.eq(&config.secret_key) { warn!( "Detected default credentials '{}:{}', we recommend that you change these values with 'RUSTFS_ACCESS_KEY' and 'RUSTFS_SECRET_KEY' environment variables", @@ -212,20 +212,20 @@ pub async fn start_http_server( let store = storage::ecfs::FS::new(); let mut b = S3ServiceBuilder::new(store.clone()); - let access_key = opt.access_key.clone(); - let secret_key = opt.secret_key.clone(); + let access_key = config.access_key.clone(); + let secret_key = config.secret_key.clone(); b.set_auth(IAMAuth::new(access_key, secret_key)); b.set_access(store.clone()); - b.set_route(admin::make_admin_route(opt.console_enable)?); + b.set_route(admin::make_admin_route(config.console_enable)?); // Virtual-hosted-style requests are only set up for S3 API when server domains are configured and console is disabled - if !opt.server_domains.is_empty() && !opt.console_enable { - MultiDomain::new(&opt.server_domains).map_err(Error::other)?; // validate domains + if !config.server_domains.is_empty() && !config.console_enable { + MultiDomain::new(&config.server_domains).map_err(Error::other)?; // validate domains // add the default port number to the given server domains let mut domain_sets = std::collections::HashSet::new(); - for domain in &opt.server_domains { + for domain in &config.server_domains { domain_sets.insert(domain.to_string()); if let Some((host, _)) = domain.split_once(':') { domain_sets.insert(format!("{host}:{server_port}")); @@ -256,7 +256,7 @@ pub async fn start_http_server( debug!("HTTP response compression is disabled"); } - let is_console = opt.console_enable; + let is_console = config.console_enable; tokio::spawn(async move { // Note: CORS layer is removed from global middleware stack // - S3 API CORS is handled by bucket-level CORS configuration in apply_cors_headers()