// Copyright 2024 RustFS Team // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! Configuration loader for environment variables and files. use crate::{ AppConfig, CacheConfig, CloudConfig, ConfigError, MonitoringConfig, TrustedProxy, TrustedProxyConfig, ValidationMode, parse_ip_list_from_env, parse_string_list_from_env, }; use ipnetwork::IpNetwork; use rustfs_config::{ DEFAULT_TRUSTED_PROXIES_LOG_LEVEL, DEFAULT_TRUSTED_PROXY_CACHE_CAPACITY, DEFAULT_TRUSTED_PROXY_CACHE_CLEANUP_INTERVAL, DEFAULT_TRUSTED_PROXY_CACHE_TTL_SECONDS, DEFAULT_TRUSTED_PROXY_CHAIN_CONTINUITY_CHECK, DEFAULT_TRUSTED_PROXY_CLOUD_METADATA_ENABLED, DEFAULT_TRUSTED_PROXY_CLOUD_METADATA_TIMEOUT, DEFAULT_TRUSTED_PROXY_CLOUD_PROVIDER_FORCE, DEFAULT_TRUSTED_PROXY_CLOUDFLARE_IPS_ENABLED, DEFAULT_TRUSTED_PROXY_ENABLE_RFC7239, DEFAULT_TRUSTED_PROXY_EXTRA_PROXIES, DEFAULT_TRUSTED_PROXY_IPS, DEFAULT_TRUSTED_PROXY_LOG_FAILED_VALIDATIONS, DEFAULT_TRUSTED_PROXY_MAX_HOPS, DEFAULT_TRUSTED_PROXY_METRICS_ENABLED, DEFAULT_TRUSTED_PROXY_PRIVATE_NETWORKS, DEFAULT_TRUSTED_PROXY_PROXIES, DEFAULT_TRUSTED_PROXY_STRUCTURED_LOGGING, DEFAULT_TRUSTED_PROXY_TRACING_ENABLED, DEFAULT_TRUSTED_PROXY_VALIDATION_MODE, ENV_TRUSTED_PROXIES_LOG_LEVEL, ENV_TRUSTED_PROXY_CACHE_CAPACITY, ENV_TRUSTED_PROXY_CACHE_CLEANUP_INTERVAL, ENV_TRUSTED_PROXY_CACHE_TTL_SECONDS, ENV_TRUSTED_PROXY_CHAIN_CONTINUITY_CHECK, ENV_TRUSTED_PROXY_CLOUD_METADATA_ENABLED, ENV_TRUSTED_PROXY_CLOUD_METADATA_TIMEOUT, ENV_TRUSTED_PROXY_CLOUD_PROVIDER_FORCE, ENV_TRUSTED_PROXY_CLOUDFLARE_IPS_ENABLED, ENV_TRUSTED_PROXY_ENABLE_RFC7239, ENV_TRUSTED_PROXY_EXTRA_PROXIES, ENV_TRUSTED_PROXY_IPS, ENV_TRUSTED_PROXY_LOG_FAILED_VALIDATIONS, ENV_TRUSTED_PROXY_MAX_HOPS, ENV_TRUSTED_PROXY_METRICS_ENABLED, ENV_TRUSTED_PROXY_PRIVATE_NETWORKS, ENV_TRUSTED_PROXY_PROXIES, ENV_TRUSTED_PROXY_STRUCTURED_LOGGING, ENV_TRUSTED_PROXY_TRACING_ENABLED, ENV_TRUSTED_PROXY_VALIDATION_MODE, }; use rustfs_utils::{get_env_bool, get_env_str, get_env_u64, get_env_usize, parse_and_resolve_address}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::str::FromStr; use tracing::info; /// Loader for application configuration. #[derive(Debug, Clone)] pub struct ConfigLoader; impl ConfigLoader { /// Loads the complete application configuration from environment variables. pub fn from_env() -> Result { // Load proxy-specific configuration. let proxy_config = Self::load_proxy_config()?; // Load cache configuration. let cache_config = Self::load_cache_config(); // Load monitoring and observability configuration. let monitoring_config = Self::load_monitoring_config(); // Load cloud provider integration configuration. let cloud_config = Self::load_cloud_config(); // Load server binding address. let server_addr = Self::load_server_addr(); Ok(AppConfig::new(proxy_config, cache_config, monitoring_config, cloud_config, server_addr)) } /// Loads trusted proxy configuration from environment variables. fn load_proxy_config() -> Result { let mut proxies = Vec::new(); // Parse base trusted proxies from environment. let base_networks = parse_ip_list_from_env(ENV_TRUSTED_PROXY_PROXIES, DEFAULT_TRUSTED_PROXY_PROXIES)?; for network in base_networks { proxies.push(TrustedProxy::Cidr(network)); } // Parse extra trusted proxies from environment. let extra_networks = parse_ip_list_from_env(ENV_TRUSTED_PROXY_EXTRA_PROXIES, DEFAULT_TRUSTED_PROXY_EXTRA_PROXIES)?; for network in extra_networks { proxies.push(TrustedProxy::Cidr(network)); } // Parse individual trusted proxy IPs. let ip_strings = parse_string_list_from_env(ENV_TRUSTED_PROXY_IPS, DEFAULT_TRUSTED_PROXY_IPS); for ip_str in ip_strings { if let Ok(ip) = ip_str.parse::() { proxies.push(TrustedProxy::Single(ip)); } } // Determine validation mode. let validation_mode_str = get_env_str(ENV_TRUSTED_PROXY_VALIDATION_MODE, DEFAULT_TRUSTED_PROXY_VALIDATION_MODE); let validation_mode = ValidationMode::from_str(&validation_mode_str)?; // Load other proxy settings. let enable_rfc7239 = get_env_bool(ENV_TRUSTED_PROXY_ENABLE_RFC7239, DEFAULT_TRUSTED_PROXY_ENABLE_RFC7239); let max_hops = get_env_usize(ENV_TRUSTED_PROXY_MAX_HOPS, DEFAULT_TRUSTED_PROXY_MAX_HOPS); let enable_chain_check = get_env_bool(ENV_TRUSTED_PROXY_CHAIN_CONTINUITY_CHECK, DEFAULT_TRUSTED_PROXY_CHAIN_CONTINUITY_CHECK); // Load private network ranges. let private_networks = parse_ip_list_from_env(ENV_TRUSTED_PROXY_PRIVATE_NETWORKS, DEFAULT_TRUSTED_PROXY_PRIVATE_NETWORKS)?; // Warn once at startup if the effective trusted set spans private/RFC1918 // (or IPv6 unique-local) ranges. This trusts every host inside those // ranges to set forwarding headers, which is broad; operators should // narrow it to the specific proxy addresses in front of this server. Self::warn_on_private_trusted_networks(&proxies); Ok(TrustedProxyConfig::new( proxies, validation_mode, enable_rfc7239, max_hops, enable_chain_check, private_networks, )) } /// Emits a one-time startup warning when the trusted proxy set includes /// private/RFC1918 (or IPv6 unique-local) ranges. fn warn_on_private_trusted_networks(proxies: &[TrustedProxy]) { let private_trusted: Vec = proxies .iter() .filter(|proxy| Self::is_private_trusted_entry(proxy)) .map(|proxy| proxy.to_string()) .collect(); if !private_trusted.is_empty() { tracing::warn!( event = "trusted_proxies.config", component = "trusted_proxies", subsystem = "config_loader", result = "private_trust_warning", private_networks = ?private_trusted, "trusted proxy set includes private ranges; narrow RUSTFS_TRUSTED_PROXY_NETWORKS to the specific proxy addresses in front of this server to reduce client IP spoofing risk" ); } } /// Returns true when a trusted proxy entry falls within a private/RFC1918 /// IPv4 range or an IPv6 unique-local (fc00::/7) range. Loopback and public /// addresses are not flagged. fn is_private_trusted_entry(proxy: &TrustedProxy) -> bool { let ip = match proxy { TrustedProxy::Single(ip) => *ip, TrustedProxy::Cidr(network) => network.network(), }; match ip { IpAddr::V4(v4) => v4.is_private(), IpAddr::V6(v6) => (v6.segments()[0] & 0xfe00) == 0xfc00, } } /// Loads cache configuration from environment variables. fn load_cache_config() -> CacheConfig { CacheConfig { capacity: get_env_usize(ENV_TRUSTED_PROXY_CACHE_CAPACITY, DEFAULT_TRUSTED_PROXY_CACHE_CAPACITY), ttl_seconds: get_env_u64(ENV_TRUSTED_PROXY_CACHE_TTL_SECONDS, DEFAULT_TRUSTED_PROXY_CACHE_TTL_SECONDS), cleanup_interval_seconds: get_env_u64( ENV_TRUSTED_PROXY_CACHE_CLEANUP_INTERVAL, DEFAULT_TRUSTED_PROXY_CACHE_CLEANUP_INTERVAL, ), } } /// Loads monitoring configuration from environment variables. fn load_monitoring_config() -> MonitoringConfig { MonitoringConfig { metrics_enabled: get_env_bool(ENV_TRUSTED_PROXY_METRICS_ENABLED, DEFAULT_TRUSTED_PROXY_METRICS_ENABLED), log_level: get_env_str(ENV_TRUSTED_PROXIES_LOG_LEVEL, DEFAULT_TRUSTED_PROXIES_LOG_LEVEL), structured_logging: get_env_bool(ENV_TRUSTED_PROXY_STRUCTURED_LOGGING, DEFAULT_TRUSTED_PROXY_STRUCTURED_LOGGING), tracing_enabled: get_env_bool(ENV_TRUSTED_PROXY_TRACING_ENABLED, DEFAULT_TRUSTED_PROXY_TRACING_ENABLED), log_failed_validations: get_env_bool( ENV_TRUSTED_PROXY_LOG_FAILED_VALIDATIONS, DEFAULT_TRUSTED_PROXY_LOG_FAILED_VALIDATIONS, ), } } /// Loads cloud configuration from environment variables. fn load_cloud_config() -> CloudConfig { let forced_provider_str = get_env_str(ENV_TRUSTED_PROXY_CLOUD_PROVIDER_FORCE, DEFAULT_TRUSTED_PROXY_CLOUD_PROVIDER_FORCE); let forced_provider = if forced_provider_str.is_empty() { None } else { Some(forced_provider_str) }; CloudConfig { metadata_enabled: get_env_bool( ENV_TRUSTED_PROXY_CLOUD_METADATA_ENABLED, DEFAULT_TRUSTED_PROXY_CLOUD_METADATA_ENABLED, ), metadata_timeout_seconds: get_env_u64( ENV_TRUSTED_PROXY_CLOUD_METADATA_TIMEOUT, DEFAULT_TRUSTED_PROXY_CLOUD_METADATA_TIMEOUT, ), cloudflare_ips_enabled: get_env_bool( ENV_TRUSTED_PROXY_CLOUDFLARE_IPS_ENABLED, DEFAULT_TRUSTED_PROXY_CLOUDFLARE_IPS_ENABLED, ), forced_provider, } } /// Loads the server binding address from environment variables. fn load_server_addr() -> SocketAddr { let address = get_env_str("RUSTFS_ADDRESS", rustfs_config::DEFAULT_ADDRESS); parse_and_resolve_address(&address) .unwrap_or_else(|_| SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), rustfs_config::DEFAULT_PORT)) } /// Loads configuration from environment, falling back to defaults on failure. pub fn from_env_or_default() -> AppConfig { match Self::from_env() { Ok(config) => { info!( event = "trusted_proxies.config", component = "trusted_proxies", subsystem = "config_loader", result = "loaded", source = "environment", "trusted proxies configuration loaded" ); config } Err(e) => { tracing::warn!( event = "trusted_proxies.config", component = "trusted_proxies", subsystem = "config_loader", result = "fallback", source = "defaults", error = %e, "trusted proxies configuration fell back to defaults" ); Self::default_config() } } } /// Returns a default configuration. pub fn default_config() -> AppConfig { let proxy_config = TrustedProxyConfig::new( vec![ TrustedProxy::Single(IpAddr::V4(Ipv4Addr::LOCALHOST)), TrustedProxy::Single(IpAddr::V6(Ipv6Addr::LOCALHOST)), ], ValidationMode::HopByHop, true, 10, true, DEFAULT_TRUSTED_PROXY_PRIVATE_NETWORKS .split(',') .filter_map(|s| s.trim().parse::().ok()) .collect(), ); AppConfig::new( proxy_config, CacheConfig::default(), MonitoringConfig::default(), CloudConfig::default(), SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), rustfs_config::DEFAULT_PORT), ) } /// Prints a summary of the configuration to the log. pub fn print_summary(config: &AppConfig) { info!( event = "trusted_proxies.config", component = "trusted_proxies", subsystem = "config_loader", result = "summary", server_addr = %config.server_addr, trusted_proxy_count = config.proxy.proxies.len(), validation_mode = config.proxy.validation_mode.as_str(), cache_capacity = config.cache.capacity, cache_ttl_seconds = config.cache.ttl_seconds, cache_cleanup_interval_seconds = config.cache.cleanup_interval_seconds, metrics_enabled = config.monitoring.metrics_enabled, structured_logging = config.monitoring.structured_logging, tracing_enabled = config.monitoring.tracing_enabled, log_failed_validations = config.monitoring.log_failed_validations, cloud_metadata_enabled = config.cloud.metadata_enabled, cloud_metadata_timeout_seconds = config.cloud.metadata_timeout_seconds, cloudflare_ips_enabled = config.cloud.cloudflare_ips_enabled, forced_provider = config.cloud.forced_provider.as_deref().unwrap_or("none"), "trusted proxies configuration summarized" ); if !config.proxy.proxies.is_empty() { tracing::debug!( event = "trusted_proxies.config", component = "trusted_proxies", subsystem = "config_loader", result = "trusted_networks", trusted_proxy_count = config.proxy.proxies.len(), trusted_networks = ?config.proxy.get_network_strings(), "trusted proxies networks enumerated" ); } } }