Skip to content

Commit c6cdd75

Browse files
feat(config): block sensitive env overrides by suffix and add tests
- Add suffix-based, case-insensitive filter for sensitive env vars in `zainod/src/config.rs`: - Deny leaf keys ending with: `password`, `secret`, `token`, `cookie`, `private_key`. - Pre-filter `ZAINO_*` vars and pass sanitized map via `Environment::source(Some(filtered_env))` with `try_parsing(true)`. - Keep non-sensitive fields overridable (e.g., `cookie_dir`, `tls_cert_path`, `tls_key_path`). - Add tests in `zainod/tests/config.rs`: - `test_env_unknown_non_sensitive_key_is_ignored` - `test_env_unknown_sensitive_key_is_ignored` - `test_env_validator_password_is_ignored` (env does not override default)
1 parent 2358167 commit c6cdd75

2 files changed

Lines changed: 83 additions & 1 deletion

File tree

zainod/src/config.rs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,17 @@ use zaino_state::{BackendConfig, FetchServiceConfig, StateServiceConfig};
2020

2121
use crate::error::IndexerError;
2222

23+
/// Returns true if a leaf key name should be considered sensitive and blocked
24+
/// from environment variable overrides.
25+
fn is_sensitive_leaf_key(leaf_key: &str) -> bool {
26+
let lower = leaf_key.to_ascii_lowercase();
27+
lower.ends_with("password")
28+
|| lower.ends_with("secret")
29+
|| lower.ends_with("token")
30+
|| lower.ends_with("cookie")
31+
|| lower.ends_with("private_key")
32+
}
33+
2334
/// Custom deserialization function for `SocketAddr` from a String.
2435
/// Used by Serde's `deserialize_with`.
2536
fn deserialize_socketaddr_from_string<'de, D>(deserializer: D) -> Result<SocketAddr, D::Error>
@@ -397,13 +408,39 @@ pub fn load_config(file_path: &Path) -> Result<IndexerConfig, IndexerError> {
397408
IndexerError::ConfigError(format!("Failed to create config from defaults: {e}"))
398409
})?;
399410

411+
// Build a filtered view of the process environment for ZAINO_* keys,
412+
// removing sensitive-leaf overrides.
413+
let mut filtered_env: std::collections::HashMap<String, String> =
414+
std::collections::HashMap::new();
415+
for (key, value) in std::env::vars() {
416+
if !key.starts_with("ZAINO_") {
417+
continue;
418+
}
419+
420+
if let Some(without_prefix) = key.strip_prefix("ZAINO_") {
421+
// Zaino uses flat env keys, but split on "__" for consistency with nested patterns.
422+
let parts: Vec<&str> = without_prefix.split("__").collect();
423+
if let Some(leaf) = parts.last() {
424+
if is_sensitive_leaf_key(leaf) {
425+
continue;
426+
}
427+
}
428+
}
429+
430+
filtered_env.insert(key, value);
431+
}
432+
400433
let parsed_config: IndexerConfig = config::Config::builder()
401434
// 1. Start with defaults (lowest precedence)
402435
.add_source(defaults_config)
403436
// 2. Override with TOML file values
404437
.add_source(config::File::from(file_path).required(true))
405438
// 3. Override with environment variables (highest precedence)
406-
.add_source(config::Environment::with_prefix("ZAINO").try_parsing(true))
439+
.add_source(
440+
config::Environment::with_prefix("ZAINO")
441+
.try_parsing(true)
442+
.source(Some(filtered_env)),
443+
)
407444
.build()
408445
.and_then(|config| config.try_deserialize())
409446
.map_err(|e| IndexerError::ConfigError(e.to_string()))?;

zainod/tests/config.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,3 +451,48 @@ fn test_invalid_env_var_type() {
451451
assert!(msg.to_lowercase().contains("map_capacity") || msg.contains("invalid"));
452452
}
453453
}
454+
455+
// --- Sensitive env deny-list behaviour ---
456+
457+
#[test]
458+
fn test_env_unknown_non_sensitive_key_is_ignored() {
459+
let env = EnvGuard::new();
460+
let temp_dir = env.temp_dir();
461+
462+
let test_config_path = env.create_file(&temp_dir, "test_config.toml", "");
463+
// Unknown non-sensitive key is ignored because IndexerConfig does not deny unknown fields
464+
env.set_var("ZAINO_FOO", "bar");
465+
let result = load_config(&test_config_path);
466+
assert!(result.is_ok());
467+
}
468+
469+
#[test]
470+
fn test_env_unknown_sensitive_key_is_ignored() {
471+
let env = EnvGuard::new();
472+
let temp_dir = env.temp_dir();
473+
474+
let test_config_path = env.create_file(&temp_dir, "test_config.toml", "");
475+
// Unknown sensitive-suffix key should be filtered and not error
476+
env.set_var("ZAINO_TOKEN", "supersecret");
477+
let result = load_config(&test_config_path);
478+
assert!(result.is_ok());
479+
}
480+
481+
#[test]
482+
fn test_env_validator_password_is_ignored() {
483+
let env = EnvGuard::new();
484+
let temp_dir = env.temp_dir();
485+
486+
// Minimal config to satisfy required fields
487+
let toml_str = r#"
488+
network = "Testnet"
489+
"#;
490+
let path = env.create_file(&temp_dir, "base.toml", toml_str);
491+
492+
// Attempt to set validator_password via env; should be ignored
493+
env.set_var("ZAINO_VALIDATOR_PASSWORD", "topsecret");
494+
495+
let cfg = load_config(&path).expect("config should load with filtered secret env");
496+
// Default is Some("xxxxxx") per IndexerConfig::default(); env should not override
497+
assert_eq!(cfg.validator_password, Some("xxxxxx".to_string()));
498+
}

0 commit comments

Comments
 (0)