synor/apps/faucet/src/secrets.rs
Gulshan Yadav b22c1b89f0 feat: Phase 7 production readiness improvements
- Add SYNOR_BOOTSTRAP_PEERS env var for runtime seed node configuration
- Implement secrets provider abstraction for faucet wallet key security
  (supports file-based secrets in /run/secrets for production)
- Create WASM crypto crate foundation for web wallet (Ed25519, BIP-39)
- Add DEPLOYMENT.md guide for testnet deployment
- Add SECURITY_AUDIT_SCOPE.md for external security audit preparation
- Document seed node deployment process in synor-network

Security improvements:
- Faucet now auto-detects /run/secrets for secure key storage
- CORS already defaults to specific origins (https://faucet.synor.cc)
- Bootstrap peers now configurable at runtime without recompilation
2026-01-08 07:21:14 +05:30

240 lines
7.2 KiB
Rust

//! 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<String>;
/// 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/<pid>/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<String> {
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<Path>) -> Self {
Self {
secrets_dir: secrets_dir.as_ref().to_path_buf(),
}
}
}
impl SecretProvider for FileSecretProvider {
fn get(&self, name: &str) -> Option<String> {
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<Box<dyn SecretProvider>>,
}
#[allow(dead_code)]
impl ChainedSecretProvider {
/// Create a new chained provider with the given providers.
pub fn new(providers: Vec<Box<dyn SecretProvider>>) -> Self {
Self { providers }
}
}
impl SecretProvider for ChainedSecretProvider {
fn get(&self, name: &str) -> Option<String> {
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<dyn SecretProvider> {
// 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");
}
}