- 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
240 lines
7.2 KiB
Rust
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");
|
|
}
|
|
}
|