Skip to main content

aegis_server/
secrets.rs

1//! Aegis Secrets Management
2//!
3//! Secure secrets management with support for environment variables and HashiCorp Vault.
4//!
5//! @version 0.1.0
6//! @author AutomataNexus Development Team
7
8use parking_lot::RwLock;
9use std::collections::HashMap;
10use std::sync::Arc;
11
12// =============================================================================
13// Secrets Provider Trait
14// =============================================================================
15
16/// Trait for secrets providers.
17pub trait SecretsProvider: Send + Sync {
18    /// Get a secret value by key.
19    fn get(&self, key: &str) -> Option<String>;
20
21    /// Get a secret with a default fallback.
22    fn get_or(&self, key: &str, default: &str) -> String {
23        self.get(key).unwrap_or_else(|| default.to_string())
24    }
25
26    /// Check if a secret exists.
27    fn exists(&self, key: &str) -> bool {
28        self.get(key).is_some()
29    }
30}
31
32// =============================================================================
33// Environment Variables Provider
34// =============================================================================
35
36/// Secrets provider that reads from environment variables.
37#[derive(Debug, Default)]
38pub struct EnvSecretsProvider;
39
40impl SecretsProvider for EnvSecretsProvider {
41    fn get(&self, key: &str) -> Option<String> {
42        std::env::var(key).ok()
43    }
44}
45
46// =============================================================================
47// HashiCorp Vault Provider
48// =============================================================================
49
50/// Configuration for HashiCorp Vault.
51#[derive(Debug, Clone)]
52pub struct VaultConfig {
53    /// Vault server address (e.g., "https://vault.example.com:8200")
54    pub address: String,
55    /// Authentication token
56    pub token: Option<String>,
57    /// AppRole role_id for authentication
58    pub role_id: Option<String>,
59    /// AppRole secret_id for authentication
60    pub secret_id: Option<String>,
61    /// Kubernetes auth role (for K8s deployments)
62    pub k8s_role: Option<String>,
63    /// Secret engine mount path (default: "secret")
64    pub mount_path: String,
65    /// Secret path prefix (e.g., "aegis/prod")
66    pub secret_path: String,
67    /// Whether to use TLS verification
68    pub tls_verify: bool,
69}
70
71impl Default for VaultConfig {
72    fn default() -> Self {
73        Self {
74            address: std::env::var("VAULT_ADDR")
75                .unwrap_or_else(|_| "http://127.0.0.1:8200".to_string()),
76            token: std::env::var("VAULT_TOKEN").ok(),
77            role_id: std::env::var("VAULT_ROLE_ID").ok(),
78            secret_id: std::env::var("VAULT_SECRET_ID").ok(),
79            k8s_role: std::env::var("VAULT_K8S_ROLE").ok(),
80            mount_path: std::env::var("VAULT_MOUNT_PATH").unwrap_or_else(|_| "secret".to_string()),
81            secret_path: std::env::var("VAULT_SECRET_PATH").unwrap_or_else(|_| "aegis".to_string()),
82            tls_verify: std::env::var("VAULT_TLS_VERIFY")
83                .map(|v| v != "false" && v != "0")
84                .unwrap_or(true),
85        }
86    }
87}
88
89/// Secrets provider that reads from HashiCorp Vault.
90pub struct VaultSecretsProvider {
91    config: VaultConfig,
92    client: reqwest::Client,
93    token: RwLock<Option<String>>,
94    cache: RwLock<HashMap<String, String>>,
95}
96
97impl VaultSecretsProvider {
98    /// Create a new Vault secrets provider.
99    pub fn new(config: VaultConfig) -> Self {
100        let client = reqwest::Client::builder()
101            .danger_accept_invalid_certs(!config.tls_verify)
102            .build()
103            .expect("Failed to create HTTP client");
104
105        let token = config.token.clone();
106
107        Self {
108            config,
109            client,
110            token: RwLock::new(token),
111            cache: RwLock::new(HashMap::new()),
112        }
113    }
114
115    /// Create from environment variables.
116    pub fn from_env() -> Option<Self> {
117        let config = VaultConfig::default();
118
119        // Only create if Vault address is configured
120        if std::env::var("VAULT_ADDR").is_err() {
121            return None;
122        }
123
124        Some(Self::new(config))
125    }
126
127    /// Authenticate with Vault using configured method.
128    pub async fn authenticate(&self) -> Result<(), String> {
129        // If we already have a token, we're good
130        if self.token.read().is_some() {
131            return Ok(());
132        }
133
134        // Try AppRole authentication
135        if let (Some(role_id), Some(secret_id)) = (&self.config.role_id, &self.config.secret_id) {
136            return self.auth_approle(role_id, secret_id).await;
137        }
138
139        // Try Kubernetes authentication
140        if let Some(k8s_role) = &self.config.k8s_role {
141            return self.auth_kubernetes(k8s_role).await;
142        }
143
144        Err("No authentication method configured. Set VAULT_TOKEN, VAULT_ROLE_ID/VAULT_SECRET_ID, or VAULT_K8S_ROLE".to_string())
145    }
146
147    /// Authenticate using AppRole.
148    async fn auth_approle(&self, role_id: &str, secret_id: &str) -> Result<(), String> {
149        let url = format!("{}/v1/auth/approle/login", self.config.address);
150        let body = serde_json::json!({
151            "role_id": role_id,
152            "secret_id": secret_id
153        });
154
155        let response = self
156            .client
157            .post(&url)
158            .json(&body)
159            .send()
160            .await
161            .map_err(|e| format!("Vault AppRole auth failed: {}", e))?;
162
163        if !response.status().is_success() {
164            return Err(format!(
165                "Vault AppRole auth failed: HTTP {}",
166                response.status()
167            ));
168        }
169
170        let data: serde_json::Value = response
171            .json()
172            .await
173            .map_err(|e| format!("Failed to parse Vault response: {}", e))?;
174
175        let token = data["auth"]["client_token"]
176            .as_str()
177            .ok_or("No token in Vault response")?
178            .to_string();
179
180        *self.token.write() = Some(token);
181        tracing::info!("Successfully authenticated with Vault using AppRole");
182
183        Ok(())
184    }
185
186    /// Authenticate using Kubernetes service account.
187    async fn auth_kubernetes(&self, role: &str) -> Result<(), String> {
188        // Read the service account token
189        let jwt = std::fs::read_to_string("/var/run/secrets/kubernetes.io/serviceaccount/token")
190            .map_err(|e| format!("Failed to read K8s service account token: {}", e))?;
191
192        let url = format!("{}/v1/auth/kubernetes/login", self.config.address);
193        let body = serde_json::json!({
194            "role": role,
195            "jwt": jwt
196        });
197
198        let response = self
199            .client
200            .post(&url)
201            .json(&body)
202            .send()
203            .await
204            .map_err(|e| format!("Vault K8s auth failed: {}", e))?;
205
206        if !response.status().is_success() {
207            return Err(format!("Vault K8s auth failed: HTTP {}", response.status()));
208        }
209
210        let data: serde_json::Value = response
211            .json()
212            .await
213            .map_err(|e| format!("Failed to parse Vault response: {}", e))?;
214
215        let token = data["auth"]["client_token"]
216            .as_str()
217            .ok_or("No token in Vault response")?
218            .to_string();
219
220        *self.token.write() = Some(token);
221        tracing::info!("Successfully authenticated with Vault using Kubernetes");
222
223        Ok(())
224    }
225
226    /// Read a secret from Vault (KV v2).
227    pub async fn read_secret(&self, key: &str) -> Result<String, String> {
228        // Check cache first
229        if let Some(value) = self.cache.read().get(key) {
230            return Ok(value.clone());
231        }
232
233        let token = self
234            .token
235            .read()
236            .clone()
237            .ok_or("Not authenticated with Vault")?;
238
239        let url = format!(
240            "{}/v1/{}/data/{}/{}",
241            self.config.address, self.config.mount_path, self.config.secret_path, key
242        );
243
244        let response = self
245            .client
246            .get(&url)
247            .header("X-Vault-Token", &token)
248            .send()
249            .await
250            .map_err(|e| format!("Vault read failed: {}", e))?;
251
252        if !response.status().is_success() {
253            return Err(format!("Vault read failed: HTTP {}", response.status()));
254        }
255
256        let data: serde_json::Value = response
257            .json()
258            .await
259            .map_err(|e| format!("Failed to parse Vault response: {}", e))?;
260
261        // KV v2 format: data.data.{key}
262        let value = data["data"]["data"]["value"]
263            .as_str()
264            .ok_or_else(|| format!("Secret '{}' not found or has no 'value' field", key))?
265            .to_string();
266
267        // Cache the value
268        self.cache.write().insert(key.to_string(), value.clone());
269
270        Ok(value)
271    }
272
273    /// Read all secrets at a path and cache them.
274    pub async fn load_secrets(&self) -> Result<(), String> {
275        let token = self
276            .token
277            .read()
278            .clone()
279            .ok_or("Not authenticated with Vault")?;
280
281        let url = format!(
282            "{}/v1/{}/data/{}",
283            self.config.address, self.config.mount_path, self.config.secret_path
284        );
285
286        let response = self
287            .client
288            .get(&url)
289            .header("X-Vault-Token", &token)
290            .send()
291            .await
292            .map_err(|e| format!("Vault read failed: {}", e))?;
293
294        if !response.status().is_success() {
295            return Err(format!("Vault read failed: HTTP {}", response.status()));
296        }
297
298        let data: serde_json::Value = response
299            .json()
300            .await
301            .map_err(|e| format!("Failed to parse Vault response: {}", e))?;
302
303        // KV v2 format: data.data contains all key-value pairs
304        if let Some(secrets) = data["data"]["data"].as_object() {
305            let mut cache = self.cache.write();
306            for (key, value) in secrets {
307                if let Some(v) = value.as_str() {
308                    cache.insert(key.clone(), v.to_string());
309                }
310            }
311            tracing::info!("Loaded {} secrets from Vault", cache.len());
312        }
313
314        Ok(())
315    }
316
317    /// Get a cached secret (synchronous).
318    pub fn get_cached(&self, key: &str) -> Option<String> {
319        self.cache.read().get(key).cloned()
320    }
321}
322
323impl SecretsProvider for VaultSecretsProvider {
324    fn get(&self, key: &str) -> Option<String> {
325        // First check cache
326        if let Some(value) = self.get_cached(key) {
327            return Some(value);
328        }
329
330        // Fall back to environment variable
331        std::env::var(key).ok()
332    }
333}
334
335// =============================================================================
336// Composite Secrets Manager
337// =============================================================================
338
339/// Manages secrets from multiple providers with fallback chain.
340pub struct SecretsManager {
341    providers: Vec<Arc<dyn SecretsProvider>>,
342}
343
344impl SecretsManager {
345    /// Create a new secrets manager with the given providers.
346    /// Providers are checked in order; first match wins.
347    pub fn new(providers: Vec<Arc<dyn SecretsProvider>>) -> Self {
348        Self { providers }
349    }
350
351    /// Create a secrets manager with environment variables only.
352    pub fn env_only() -> Self {
353        Self {
354            providers: vec![Arc::new(EnvSecretsProvider)],
355        }
356    }
357
358    /// Create a secrets manager that tries Vault first, then environment variables.
359    pub fn with_vault_fallback(vault: VaultSecretsProvider) -> Self {
360        Self {
361            providers: vec![Arc::new(vault), Arc::new(EnvSecretsProvider)],
362        }
363    }
364}
365
366impl SecretsProvider for SecretsManager {
367    fn get(&self, key: &str) -> Option<String> {
368        for provider in &self.providers {
369            if let Some(value) = provider.get(key) {
370                return Some(value);
371            }
372        }
373        None
374    }
375}
376
377// =============================================================================
378// Helper Functions
379// =============================================================================
380
381/// Wrapper that implements SecretsProvider for AegisVault.
382pub struct AegisVaultProvider {
383    vault: std::sync::Arc<aegis_vault::AegisVault>,
384}
385
386impl AegisVaultProvider {
387    pub fn new(vault: std::sync::Arc<aegis_vault::AegisVault>) -> Self {
388        Self { vault }
389    }
390}
391
392impl SecretsProvider for AegisVaultProvider {
393    fn get(&self, key: &str) -> Option<String> {
394        self.vault.get(key, "secrets_manager").ok()
395    }
396}
397
398/// Initialize secrets manager from environment configuration.
399/// Provider chain: built-in vault → external HashiCorp Vault → environment variables.
400pub async fn init_secrets_manager(
401    built_in_vault: Option<std::sync::Arc<aegis_vault::AegisVault>>,
402) -> SecretsManager {
403    let mut providers: Vec<Arc<dyn SecretsProvider>> = Vec::new();
404
405    // First priority: built-in Aegis vault
406    if let Some(vault) = built_in_vault {
407        if !vault.is_sealed() {
408            tracing::info!("Secrets provider chain: built-in vault (active)");
409            providers.push(Arc::new(AegisVaultProvider::new(vault)));
410        } else {
411            tracing::info!("Built-in vault is sealed; skipping as secrets provider");
412        }
413    }
414
415    // Second priority: external HashiCorp Vault
416    if let Some(vault) = VaultSecretsProvider::from_env() {
417        if let Err(e) = vault.authenticate().await {
418            tracing::warn!("External Vault authentication failed: {}.", e);
419        } else {
420            if let Err(e) = vault.load_secrets().await {
421                tracing::warn!("Failed to load secrets from external Vault: {}.", e);
422            }
423            tracing::info!("Secrets provider chain: +external Vault");
424            providers.push(Arc::new(vault));
425        }
426    }
427
428    // Always fall back to environment variables
429    providers.push(Arc::new(EnvSecretsProvider));
430    tracing::info!(
431        "Secrets provider chain: +environment variables ({} providers total)",
432        providers.len()
433    );
434
435    SecretsManager::new(providers)
436}
437
438/// Standard secret keys used by Aegis.
439pub mod keys {
440    /// Admin username for initial setup
441    pub const ADMIN_USERNAME: &str = "AEGIS_ADMIN_USERNAME";
442    /// Admin password for initial setup
443    pub const ADMIN_PASSWORD: &str = "AEGIS_ADMIN_PASSWORD";
444    /// Admin email for initial setup
445    pub const ADMIN_EMAIL: &str = "AEGIS_ADMIN_EMAIL";
446    /// TLS certificate path
447    pub const TLS_CERT_PATH: &str = "AEGIS_TLS_CERT";
448    /// TLS private key path
449    pub const TLS_KEY_PATH: &str = "AEGIS_TLS_KEY";
450    /// Cluster TLS CA certificate path
451    pub const CLUSTER_CA_CERT_PATH: &str = "AEGIS_CLUSTER_CA_CERT";
452    /// Cluster TLS client certificate path (for mTLS)
453    pub const CLUSTER_CLIENT_CERT_PATH: &str = "AEGIS_CLUSTER_CLIENT_CERT";
454    /// Cluster TLS client key path (for mTLS)
455    pub const CLUSTER_CLIENT_KEY_PATH: &str = "AEGIS_CLUSTER_CLIENT_KEY";
456    /// Database encryption key
457    pub const ENCRYPTION_KEY: &str = "AEGIS_ENCRYPTION_KEY";
458    /// JWT signing secret
459    pub const JWT_SECRET: &str = "AEGIS_JWT_SECRET";
460    /// LDAP bind password
461    pub const LDAP_BIND_PASSWORD: &str = "AEGIS_LDAP_BIND_PASSWORD";
462    /// OAuth2 client secret
463    pub const OAUTH_CLIENT_SECRET: &str = "AEGIS_OAUTH_CLIENT_SECRET";
464}
465
466// =============================================================================
467// Tests
468// =============================================================================
469
470#[cfg(test)]
471mod tests {
472    use super::*;
473
474    #[test]
475    fn test_env_provider() {
476        std::env::set_var("TEST_SECRET_KEY", "test_value");
477        let provider = EnvSecretsProvider;
478        assert_eq!(
479            provider.get("TEST_SECRET_KEY"),
480            Some("test_value".to_string())
481        );
482        assert_eq!(provider.get("NONEXISTENT_KEY"), None);
483        std::env::remove_var("TEST_SECRET_KEY");
484    }
485
486    #[test]
487    fn test_secrets_manager_fallback() {
488        std::env::set_var("TEST_FALLBACK_KEY", "fallback_value");
489        let manager = SecretsManager::env_only();
490        assert_eq!(
491            manager.get("TEST_FALLBACK_KEY"),
492            Some("fallback_value".to_string())
493        );
494        std::env::remove_var("TEST_FALLBACK_KEY");
495    }
496
497    #[test]
498    fn test_get_or_default() {
499        let provider = EnvSecretsProvider;
500        assert_eq!(provider.get_or("NONEXISTENT", "default"), "default");
501    }
502
503    #[test]
504    fn test_vault_config_from_env() {
505        // Clear any existing vars
506        std::env::remove_var("VAULT_ADDR");
507
508        let config = VaultConfig::default();
509        assert_eq!(config.address, "http://127.0.0.1:8200");
510        assert_eq!(config.mount_path, "secret");
511        assert_eq!(config.secret_path, "aegis");
512        assert!(config.tls_verify);
513    }
514}