Jesús Pérez 2cc472b0bf
Some checks failed
Rust CI / Security Audit (push) Has been cancelled
Rust CI / Check + Test + Lint (nightly) (push) Has been cancelled
Rust CI / Check + Test + Lint (stable) (push) Has been cancelled
chore: use +nightly for cargo fmt and fix pre-commit a just recipes
2025-12-29 05:04:53 +00:00

227 lines
6.4 KiB
Rust

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<P: AsRef<Path>>(path: P) -> ConfigResult<Self> {
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<Self> {
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<String> {
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: &regex::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);
}
}