1use parking_lot::RwLock;
9use std::collections::HashMap;
10use std::sync::Arc;
11
12pub trait SecretsProvider: Send + Sync {
18 fn get(&self, key: &str) -> Option<String>;
20
21 fn get_or(&self, key: &str, default: &str) -> String {
23 self.get(key).unwrap_or_else(|| default.to_string())
24 }
25
26 fn exists(&self, key: &str) -> bool {
28 self.get(key).is_some()
29 }
30}
31
32#[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#[derive(Debug, Clone)]
52pub struct VaultConfig {
53 pub address: String,
55 pub token: Option<String>,
57 pub role_id: Option<String>,
59 pub secret_id: Option<String>,
61 pub k8s_role: Option<String>,
63 pub mount_path: String,
65 pub secret_path: String,
67 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
89pub 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 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 pub fn from_env() -> Option<Self> {
117 let config = VaultConfig::default();
118
119 if std::env::var("VAULT_ADDR").is_err() {
121 return None;
122 }
123
124 Some(Self::new(config))
125 }
126
127 pub async fn authenticate(&self) -> Result<(), String> {
129 if self.token.read().is_some() {
131 return Ok(());
132 }
133
134 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 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 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 async fn auth_kubernetes(&self, role: &str) -> Result<(), String> {
188 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 pub async fn read_secret(&self, key: &str) -> Result<String, String> {
228 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 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 self.cache.write().insert(key.to_string(), value.clone());
269
270 Ok(value)
271 }
272
273 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 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 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 if let Some(value) = self.get_cached(key) {
327 return Some(value);
328 }
329
330 std::env::var(key).ok()
332 }
333}
334
335pub struct SecretsManager {
341 providers: Vec<Arc<dyn SecretsProvider>>,
342}
343
344impl SecretsManager {
345 pub fn new(providers: Vec<Arc<dyn SecretsProvider>>) -> Self {
348 Self { providers }
349 }
350
351 pub fn env_only() -> Self {
353 Self {
354 providers: vec![Arc::new(EnvSecretsProvider)],
355 }
356 }
357
358 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
377pub 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
398pub 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 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 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 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
438pub mod keys {
440 pub const ADMIN_USERNAME: &str = "AEGIS_ADMIN_USERNAME";
442 pub const ADMIN_PASSWORD: &str = "AEGIS_ADMIN_PASSWORD";
444 pub const ADMIN_EMAIL: &str = "AEGIS_ADMIN_EMAIL";
446 pub const TLS_CERT_PATH: &str = "AEGIS_TLS_CERT";
448 pub const TLS_KEY_PATH: &str = "AEGIS_TLS_KEY";
450 pub const CLUSTER_CA_CERT_PATH: &str = "AEGIS_CLUSTER_CA_CERT";
452 pub const CLUSTER_CLIENT_CERT_PATH: &str = "AEGIS_CLUSTER_CLIENT_CERT";
454 pub const CLUSTER_CLIENT_KEY_PATH: &str = "AEGIS_CLUSTER_CLIENT_KEY";
456 pub const ENCRYPTION_KEY: &str = "AEGIS_ENCRYPTION_KEY";
458 pub const JWT_SECRET: &str = "AEGIS_JWT_SECRET";
460 pub const LDAP_BIND_PASSWORD: &str = "AEGIS_LDAP_BIND_PASSWORD";
462 pub const OAUTH_CLIENT_SECRET: &str = "AEGIS_OAUTH_CLIENT_SECRET";
464}
465
466#[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 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}