mod auth; mod crypto; mod engines; mod error; mod logging; mod seal; mod server; mod storage; mod telemetry; mod vault; // Re-export all public types use std::path::Path; pub use auth::{AuthConfig, CedarAuthConfig, TokenAuthConfig}; pub use crypto::{ AwsLcCryptoConfig, CryptoConfig, OpenSSLCryptoConfig, OqsCryptoConfig, RustCryptoCryptoConfig, }; pub use engines::{EngineConfig, EnginesConfig}; pub use error::{ConfigError, ConfigResult}; pub use logging::LoggingConfig; pub use seal::{AutoUnsealConfig, SealConfig, ShamirSealConfig}; pub use server::ServerSection; pub use storage::{ EtcdStorageConfig, FilesystemStorageConfig, PostgreSQLStorageConfig, StorageConfig, SurrealDBStorageConfig, }; pub use telemetry::TelemetryConfig; pub use vault::VaultSection; /// Main vault configuration #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct VaultConfig { #[serde(default)] pub vault: VaultSection, #[serde(default)] pub server: ServerSection, pub storage: StorageConfig, #[serde(default)] pub crypto: CryptoConfig, #[serde(default)] pub seal: SealConfig, #[serde(default)] pub auth: AuthConfig, #[serde(default)] pub engines: EnginesConfig, #[serde(default)] pub logging: LoggingConfig, #[serde(default)] pub telemetry: TelemetryConfig, } impl VaultConfig { /// Load configuration from TOML file pub fn from_file>(path: P) -> ConfigResult { let content = std::fs::read_to_string(path)?; Self::from_str(&content) } /// Load configuration from TOML string #[allow(clippy::should_implement_trait)] pub fn from_str(content: &str) -> ConfigResult { let content = Self::substitute_env_vars(content)?; let config: Self = toml::from_str(&content)?; config.validate()?; Ok(config) } /// Validate configuration fn validate(&self) -> ConfigResult<()> { // Validate crypto backend let valid_crypto_backends = ["openssl", "aws-lc", "rustcrypto", "tongsuo", "oqs"]; if !valid_crypto_backends.contains(&self.vault.crypto_backend.as_str()) { return Err(ConfigError::UnknownCryptoBackend( self.vault.crypto_backend.clone(), )); } // Validate storage backend let valid_storage_backends = ["filesystem", "surrealdb", "etcd", "postgresql"]; if !valid_storage_backends.contains(&self.storage.backend.as_str()) { return Err(ConfigError::UnknownStorageBackend( self.storage.backend.clone(), )); } // Validate seal type let valid_seal_types = ["shamir", "auto", "transit"]; if !valid_seal_types.contains(&self.seal.seal_type.as_str()) { return Err(ConfigError::InvalidSealConfig( "Invalid seal type".to_string(), )); } // Validate Shamir configuration if self.seal.seal_type == "shamir" { if self.seal.shamir.shares < 2 { return Err(ConfigError::InvalidSealConfig( "Shamir shares must be >= 2".to_string(), )); } if self.seal.shamir.threshold > self.seal.shamir.shares { return Err(ConfigError::InvalidSealConfig( "Shamir threshold must be <= shares".to_string(), )); } if self.seal.shamir.threshold < 1 { return Err(ConfigError::InvalidSealConfig( "Shamir threshold must be >= 1".to_string(), )); } } // Validate engine mount paths (no duplicates, valid format) let paths = self.engines.all_paths(); let mut seen = std::collections::HashSet::new(); for path in paths { if !path.starts_with('/') || path == "/" { return Err(ConfigError::InvalidMountPath(path)); } if !seen.insert(path.clone()) { return Err(ConfigError::DuplicateMountPath(path)); } } Ok(()) } /// Substitute environment variables in format ${VAR_NAME} /// Only processes variables in active (uncommented) sections fn substitute_env_vars(content: &str) -> ConfigResult { let re = regex::Regex::new(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}") .map_err(|e| ConfigError::Invalid(e.to_string()))?; // Process line by line to skip commented sections let processed = content .lines() .map(|line| { // Skip lines that start with # (comments) if line.trim_start().starts_with('#') { return line.to_string(); } // Replace env vars only in non-comment lines re.replace_all(line, |caps: ®ex::Captures| { let var_name = &caps[1]; std::env::var(var_name).unwrap_or_else(|_| format!("${{{}}}", var_name)) }) .to_string() }) .collect::>() .join("\n"); // Check if any variables remain unsubstituted in non-comment lines for line in processed.lines() { if !line.trim_start().starts_with('#') && re.is_match(line) { if let Some(m) = re.find(line) { let var_name = &line[m.start() + 2..m.end() - 1]; return Err(ConfigError::EnvVarNotFound(var_name.to_string())); } } } Ok(processed) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_config_env_var_substitution() { std::env::set_var("TEST_PASSWORD", "secret123"); let config_str = r#" [storage] backend = "surrealdb" [storage.surrealdb] password = "${TEST_PASSWORD}" "#; let config = VaultConfig::from_str(config_str).expect("Failed to parse config"); assert_eq!( config.storage.surrealdb.password, Some("secret123".to_string()) ); } #[test] fn test_config_validation_invalid_crypto_backend() { let config_str = r#" [vault] crypto_backend = "invalid" [storage] backend = "filesystem" "#; let result = VaultConfig::from_str(config_str); assert!(result.is_err()); } #[test] fn test_config_validation_shamir_threshold() { let config_str = r#" [storage] backend = "filesystem" [seal] seal_type = "shamir" [seal.shamir] shares = 3 threshold = 5 "#; let result = VaultConfig::from_str(config_str); assert!(result.is_err()); } #[test] fn test_config_default_values() { let config_str = r#" [storage] backend = "filesystem" "#; let config = VaultConfig::from_str(config_str).expect("Failed to parse config"); assert_eq!(config.vault.crypto_backend, "openssl"); assert_eq!(config.server.address, "127.0.0.1:8200"); assert_eq!(config.seal.seal_type, "shamir"); assert_eq!(config.seal.shamir.shares, 5); assert_eq!(config.seal.shamir.threshold, 3); } #[test] fn test_config_ignores_commented_env_vars() { // This test verifies that env vars in commented sections don't cause config // load failure let config_str = r#" [vault] crypto_backend = "openssl" [storage] backend = "filesystem" [storage.filesystem] path = "/tmp/vault" # Example SurrealDB configuration (commented out) # [storage.surrealdb] # endpoint = "ws://localhost:8000" # password = "${SURREAL_PASSWORD}" "#; // Should succeed even though SURREAL_PASSWORD env var doesn't exist let result = VaultConfig::from_str(config_str); assert!( result.is_ok(), "Config should load without commented env vars failing" ); } #[test] fn test_config_fails_on_active_missing_env_vars() { let config_str = r#" [storage] backend = "filesystem" [storage.filesystem] path = "${MISSING_VAR}" "#; // Should fail because MISSING_VAR is in active (uncommented) config let result = VaultConfig::from_str(config_str); assert!( result.is_err(), "Config should fail on missing env vars in active sections" ); } }