//! Secrets management for the faucet. //! //! Provides a pluggable secrets provider abstraction that supports: //! - Environment variables (development only) //! - File-based secrets (simple production) //! - External secrets managers (enterprise production) //! //! # Security Best Practices //! //! 1. **Never use environment variables in production** - They can be exposed //! in process listings, logs, and container orchestration UIs. //! //! 2. **Use file-based secrets** as minimum security - Mount a secret file //! with restricted permissions (0400) from a secure volume. //! //! 3. **Use a secrets manager** for production - AWS Secrets Manager, //! HashiCorp Vault, or similar provides rotation and audit logging. use std::path::Path; use tracing::{info, warn}; /// Secret provider trait for pluggable secret storage backends. pub trait SecretProvider: Send + Sync { /// Get a secret value by name. fn get(&self, name: &str) -> Option; /// Provider name for logging. fn provider_name(&self) -> &'static str; } /// Environment variable secret provider (development only). /// /// # Warning /// /// Environment variables are NOT secure for production use: /// - Visible in `/proc//environ` on Linux /// - Exposed in `ps auxe` output /// - May be logged by container orchestrators /// - No access control or audit logging pub struct EnvSecretProvider; impl SecretProvider for EnvSecretProvider { fn get(&self, name: &str) -> Option { std::env::var(name).ok() } fn provider_name(&self) -> &'static str { "environment" } } /// File-based secret provider. /// /// Reads secrets from files in a secrets directory. Each secret is stored /// as a separate file named after the secret key. /// /// # Example /// /// ```text /// /run/secrets/ /// ├── FAUCET_WALLET_KEY /// └── DATABASE_PASSWORD /// ``` /// /// # Security Notes /// /// - Set file permissions to 0400 (owner read only) /// - Mount from tmpfs or encrypted volume /// - Use immutable container images pub struct FileSecretProvider { secrets_dir: std::path::PathBuf, } impl FileSecretProvider { /// Create a new file-based secret provider. pub fn new(secrets_dir: impl AsRef) -> Self { Self { secrets_dir: secrets_dir.as_ref().to_path_buf(), } } } impl SecretProvider for FileSecretProvider { fn get(&self, name: &str) -> Option { let path = self.secrets_dir.join(name); std::fs::read_to_string(&path) .ok() .map(|s| s.trim().to_string()) } fn provider_name(&self) -> &'static str { "file" } } /// Chained secret provider that tries multiple providers in order. /// /// Useful for fallback scenarios, e.g., try file first, then env. #[allow(dead_code)] pub struct ChainedSecretProvider { providers: Vec>, } #[allow(dead_code)] impl ChainedSecretProvider { /// Create a new chained provider with the given providers. pub fn new(providers: Vec>) -> Self { Self { providers } } } impl SecretProvider for ChainedSecretProvider { fn get(&self, name: &str) -> Option { for provider in &self.providers { if let Some(value) = provider.get(name) { return Some(value); } } None } fn provider_name(&self) -> &'static str { "chained" } } /// Creates the appropriate secret provider based on configuration. /// /// Priority order: /// 1. If SECRETS_DIR is set, use file-based provider /// 2. If SECRETS_PROVIDER=file, use file-based provider with /run/secrets /// 3. Fall back to environment variables with a warning pub fn create_secret_provider() -> Box { // Check for explicit secrets directory if let Ok(dir) = std::env::var("SECRETS_DIR") { info!(dir = %dir, "Using file-based secrets provider"); return Box::new(FileSecretProvider::new(dir)); } // Check for explicit provider type if let Ok(provider) = std::env::var("SECRETS_PROVIDER") { match provider.as_str() { "file" => { let dir = std::env::var("SECRETS_DIR").unwrap_or_else(|_| "/run/secrets".to_string()); info!(dir = %dir, "Using file-based secrets provider"); return Box::new(FileSecretProvider::new(dir)); } "env" => { warn!( "Using environment variable secrets provider - NOT RECOMMENDED FOR PRODUCTION" ); return Box::new(EnvSecretProvider); } _ => { warn!(provider = %provider, "Unknown secrets provider, falling back to environment"); } } } // Default: check for /run/secrets directory (common in Docker/Kubernetes) if Path::new("/run/secrets").is_dir() { info!("Detected /run/secrets directory, using file-based secrets"); return Box::new(FileSecretProvider::new("/run/secrets")); } // Fallback to environment variables with warning warn!( "Using environment variable secrets provider. \ This is NOT recommended for production. \ Set SECRETS_DIR or mount secrets to /run/secrets." ); Box::new(EnvSecretProvider) } /// Zeroize a string in memory (best effort). /// /// This attempts to overwrite the string's memory before dropping. /// Note: Rust's String may have been reallocated, so this isn't perfect. #[allow(dead_code)] pub fn zeroize_string(mut s: String) { // SAFETY: We're about to drop this string anyway unsafe { let bytes = s.as_bytes_mut(); std::ptr::write_bytes(bytes.as_mut_ptr(), 0, bytes.len()); } drop(s); } #[cfg(test)] mod tests { use super::*; use tempfile::tempdir; #[test] fn test_file_secret_provider() { let dir = tempdir().unwrap(); let secret_path = dir.path().join("TEST_SECRET"); std::fs::write(&secret_path, "secret_value\n").unwrap(); let provider = FileSecretProvider::new(dir.path()); assert_eq!( provider.get("TEST_SECRET"), Some("secret_value".to_string()) ); assert_eq!(provider.get("NONEXISTENT"), None); } #[test] fn test_env_secret_provider() { std::env::set_var("TEST_ENV_SECRET", "env_value"); let provider = EnvSecretProvider; assert_eq!( provider.get("TEST_ENV_SECRET"), Some("env_value".to_string()) ); std::env::remove_var("TEST_ENV_SECRET"); } #[test] fn test_chained_provider() { let dir = tempdir().unwrap(); let secret_path = dir.path().join("FILE_SECRET"); std::fs::write(&secret_path, "file_value").unwrap(); std::env::set_var("ENV_SECRET", "env_value"); let provider = ChainedSecretProvider::new(vec![ Box::new(FileSecretProvider::new(dir.path())), Box::new(EnvSecretProvider), ]); // File provider takes precedence assert_eq!(provider.get("FILE_SECRET"), Some("file_value".to_string())); // Falls through to env provider assert_eq!(provider.get("ENV_SECRET"), Some("env_value".to_string())); std::env::remove_var("ENV_SECRET"); } }