diff --git a/.docker/compose/docker-compose.cluster.local-build.yml b/.docker/compose/docker-compose.cluster.local-build.yml index 173f9ba97..a41ec96d2 100644 --- a/.docker/compose/docker-compose.cluster.local-build.yml +++ b/.docker/compose/docker-compose.cluster.local-build.yml @@ -23,8 +23,8 @@ services: - RUSTFS_VOLUMES=http://node{1...4}:9000/data/rustfs{0...3} - RUSTFS_ADDRESS=:9000 - RUSTFS_CONSOLE_ENABLE=true - - RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfsadmin} - - RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfsadmin} + - RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfs-cluster-admin} + - RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfs-cluster-secret} - RUSTFS_OBS_ENDPOINT=${RUSTFS_OBS_ENDPOINT:-http://host.docker.internal:4318} - RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL:-info} - RUSTFS_UNSAFE_BYPASS_DISK_CHECK=${RUSTFS_UNSAFE_BYPASS_DISK_CHECK:-true} @@ -50,8 +50,8 @@ services: - RUSTFS_VOLUMES=http://node{1...4}:9000/data/rustfs{0...3} - RUSTFS_ADDRESS=:9000 - RUSTFS_CONSOLE_ENABLE=true - - RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfsadmin} - - RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfsadmin} + - RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfs-cluster-admin} + - RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfs-cluster-secret} - RUSTFS_OBS_ENDPOINT=${RUSTFS_OBS_ENDPOINT:-http://host.docker.internal:4318} - RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL:-info} - RUSTFS_UNSAFE_BYPASS_DISK_CHECK=${RUSTFS_UNSAFE_BYPASS_DISK_CHECK:-true} @@ -77,8 +77,8 @@ services: - RUSTFS_VOLUMES=http://node{1...4}:9000/data/rustfs{0...3} - RUSTFS_ADDRESS=:9000 - RUSTFS_CONSOLE_ENABLE=true - - RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfsadmin} - - RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfsadmin} + - RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfs-cluster-admin} + - RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfs-cluster-secret} - RUSTFS_OBS_ENDPOINT=${RUSTFS_OBS_ENDPOINT:-http://host.docker.internal:4318} - RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL:-info} - RUSTFS_UNSAFE_BYPASS_DISK_CHECK=${RUSTFS_UNSAFE_BYPASS_DISK_CHECK:-true} @@ -104,8 +104,8 @@ services: - RUSTFS_VOLUMES=http://node{1...4}:9000/data/rustfs{0...3} - RUSTFS_ADDRESS=:9000 - RUSTFS_CONSOLE_ENABLE=true - - RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfsadmin} - - RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfsadmin} + - RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfs-cluster-admin} + - RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfs-cluster-secret} - RUSTFS_OBS_ENDPOINT=${RUSTFS_OBS_ENDPOINT:-http://host.docker.internal:4318} - RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL:-info} - RUSTFS_UNSAFE_BYPASS_DISK_CHECK=${RUSTFS_UNSAFE_BYPASS_DISK_CHECK:-true} diff --git a/.docker/compose/docker-compose.cluster.yaml b/.docker/compose/docker-compose.cluster.yaml index 0613dee28..58f381091 100644 --- a/.docker/compose/docker-compose.cluster.yaml +++ b/.docker/compose/docker-compose.cluster.yaml @@ -21,8 +21,8 @@ services: - RUSTFS_VOLUMES=http://node{0...3}:9000/data/rustfs{0...3} - RUSTFS_ADDRESS=0.0.0.0:9000 - RUSTFS_CONSOLE_ENABLE=true - - RUSTFS_ACCESS_KEY=rustfsadmin - - RUSTFS_SECRET_KEY=rustfsadmin + - RUSTFS_ACCESS_KEY=rustfs-cluster-admin + - RUSTFS_SECRET_KEY=rustfs-cluster-secret platform: linux/amd64 ports: - "9000:9000" # Map port 9001 of the host to port 9000 of the container @@ -38,8 +38,8 @@ services: - RUSTFS_VOLUMES=http://node{0...3}:9000/data/rustfs{0...3} - RUSTFS_ADDRESS=0.0.0.0:9000 - RUSTFS_CONSOLE_ENABLE=true - - RUSTFS_ACCESS_KEY=rustfsadmin - - RUSTFS_SECRET_KEY=rustfsadmin + - RUSTFS_ACCESS_KEY=rustfs-cluster-admin + - RUSTFS_SECRET_KEY=rustfs-cluster-secret platform: linux/amd64 ports: - "9001:9000" # Map port 9002 of the host to port 9000 of the container @@ -55,8 +55,8 @@ services: - RUSTFS_VOLUMES=http://node{0...3}:9000/data/rustfs{0...3} - RUSTFS_ADDRESS=0.0.0.0:9000 - RUSTFS_CONSOLE_ENABLE=true - - RUSTFS_ACCESS_KEY=rustfsadmin - - RUSTFS_SECRET_KEY=rustfsadmin + - RUSTFS_ACCESS_KEY=rustfs-cluster-admin + - RUSTFS_SECRET_KEY=rustfs-cluster-secret platform: linux/amd64 ports: - "9002:9000" # Map port 9003 of the host to port 9000 of the container @@ -72,8 +72,8 @@ services: - RUSTFS_VOLUMES=http://node{0...3}:9000/data/rustfs{0...3} - RUSTFS_ADDRESS=0.0.0.0:9000 - RUSTFS_CONSOLE_ENABLE=true - - RUSTFS_ACCESS_KEY=rustfsadmin - - RUSTFS_SECRET_KEY=rustfsadmin + - RUSTFS_ACCESS_KEY=rustfs-cluster-admin + - RUSTFS_SECRET_KEY=rustfs-cluster-secret platform: linux/amd64 ports: - "9003:9000" # Map port 9004 of the host to port 9000 of the container diff --git a/.github/workflows/e2e-s3tests.yml b/.github/workflows/e2e-s3tests.yml index a2a8829ae..06b696abc 100644 --- a/.github/workflows/e2e-s3tests.yml +++ b/.github/workflows/e2e-s3tests.yml @@ -40,8 +40,8 @@ on: env: # main user - S3_ACCESS_KEY: rustfsadmin - S3_SECRET_KEY: rustfsadmin + S3_ACCESS_KEY: rustfs-ci-admin + S3_SECRET_KEY: rustfs-ci-secret # alt user (must be different from main for many s3-tests) S3_ALT_ACCESS_KEY: rustfsalt S3_ALT_SECRET_KEY: rustfsalt diff --git a/Dockerfile.glibc b/Dockerfile.glibc index 4ca9e40e7..20005d983 100644 --- a/Dockerfile.glibc +++ b/Dockerfile.glibc @@ -105,8 +105,6 @@ RUN groupadd -g 10001 rustfs && \ ENV RUSTFS_ADDRESS=":9000" \ RUSTFS_CONSOLE_ADDRESS=":9001" \ - RUSTFS_ACCESS_KEY="rustfsadmin" \ - RUSTFS_SECRET_KEY="rustfsadmin" \ RUSTFS_CONSOLE_ENABLE="true" \ RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS="*" \ RUSTFS_VOLUMES="/data" \ diff --git a/crates/config/src/constants/app.rs b/crates/config/src/constants/app.rs index b61fe452e..305d4052c 100644 --- a/crates/config/src/constants/app.rs +++ b/crates/config/src/constants/app.rs @@ -155,6 +155,12 @@ pub const ENV_RUSTFS_SECRET_KEY: &str = "RUSTFS_SECRET_KEY"; /// Environment variable for server secret key file. pub const ENV_RUSTFS_SECRET_KEY_FILE: &str = "RUSTFS_SECRET_KEY_FILE"; +/// Environment variable to explicitly allow public default root credentials. +/// +/// This is intended for local development only. Production startup paths should +/// provide non-default `RUSTFS_ACCESS_KEY` and `RUSTFS_SECRET_KEY` values. +pub const ENV_RUSTFS_ALLOW_INSECURE_DEFAULT_CREDENTIALS: &str = "RUSTFS_ALLOW_INSECURE_DEFAULT_CREDENTIALS"; + /// Environment variable for server OBS endpoint. pub const ENV_RUSTFS_OBS_ENDPOINT: &str = "RUSTFS_OBS_ENDPOINT"; diff --git a/crates/trusted-proxies/src/simple.rs b/crates/trusted-proxies/src/simple.rs index 39e07fd0f..7b911acea 100644 --- a/crates/trusted-proxies/src/simple.rs +++ b/crates/trusted-proxies/src/simple.rs @@ -37,6 +37,9 @@ pub const SIMPLE_INTERNAL_ONLY_DEFAULT: bool = true; const HEADER_FORWARDED: &str = "forwarded"; const HEADER_X_FORWARDED_FOR: &str = "x-forwarded-for"; +const HEADER_X_FORWARDED_HOST: &str = "x-forwarded-host"; +const HEADER_X_FORWARDED_PROTO: &str = "x-forwarded-proto"; +const HEADER_X_FORWARDED_SCHEME: &str = "x-forwarded-scheme"; const HEADER_X_REAL_IP: &str = "x-real-ip"; static ENABLED: OnceLock = OnceLock::new(); @@ -257,9 +260,15 @@ fn resolve_client_info(peer_addr: Option, headers: &HeaderMap) -> Cl } match forwarded_client_ip(headers) { - Some(real_ip) if is_usable_ip(real_ip) && real_ip != peer_addr.ip() => { - ClientInfo::from_trusted_proxy(real_ip, None, None, peer_addr.ip(), 1, ValidationMode::Lenient, Vec::new()) - } + Some(real_ip) if is_usable_ip(real_ip) && real_ip != peer_addr.ip() => ClientInfo::from_trusted_proxy( + real_ip, + forwarded_host(headers), + forwarded_proto(headers), + peer_addr.ip(), + 1, + ValidationMode::Lenient, + Vec::new(), + ), _ => ClientInfo::direct(peer_addr), } } @@ -270,6 +279,72 @@ fn forwarded_client_ip(headers: &HeaderMap) -> Option { .or_else(|| parse_forwarded_header(headers)) } +fn forwarded_host(headers: &HeaderMap) -> Option { + parse_single_value_header(headers, HEADER_X_FORWARDED_HOST) + .and_then(sanitize_forwarded_host) + .or_else(|| parse_forwarded_header_value(headers, "host").and_then(sanitize_forwarded_host)) +} + +fn forwarded_proto(headers: &HeaderMap) -> Option { + parse_single_value_header(headers, HEADER_X_FORWARDED_PROTO) + .and_then(sanitize_forwarded_proto) + .or_else(|| parse_single_value_header(headers, HEADER_X_FORWARDED_SCHEME).and_then(sanitize_forwarded_proto)) + .or_else(|| parse_forwarded_header_value(headers, "proto").and_then(sanitize_forwarded_proto)) +} + +fn parse_single_value_header(headers: &HeaderMap, name: &str) -> Option { + headers + .get(name)? + .to_str() + .ok()? + .split(',') + .next() + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(ToOwned::to_owned) +} + +fn parse_forwarded_header_value(headers: &HeaderMap, name: &str) -> Option { + let value = headers.get(HEADER_FORWARDED)?.to_str().ok()?; + let first = value.split(',').next()?.trim(); + + for part in first.split(';') { + let Some((key, value)) = part.trim().split_once('=') else { + continue; + }; + if key.trim().eq_ignore_ascii_case(name) { + let value = value.trim().trim_matches('"'); + if !value.is_empty() { + return Some(value.to_string()); + } + } + } + + None +} + +fn sanitize_forwarded_host(value: String) -> Option { + sanitize_forwarded_value(&value).filter(|value| !value.chars().any(char::is_whitespace)) +} + +fn sanitize_forwarded_proto(value: String) -> Option { + let value = sanitize_forwarded_value(&value)?; + if value.eq_ignore_ascii_case("http") || value.eq_ignore_ascii_case("https") { + return Some(value.to_ascii_lowercase()); + } + + None +} + +fn sanitize_forwarded_value(value: &str) -> Option { + let value = value.trim().trim_matches('"'); + if value.is_empty() || value.len() > 256 || value.chars().any(char::is_control) { + return None; + } + + Some(value.to_string()) +} + fn parse_x_forwarded_for(headers: &HeaderMap) -> Option { let value = headers.get(HEADER_X_FORWARDED_FOR)?.to_str().ok()?; let first = value.split(',').next()?.trim(); @@ -345,8 +420,10 @@ fn parse_implementation(value: Option<&str>) -> TrustedProxyImplementation { #[cfg(test)] mod tests { use super::{ - ENV_TRUSTED_PROXY_IMPLEMENTATION, HEADER_FORWARDED, HEADER_X_FORWARDED_FOR, HEADER_X_REAL_IP, TrustedProxyImplementation, - TrustedProxyLayer, forwarded_client_ip, is_internal_ip, parse_implementation, parse_ip_token, resolve_client_info, + ENV_TRUSTED_PROXY_IMPLEMENTATION, HEADER_FORWARDED, HEADER_X_FORWARDED_FOR, HEADER_X_FORWARDED_HOST, + HEADER_X_FORWARDED_PROTO, HEADER_X_FORWARDED_SCHEME, HEADER_X_REAL_IP, TrustedProxyImplementation, TrustedProxyLayer, + forwarded_client_ip, forwarded_host, forwarded_proto, is_internal_ip, parse_implementation, parse_ip_token, + resolve_client_info, }; use crate::ClientInfo; use axum::http::{HeaderMap, HeaderValue}; @@ -403,6 +480,55 @@ mod tests { assert_eq!(client_info.proxy_ip, Some(IpAddr::from([10, 0, 0, 5]))); } + #[test] + fn test_internal_peer_preserves_forwarded_proto_and_host() { + let mut headers = HeaderMap::new(); + headers.insert(HEADER_X_FORWARDED_FOR, HeaderValue::from_static("203.0.113.10")); + headers.insert(HEADER_X_FORWARDED_HOST, HeaderValue::from_static("s3.example.test")); + headers.insert(HEADER_X_FORWARDED_PROTO, HeaderValue::from_static("https")); + + let client_info = resolve_client_info(Some(SocketAddr::from(([10, 0, 0, 5], 9000))), &headers); + + assert_eq!(client_info.forwarded_host.as_deref(), Some("s3.example.test")); + assert_eq!(client_info.forwarded_proto.as_deref(), Some("https")); + } + + #[test] + fn test_internal_peer_preserves_forwarded_scheme_fallback() { + let mut headers = HeaderMap::new(); + headers.insert(HEADER_X_FORWARDED_FOR, HeaderValue::from_static("203.0.113.10")); + headers.insert(HEADER_X_FORWARDED_SCHEME, HeaderValue::from_static("https")); + + let client_info = resolve_client_info(Some(SocketAddr::from(([10, 0, 0, 5], 9000))), &headers); + + assert_eq!(client_info.forwarded_proto.as_deref(), Some("https")); + } + + #[test] + fn test_forwarded_header_preserves_proto_and_host() { + let mut headers = HeaderMap::new(); + headers.insert( + HEADER_FORWARDED, + HeaderValue::from_static("for=203.0.113.10;proto=https;host=s3.example.test"), + ); + + let client_info = resolve_client_info(Some(SocketAddr::from(([10, 0, 0, 5], 9000))), &headers); + + assert_eq!(client_info.real_ip, IpAddr::from([203, 0, 113, 10])); + assert_eq!(client_info.forwarded_host.as_deref(), Some("s3.example.test")); + assert_eq!(client_info.forwarded_proto.as_deref(), Some("https")); + } + + #[test] + fn test_invalid_forwarded_host_and_proto_are_ignored() { + let mut headers = HeaderMap::new(); + headers.insert(HEADER_X_FORWARDED_HOST, HeaderValue::from_static("bad host")); + headers.insert(HEADER_X_FORWARDED_PROTO, HeaderValue::from_static("ftp")); + + assert_eq!(forwarded_host(&headers), None); + assert_eq!(forwarded_proto(&headers), None); + } + #[test] fn test_public_peer_keeps_direct_ip() { let mut headers = HeaderMap::new(); diff --git a/docker-compose.yml b/docker-compose.yml index c1c2b26ee..13824f967 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -33,10 +33,8 @@ services: - RUSTFS_CONSOLE_ADDRESS=0.0.0.0:9001 - RUSTFS_CONSOLE_ENABLE=true - RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS=* - # SECURITY: these defaults are public and well-known. Override before - # exposing the listener beyond localhost. - - RUSTFS_ACCESS_KEY=rustfsadmin - - RUSTFS_SECRET_KEY=rustfsadmin + - RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:?Set RUSTFS_ACCESS_KEY to a non-default value} + - RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:?Set RUSTFS_SECRET_KEY to a non-default value} - RUSTFS_OBS_LOGGER_LEVEL=info - RUSTFS_TLS_PATH=/opt/tls - RUSTFS_OBS_ENDPOINT=http://otel-collector:4318 diff --git a/entrypoint.sh b/entrypoint.sh index 94e7b8eed..252abff61 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -113,8 +113,8 @@ process_data_volumes process_log_directory # 4) Default credentials warning -if [ "${RUSTFS_ACCESS_KEY}" = "rustfsadmin" ] || [ "${RUSTFS_SECRET_KEY}" = "rustfsadmin" ]; then - echo "!!!WARNING: Using default RUSTFS_ACCESS_KEY or RUSTFS_SECRET_KEY. Override them in production!" +if [ "${RUSTFS_ACCESS_KEY:-}" = "rustfsadmin" ] || [ "${RUSTFS_SECRET_KEY:-}" = "rustfsadmin" ]; then + echo "!!!WARNING: Default credentials are only allowed on loopback or with explicit insecure local-dev opt-in." fi # 5) Append DATA_VOLUMES only if no data paths in arguments diff --git a/rustfs/src/app/bucket_usecase.rs b/rustfs/src/app/bucket_usecase.rs index 33a35adaf..adb6e7d2c 100644 --- a/rustfs/src/app/bucket_usecase.rs +++ b/rustfs/src/app/bucket_usecase.rs @@ -18,7 +18,7 @@ use crate::admin::handlers::site_replication::{ site_replication_bucket_meta_hook, site_replication_delete_bucket_hook, site_replication_make_bucket_hook, }; use crate::app::context::{AppContext, default_notify_interface, get_global_app_context}; -use crate::auth::get_condition_values; +use crate::auth::get_condition_values_with_client_info; use crate::error::ApiError; use crate::server::RemoteAddr; use crate::storage::access::{ReqInfo, authorize_request, req_info_ref}; @@ -69,6 +69,7 @@ use rustfs_targets::{ EventName, arn::{ARN, TargetIDError}, }; +use rustfs_trusted_proxies::ClientInfo; use rustfs_utils::http::{SUFFIX_FORCE_DELETE, get_header}; use rustfs_utils::obj::extract_user_defined_metadata; use rustfs_utils::string::parse_bool; @@ -1308,7 +1309,15 @@ impl DefaultBucketUsecase { .map_err(ApiError::from)?; let remote_addr = req.extensions.get::>().and_then(|opt| opt.map(|a| a.0)); - let conditions = get_condition_values(&req.headers, &rustfs_credentials::Credentials::default(), None, None, remote_addr); + let client_info = req.extensions.get::(); + let conditions = get_condition_values_with_client_info( + &req.headers, + &rustfs_credentials::Credentials::default(), + None, + None, + remote_addr, + client_info, + ); let read_allowed = PolicySys::is_allowed(&BucketPolicyArgs { bucket: &bucket, diff --git a/rustfs/src/auth.rs b/rustfs/src/auth.rs index eed5deee8..00b8c37d2 100644 --- a/rustfs/src/auth.rs +++ b/rustfs/src/auth.rs @@ -20,9 +20,8 @@ use rustfs_iam::sys::{ SESSION_POLICY_NAME, get_claims_from_token_with_secret, get_claims_from_token_with_secret_allow_missing_exp, }; use rustfs_policy::policy::{ClaimLookup, get_claim_case_insensitive}; -use rustfs_utils::http::{ - AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER, AMZ_OBJECT_LOCK_MODE_LOWER, AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER, ip::get_source_ip_raw, -}; +use rustfs_trusted_proxies::ClientInfo; +use rustfs_utils::http::{AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER, AMZ_OBJECT_LOCK_MODE_LOWER, AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER}; use s3s::S3Error; use s3s::S3ErrorCode; use s3s::S3Result; @@ -32,6 +31,7 @@ use s3s::auth::SimpleAuth; use s3s::s3_error; use serde_json::Value; use std::collections::HashMap; +use std::net::SocketAddr; use subtle::ConstantTimeEq; use time::OffsetDateTime; use time::format_description::well_known::Rfc3339; @@ -436,6 +436,20 @@ pub(crate) fn extract_string_list_claim(claims: &HashMap, claim_n } } +fn policy_source_ip(remote_addr: Option, client_info: Option<&ClientInfo>) -> String { + client_info + .map(|info| info.real_ip.to_string()) + .or_else(|| remote_addr.map(|addr| addr.ip().to_string())) + .unwrap_or_default() +} + +fn policy_secure_transport(client_info: Option<&ClientInfo>) -> bool { + client_info + .and_then(|info| info.forwarded_proto.as_deref()) + .map(|proto| proto.eq_ignore_ascii_case("https")) + .unwrap_or(false) +} + /// Get condition values for policy evaluation /// /// # Arguments @@ -453,9 +467,21 @@ pub fn get_condition_values( cred: &Credentials, version_id: Option<&str>, region: Option, - remote_addr: Option, + remote_addr: Option, ) -> HashMap> { - get_condition_values_with_query(header, cred, version_id, region, remote_addr, None) + get_condition_values_with_client_info(header, cred, version_id, region, remote_addr, None) +} + +/// Get condition values for policy evaluation with verified client information. +pub fn get_condition_values_with_client_info( + header: &HeaderMap, + cred: &Credentials, + version_id: Option<&str>, + region: Option, + remote_addr: Option, + client_info: Option<&ClientInfo>, +) -> HashMap> { + get_condition_values_with_query_and_client_info(header, cred, version_id, region, remote_addr, None, client_info) } /// Get condition values for policy evaluation with optional query-string values. @@ -475,8 +501,22 @@ pub fn get_condition_values_with_query( cred: &Credentials, version_id: Option<&str>, region: Option, - remote_addr: Option, + remote_addr: Option, query: Option<&str>, +) -> HashMap> { + get_condition_values_with_query_and_client_info(header, cred, version_id, region, remote_addr, query, None) +} + +/// Get condition values for policy evaluation with optional query-string values +/// and verified client information from trusted proxy middleware. +pub fn get_condition_values_with_query_and_client_info( + header: &HeaderMap, + cred: &Credentials, + version_id: Option<&str>, + region: Option, + remote_addr: Option, + query: Option<&str>, + client_info: Option<&ClientInfo>, ) -> HashMap> { let username = if cred.is_temp() || cred.is_service_account() { cred.parent_user.clone() @@ -510,21 +550,8 @@ pub fn get_condition_values_with_query( // Determine auth type and signature version from headers and query let (auth_type, signature_version) = determine_auth_type_and_version_with_query(header, query); - // Get TLS status from header - let is_tls = header - .get("x-forwarded-proto") - .and_then(|v| v.to_str().ok()) - .map(|s| s == "https") - .or_else(|| { - header - .get("x-forwarded-scheme") - .and_then(|v| v.to_str().ok()) - .map(|s| s == "https") - }) - .unwrap_or(false); - - // Get remote address from header or use default - let remote_addr_s = remote_addr.map(|a| a.ip().to_string()).unwrap_or_default(); + let is_tls = policy_secure_transport(client_info); + let source_ip = policy_source_ip(remote_addr, client_info); let mut args = HashMap::new(); @@ -532,7 +559,7 @@ pub fn get_condition_values_with_query( args.insert("CurrentTime".to_owned(), vec![curr_time.format(&Rfc3339).unwrap_or_default()]); args.insert("EpochTime".to_owned(), vec![epoch_time.to_string()]); args.insert("SecureTransport".to_owned(), vec![is_tls.to_string()]); - args.insert("SourceIp".to_owned(), vec![get_source_ip_raw(header, &remote_addr_s)]); + args.insert("SourceIp".to_owned(), vec![source_ip]); // Add user agent and referer if let Some(user_agent) = header.get("user-agent") { @@ -888,6 +915,7 @@ mod tests { use super::*; use http::{HeaderMap, HeaderValue, Uri}; use rustfs_credentials::Credentials; + use rustfs_trusted_proxies::ValidationMode; use s3s::auth::SecretKey; use serde_json::json; use std::collections::HashMap; @@ -1600,32 +1628,32 @@ mod tests { let conditions = get_condition_values(&headers, &cred, None, None, Some(remote_addr)); assert_eq!(conditions.get("SourceIp").unwrap()[0], "192.168.0.10"); - // Case 3: X-Forwarded-For present -> XFF (takes precedence over remote_addr) + // Case 3: X-Forwarded-For is ignored without verified proxy context headers.insert("x-forwarded-for", HeaderValue::from_static("10.0.0.1")); let conditions = get_condition_values(&headers, &cred, None, None, Some(remote_addr)); - assert_eq!(conditions.get("SourceIp").unwrap()[0], "10.0.0.1"); + assert_eq!(conditions.get("SourceIp").unwrap()[0], "192.168.0.10"); - // Case 4: X-Forwarded-For with multiple IPs -> First IP + // Case 4: X-Forwarded-For with multiple IPs is ignored without verified proxy context headers.insert("x-forwarded-for", HeaderValue::from_static("10.0.0.3, 10.0.0.4")); let conditions = get_condition_values(&headers, &cred, None, None, Some(remote_addr)); - assert_eq!(conditions.get("SourceIp").unwrap()[0], "10.0.0.3"); + assert_eq!(conditions.get("SourceIp").unwrap()[0], "192.168.0.10"); - // Case 5: X-Real-IP present (XFF removed) -> X-Real-IP + // Case 5: X-Real-IP is ignored without verified proxy context headers.remove("x-forwarded-for"); headers.insert("x-real-ip", HeaderValue::from_static("10.0.0.2")); let conditions = get_condition_values(&headers, &cred, None, None, Some(remote_addr)); - assert_eq!(conditions.get("SourceIp").unwrap()[0], "10.0.0.2"); + assert_eq!(conditions.get("SourceIp").unwrap()[0], "192.168.0.10"); - // Case 6: Forwarded header present (X-Real-IP removed) -> Forwarded + // Case 6: Forwarded is ignored without verified proxy context headers.remove("x-real-ip"); headers.insert("forwarded", HeaderValue::from_static("for=10.0.0.5;proto=http")); let conditions = get_condition_values(&headers, &cred, None, None, Some(remote_addr)); - assert_eq!(conditions.get("SourceIp").unwrap()[0], "10.0.0.5"); + assert_eq!(conditions.get("SourceIp").unwrap()[0], "192.168.0.10"); - // Case 7: Forwarded header with quotes and multiple values + // Case 7: Forwarded with quotes and multiple values is ignored without verified proxy context headers.insert("forwarded", HeaderValue::from_static("for=\"10.0.0.6\", for=10.0.0.7")); let conditions = get_condition_values(&headers, &cred, None, None, Some(remote_addr)); - assert_eq!(conditions.get("SourceIp").unwrap()[0], "10.0.0.6"); + assert_eq!(conditions.get("SourceIp").unwrap()[0], "192.168.0.10"); // Case 8: IPv6 Remote Addr let remote_addr_v6: std::net::SocketAddr = "[2001:db8::1]:8080".parse().unwrap(); @@ -1634,6 +1662,41 @@ mod tests { assert_eq!(conditions.get("SourceIp").unwrap()[0], "2001:db8::1"); } + #[test] + fn test_get_condition_values_uses_verified_client_info() { + let mut headers = HeaderMap::new(); + headers.insert("x-forwarded-for", HeaderValue::from_static("10.0.0.1")); + headers.insert("x-forwarded-proto", HeaderValue::from_static("https")); + let cred = Credentials::default(); + let remote_addr: std::net::SocketAddr = "192.168.0.10:12345".parse().unwrap(); + let client_info = ClientInfo::from_trusted_proxy( + "10.0.0.1".parse().unwrap(), + None, + Some("https".to_string()), + "192.168.0.10".parse().unwrap(), + 1, + ValidationMode::Lenient, + Vec::new(), + ); + + let conditions = + get_condition_values_with_client_info(&headers, &cred, None, None, Some(remote_addr), Some(&client_info)); + + assert_eq!(conditions.get("SourceIp").unwrap()[0], "10.0.0.1"); + assert_eq!(conditions.get("SecureTransport").unwrap()[0], "true"); + } + + #[test] + fn test_get_condition_values_ignores_unverified_secure_transport_header() { + let mut headers = HeaderMap::new(); + headers.insert("x-forwarded-proto", HeaderValue::from_static("https")); + let cred = Credentials::default(); + + let conditions = get_condition_values(&headers, &cred, None, None, None); + + assert_eq!(conditions.get("SecureTransport").unwrap()[0], "false"); + } + // ========== KEYSTONE AUTHENTICATION TESTS ========== #[tokio::test] diff --git a/rustfs/src/config/config_struct.rs b/rustfs/src/config/config_struct.rs index 19b7b2704..6c6f238a7 100644 --- a/rustfs/src/config/config_struct.rs +++ b/rustfs/src/config/config_struct.rs @@ -24,6 +24,7 @@ use rustfs_config::{ }; use rustfs_credentials::{DEFAULT_ACCESS_KEY, DEFAULT_SECRET_KEY, Masked}; use std::collections::HashSet; +use std::net::SocketAddr; use std::sync::{Mutex, OnceLock}; pub(crate) const LEGACY_ENV_RUSTFS_ROOT_USER: &str = "RUSTFS_ROOT_USER"; @@ -174,6 +175,14 @@ impl Config { } } + pub fn is_using_default_credentials(&self) -> bool { + DEFAULT_ACCESS_KEY.eq(&self.access_key) && DEFAULT_SECRET_KEY.eq(&self.secret_key) + } + + pub fn default_credentials_allowed_for_addr(&self, server_addr: SocketAddr, allow_insecure_defaults: bool) -> bool { + !self.is_using_default_credentials() || server_addr.ip().is_loopback() || allow_insecure_defaults + } + /// Create Config from Opt pub(super) fn from_opt(opt: Opt) -> std::io::Result { let Opt { diff --git a/rustfs/src/config/config_test.rs b/rustfs/src/config/config_test.rs index 4c1da3bd1..4e13d5d90 100644 --- a/rustfs/src/config/config_test.rs +++ b/rustfs/src/config/config_test.rs @@ -113,6 +113,24 @@ mod tests { assert_eq!(config.buffer_profile, "GeneralPurpose"); } + #[test] + fn default_credentials_allowed_only_for_loopback_or_explicit_opt_in() { + let config = Config::new("0.0.0.0:9000", vec!["/tmp/rustfs-vol1".to_string()]); + + assert!(!config.default_credentials_allowed_for_addr("0.0.0.0:9000".parse().unwrap(), false)); + assert!(config.default_credentials_allowed_for_addr("127.0.0.1:9000".parse().unwrap(), false)); + assert!(config.default_credentials_allowed_for_addr("0.0.0.0:9000".parse().unwrap(), true)); + } + + #[test] + fn custom_credentials_allowed_on_non_loopback() { + let mut config = Config::new("0.0.0.0:9000", vec!["/tmp/rustfs-vol1".to_string()]); + config.access_key = "custom-access-key".to_string(); + config.secret_key = "custom-secret-key".to_string(); + + assert!(config.default_credentials_allowed_for_addr("0.0.0.0:9000".parse().unwrap(), false)); + } + #[test] #[serial] fn test_custom_console_configuration() { diff --git a/rustfs/src/embedded.rs b/rustfs/src/embedded.rs index 247abb296..e845f29cb 100644 --- a/rustfs/src/embedded.rs +++ b/rustfs/src/embedded.rs @@ -72,7 +72,7 @@ use rustfs_ecstore::{ }; use rustfs_iam::init_iam_sys; use rustfs_obs::{init_obs, set_global_guard}; -use rustfs_utils::net::parse_and_resolve_address; +use rustfs_utils::{get_env_bool, net::parse_and_resolve_address}; use rustls::crypto::aws_lc_rs::default_provider; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::path::{Path, PathBuf}; @@ -303,19 +303,8 @@ impl RustFSServerBuilder { // Trusted proxies. rustfs_trusted_proxies::init(); - // Credentials. - init_global_action_credentials(Some(config.access_key.clone()), Some(config.secret_key.clone())) - .map_err(|e| ServerError::Init(format!("credentials: {e:?}")))?; - - // Region. - if let Some(region_str) = &config.region { - let region = region_str - .parse() - .map_err(|e| ServerError::Init(format!("invalid region '{region_str}': {e}")))?; - rustfs_ecstore::global::set_global_region(region); - } - - // Resolve listen address. + // Resolve listen address before credential initialization so unsafe + // default credentials can fail before the server binds a listener. let server_addr = parse_and_resolve_address(config.address.as_str()).map_err(|e| ServerError::Init(format!("address: {e}")))?; @@ -328,6 +317,26 @@ impl RustFSServerBuilder { )); } + let allow_insecure_defaults = get_env_bool(rustfs_config::ENV_RUSTFS_ALLOW_INSECURE_DEFAULT_CREDENTIALS, false); + if !config.default_credentials_allowed_for_addr(server_addr, allow_insecure_defaults) { + return Err(ServerError::Init( + "default root credentials are not allowed on non-loopback listeners; set access_key and secret_key to non-default values, bind to loopback, or set RUSTFS_ALLOW_INSECURE_DEFAULT_CREDENTIALS=true for local development only" + .to_string(), + )); + } + + // Credentials. + init_global_action_credentials(Some(config.access_key.clone()), Some(config.secret_key.clone())) + .map_err(|e| ServerError::Init(format!("credentials: {e:?}")))?; + + // Region. + if let Some(region_str) = &config.region { + let region = region_str + .parse() + .map_err(|e| ServerError::Init(format!("invalid region '{region_str}': {e}")))?; + rustfs_ecstore::global::set_global_region(region); + } + let server_port = server_addr.port(); set_global_rustfs_port(server_port); diff --git a/rustfs/src/main.rs b/rustfs/src/main.rs index 86a39b676..e1f4e2f52 100644 --- a/rustfs/src/main.rs +++ b/rustfs/src/main.rs @@ -59,7 +59,7 @@ use rustfs_iam::{init_iam_sys, init_oidc_sys}; use rustfs_obs::{init_metrics_runtime, init_obs, set_global_guard}; use rustfs_scanner::init_data_scanner; use rustfs_utils::{ - ExternalEnvCompatReport, apply_external_env_compat, get_env_bool_with_aliases, net::parse_and_resolve_address, + ExternalEnvCompatReport, apply_external_env_compat, get_env_bool, get_env_bool_with_aliases, net::parse_and_resolve_address, }; use rustls::crypto::aws_lc_rs::default_provider; use std::io::{Error, Result}; @@ -138,11 +138,15 @@ fn format_external_prefix_mappings(report: &ExternalEnvCompatReport) -> String { } fn is_using_default_credentials(config: &rustfs::config::Config) -> bool { - rustfs_credentials::DEFAULT_ACCESS_KEY.eq(&config.access_key) && rustfs_credentials::DEFAULT_SECRET_KEY.eq(&config.secret_key) + config.is_using_default_credentials() } -const DEFAULT_CREDENTIALS_WARNING_MESSAGE: &str = - "Detected default root credentials; change them with the RUSTFS_ACCESS_KEY and RUSTFS_SECRET_KEY environment variables"; +const DEFAULT_CREDENTIALS_WARNING_MESSAGE: &str = "Detected default root credentials; set RUSTFS_ACCESS_KEY and RUSTFS_SECRET_KEY to non-default values, or use RUSTFS_ALLOW_INSECURE_DEFAULT_CREDENTIALS=true only for local development"; +const DEFAULT_CREDENTIALS_ERROR_MESSAGE: &str = "Default root credentials are not allowed on non-loopback listeners; set RUSTFS_ACCESS_KEY and RUSTFS_SECRET_KEY to non-default values, bind to loopback, or set RUSTFS_ALLOW_INSECURE_DEFAULT_CREDENTIALS=true for local development only"; + +fn allow_insecure_default_credentials() -> bool { + get_env_bool(rustfs_config::ENV_RUSTFS_ALLOW_INSECURE_DEFAULT_CREDENTIALS, false) +} async fn async_main() -> Result<()> { // Parse command line arguments @@ -265,6 +269,15 @@ async fn run(config: rustfs::config::Config) -> Result<()> { let server_port = server_addr.port(); let server_address = server_addr.to_string(); + if !config.default_credentials_allowed_for_addr(server_addr, allow_insecure_default_credentials()) { + error!("{DEFAULT_CREDENTIALS_ERROR_MESSAGE}"); + return Err(Error::other(DEFAULT_CREDENTIALS_ERROR_MESSAGE)); + } + + if is_using_default_credentials(&config) { + warn!("{}", DEFAULT_CREDENTIALS_WARNING_MESSAGE); + } + info!( target: "rustfs::main::run", server_address = %server_address, @@ -360,10 +373,6 @@ async fn run(config: rustfs::config::Config) -> Result<()> { None }; - if is_using_default_credentials(&config) { - warn!("{}", DEFAULT_CREDENTIALS_WARNING_MESSAGE); - } - let ctx = CancellationToken::new(); // init store @@ -832,12 +841,13 @@ mod tests { } #[test] - fn default_credentials_warning_message_does_not_expose_values() { - let message = DEFAULT_CREDENTIALS_WARNING_MESSAGE; - - assert!(message.contains(rustfs_config::ENV_RUSTFS_ACCESS_KEY)); - assert!(message.contains(rustfs_config::ENV_RUSTFS_SECRET_KEY)); - assert!(!message.contains(rustfs_credentials::DEFAULT_ACCESS_KEY)); - assert!(!message.contains(rustfs_credentials::DEFAULT_SECRET_KEY)); + fn default_credentials_messages_are_actionable_without_exposing_values() { + for message in [DEFAULT_CREDENTIALS_WARNING_MESSAGE, DEFAULT_CREDENTIALS_ERROR_MESSAGE] { + assert!(message.contains(rustfs_config::ENV_RUSTFS_ACCESS_KEY)); + assert!(message.contains(rustfs_config::ENV_RUSTFS_SECRET_KEY)); + assert!(message.contains(rustfs_config::ENV_RUSTFS_ALLOW_INSECURE_DEFAULT_CREDENTIALS)); + assert!(!message.contains(rustfs_credentials::DEFAULT_ACCESS_KEY)); + assert!(!message.contains(rustfs_credentials::DEFAULT_SECRET_KEY)); + } } } diff --git a/rustfs/src/storage/access.rs b/rustfs/src/storage/access.rs index 473d3b87c..e0c10e068 100644 --- a/rustfs/src/storage/access.rs +++ b/rustfs/src/storage/access.rs @@ -13,7 +13,7 @@ // limitations under the License. use super::ecfs::FS; -use crate::auth::{check_key_valid, get_condition_values_with_query, get_session_token}; +use crate::auth::{check_key_valid, get_condition_values_with_query_and_client_info, get_session_token}; use crate::error::ApiError; use crate::license::license_check; use crate::server::RemoteAddr; @@ -30,6 +30,7 @@ use rustfs_policy::policy::{ Args, BucketPolicy, BucketPolicyArgs, bucket_policy_needs_existing_object_tag_for_args, bucket_policy_uses_existing_object_tag_conditions, }; +use rustfs_trusted_proxies::ClientInfo; use rustfs_utils::http::AMZ_OBJECT_LOCK_BYPASS_GOVERNANCE; use s3s::access::{S3Access, S3AccessContext}; use s3s::{S3Error, S3ErrorCode, S3Request, S3Result, dto::*, s3_error}; @@ -334,8 +335,16 @@ pub async fn authorize_request(req: &mut S3Request, action: Action) -> S3R let default_claims = HashMap::new(); let claims = cred.claims.as_ref().unwrap_or(&default_claims); - let mut conditions = - get_condition_values_with_query(&req.headers, cred, version_id.as_deref(), None, remote_addr, req.uri.query()); + let client_info = req.extensions.get::(); + let mut conditions = get_condition_values_with_query_and_client_info( + &req.headers, + cred, + version_id.as_deref(), + None, + remote_addr, + req.uri.query(), + client_info, + ); merge_list_bucket_query_conditions(action, req.uri.query(), &mut conditions); let action_args = Args { @@ -543,13 +552,15 @@ pub async fn authorize_request(req: &mut S3Request, action: Action) -> S3R } } else { let default_cred = rustfs_credentials::Credentials::default(); - let mut conditions = get_condition_values_with_query( + let client_info = req.extensions.get::(); + let mut conditions = get_condition_values_with_query_and_client_info( &req.headers, &default_cred, version_id.as_deref(), req.region.clone(), remote_addr, req.uri.query(), + client_info, ); merge_list_bucket_query_conditions(action, req.uri.query(), &mut conditions); diff --git a/scripts/dev_rustfs.env b/scripts/dev_rustfs.env index c9754b506..8debedfaa 100644 --- a/scripts/dev_rustfs.env +++ b/scripts/dev_rustfs.env @@ -1,5 +1,5 @@ -RUSTFS_ACCESS_KEY=rustfsadmin -RUSTFS_SECRET_KEY=rustfsadmin +RUSTFS_ACCESS_KEY=rustfs-dev-admin +RUSTFS_SECRET_KEY=rustfs-dev-secret RUSTFS_VOLUMES="http://node{1...4}:7000/data/rustfs{0...3} http://node{5...8}:7000/data/rustfs{0...3}" RUSTFS_ADDRESS=":7000" diff --git a/scripts/e2e-run.sh b/scripts/e2e-run.sh index b518c5983..8413b2fa1 100755 --- a/scripts/e2e-run.sh +++ b/scripts/e2e-run.sh @@ -22,12 +22,14 @@ mkdir -p $VOLUME export RUST_LOG="rustfs=debug,ecstore=debug,s3s=debug,iam=debug" export RUST_BACKTRACE=full +export RUSTFS_ACCESS_KEY=rustfs-e2e-admin +export RUSTFS_SECRET_KEY=rustfs-e2e-secret $BIN $VOLUME > /tmp/rustfs.log 2>&1 & sleep 10 -export AWS_ACCESS_KEY_ID=rustfsadmin -export AWS_SECRET_ACCESS_KEY=rustfsadmin +export AWS_ACCESS_KEY_ID=$RUSTFS_ACCESS_KEY +export AWS_SECRET_ACCESS_KEY=$RUSTFS_SECRET_KEY export AWS_REGION=us-east-1 export AWS_ENDPOINT_URL=http://localhost:9000 export RUST_LOG="s3s_e2e=debug,s3s_test=info,s3s=debug" diff --git a/scripts/run_e2e_tests.sh b/scripts/run_e2e_tests.sh index 754782f15..5a10a6490 100755 --- a/scripts/run_e2e_tests.sh +++ b/scripts/run_e2e_tests.sh @@ -134,7 +134,7 @@ start_rustfs() { # Start RustFS in background with environment variables cd "$TARGET_DIR" - RUSTFS_ACCESS_KEY=rustfsadmin RUSTFS_SECRET_KEY=rustfsadmin \ + RUSTFS_ACCESS_KEY=rustfs-e2e-admin RUSTFS_SECRET_KEY=rustfs-e2e-secret \ RUSTFS_OBS_LOG_DIRECTORY="$TARGET_DIR/logs" \ ./rustfs --address :9000 "$DATA_DIR" > rustfs.log 2>&1 & RUSTFS_PID=$! @@ -317,4 +317,4 @@ main() { } # Run main function -main "$@" \ No newline at end of file +main "$@" diff --git a/scripts/run_four_node_cluster_failover_bench.sh b/scripts/run_four_node_cluster_failover_bench.sh index f9f0fb673..2c14b3ca7 100755 --- a/scripts/run_four_node_cluster_failover_bench.sh +++ b/scripts/run_four_node_cluster_failover_bench.sh @@ -14,8 +14,8 @@ KEEP_UP="${KEEP_UP:-false}" PRECHECK_AUTO_CLEANUP="${PRECHECK_AUTO_CLEANUP:-true}" WAIT_PROBE_MODE="${WAIT_PROBE_MODE:-service}" -RUSTFS_ACCESS_KEY="${RUSTFS_ACCESS_KEY:-rustfsadmin}" -RUSTFS_SECRET_KEY="${RUSTFS_SECRET_KEY:-rustfsadmin}" +RUSTFS_ACCESS_KEY="${RUSTFS_ACCESS_KEY:-rustfs-cluster-admin}" +RUSTFS_SECRET_KEY="${RUSTFS_SECRET_KEY:-rustfs-cluster-secret}" RUSTFS_OBS_ENDPOINT="${RUSTFS_OBS_ENDPOINT:-}" RUSTFS_UNSAFE_BYPASS_DISK_CHECK="${RUSTFS_UNSAFE_BYPASS_DISK_CHECK:-true}" diff --git a/scripts/s3-tests/README.md b/scripts/s3-tests/README.md index b6a7683fe..df484f8dc 100644 --- a/scripts/s3-tests/README.md +++ b/scripts/s3-tests/README.md @@ -130,8 +130,8 @@ DEPLOY_MODE=existing S3_HOST=192.168.1.100 S3_PORT=9000 ./scripts/s3-tests/run.s ### Service Configuration -- `S3_ACCESS_KEY`: Main user access key (default: `rustfsadmin`) -- `S3_SECRET_KEY`: Main user secret key (default: `rustfsadmin`) +- `S3_ACCESS_KEY`: Main user access key (default: `rustfs-ci-admin`) +- `S3_SECRET_KEY`: Main user secret key (default: `rustfs-ci-secret`) - `S3_ALT_ACCESS_KEY`: Alt user access key (default: `rustfsalt`) - `S3_ALT_SECRET_KEY`: Alt user secret key (default: `rustfsalt`) - `S3_REGION`: S3 region (default: `us-east-1`) @@ -420,8 +420,8 @@ curl http://192.168.1.100:9000/health # Verify S3 API is responding awscurl --service s3 --region us-east-1 \ - --access_key rustfsadmin \ - --secret_key rustfsadmin \ + --access_key rustfs-ci-admin \ + --secret_key rustfs-ci-secret \ -X GET "http://192.168.1.100:9000/" ``` diff --git a/scripts/s3-tests/run.sh b/scripts/s3-tests/run.sh index fd19bd9a3..3eb91aec9 100755 --- a/scripts/s3-tests/run.sh +++ b/scripts/s3-tests/run.sh @@ -25,8 +25,8 @@ PYTHON_VERSION=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.v export PATH="$HOME/Library/Python/${PYTHON_VERSION}/bin:$HOME/.local/bin:$PATH" # Configuration -S3_ACCESS_KEY="${S3_ACCESS_KEY:-rustfsadmin}" -S3_SECRET_KEY="${S3_SECRET_KEY:-rustfsadmin}" +S3_ACCESS_KEY="${S3_ACCESS_KEY:-rustfs-ci-admin}" +S3_SECRET_KEY="${S3_SECRET_KEY:-rustfs-ci-secret}" S3_ALT_ACCESS_KEY="${S3_ALT_ACCESS_KEY:-rustfsalt}" S3_ALT_SECRET_KEY="${S3_ALT_SECRET_KEY:-rustfsalt}" S3_REGION="${S3_REGION:-us-east-1}" @@ -218,8 +218,8 @@ Environment Variables: RUSTFS_BINARY - Path to RustFS binary (for binary mode, default: ./target/release/rustfs) S3_HOST - S3 service host (default: 127.0.0.1) S3_PORT - S3 service port (default: 9000) - S3_ACCESS_KEY - Main user access key (default: rustfsadmin) - S3_SECRET_KEY - Main user secret key (default: rustfsadmin) + S3_ACCESS_KEY - Main user access key (default: rustfs-ci-admin) + S3_SECRET_KEY - Main user secret key (default: rustfs-ci-secret) S3_ALT_ACCESS_KEY - Alt user access key (default: rustfsalt) S3_ALT_SECRET_KEY - Alt user secret key (default: rustfsalt) MAXFAIL - Stop after N failures (default: 1) @@ -690,7 +690,7 @@ envsubst < "${TEMPLATE_PATH}" > "${CONF_OUTPUT_PATH}" || { } # Step 7: Provision s3-tests alt user -# Note: Main user (rustfsadmin) is a system user and doesn't need to be created via API +# Note: The configured main user is a system user and doesn't need to be created via API log_info "Provisioning s3-tests alt user..." # Helper function to install Python packages with fallback for externally-managed environments