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, 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"]; 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} 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()))?; let result = re.replace_all(content, |caps: ®ex::Captures| { let var_name = &caps[1]; std::env::var(var_name).unwrap_or_else(|_| format!("${{{}}}", var_name)) }); // Check if any variables remain unsubstituted if re.is_match(&result) { if let Some(m) = re.find(&result) { let var_name = &result[m.start() + 2..m.end() - 1]; return Err(ConfigError::EnvVarNotFound(var_name.to_string())); } } Ok(result.to_string()) } } #[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); } }