feat(config): allow specifying keys via files (key files) (#1814)

Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: heihutu <30542132+heihutu@users.noreply.github.com>
This commit is contained in:
Jasmine Lowen 🦁
2026-02-15 09:28:52 +01:00
committed by GitHub
parent da15d622a0
commit 21ef6d505e
5 changed files with 286 additions and 66 deletions

View File

@@ -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);
});
});
});
}
}

View File

@@ -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<String>,
/// 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<String>,
/// Access key stored in a file used for authentication.
#[arg(long, env = "RUSTFS_ACCESS_KEY_FILE", group = "access-key")]
pub access_key_file: Option<PathBuf>,
/// 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<String>,
/// Secret key stored in a file used for authentication.
#[arg(long, env = "RUSTFS_SECRET_KEY_FILE", group = "secret-key")]
pub secret_key_file: Option<PathBuf>,
/// 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<String>,
/// 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<String>,
/// 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<String>,
pub license: Option<String>,
pub region: Option<String>,
/// 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<String>,
/// Vault address for vault backend
pub kms_vault_address: Option<String>,
/// Vault token for vault backend
pub kms_vault_token: Option<String>,
/// Default KMS key ID for encryption
pub kms_default_key_id: Option<String>,
/// 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<Self> {
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)

View File

@@ -163,22 +163,22 @@ pub(crate) async fn add_bucket_notification_configuration(buckets: Vec<String>)
/// 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);

View File

@@ -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<tokio::sync::broadcast::Sender<()>> = None;
// Initialize buffer profiling system
init_buffer_profile_system(&opt);
init_buffer_profile_system(&config);
// Initialize event notifier
init_event_notifier().await;

View File

@@ -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<GlobalReadiness>,
) -> Result<tokio::sync::broadcast::Sender<()>> {
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()