1use serde::{Deserialize, Serialize};
7use std::path::PathBuf;
8use std::time::Duration;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct SecretsConfig {
13 #[serde(flatten)]
15 pub backend: SecretsBackend,
16 #[serde(default)]
18 pub common: CommonSecretsConfig,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23#[serde(tag = "type", rename_all = "lowercase")]
24pub enum SecretsBackend {
25 Vault(VaultConfig),
27 File(FileConfig),
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct CommonSecretsConfig {
34 #[serde(default = "default_timeout")]
36 pub timeout_seconds: u64,
37 #[serde(default = "default_max_retries")]
39 pub max_retries: u32,
40 #[serde(default = "default_enable_cache")]
42 pub enable_cache: bool,
43 #[serde(default = "default_cache_ttl")]
45 pub cache_ttl_seconds: u64,
46 pub audit: Option<super::auditing::AuditConfig>,
48}
49
50impl Default for CommonSecretsConfig {
51 fn default() -> Self {
52 Self {
53 timeout_seconds: default_timeout(),
54 max_retries: default_max_retries(),
55 enable_cache: default_enable_cache(),
56 cache_ttl_seconds: default_cache_ttl(),
57 audit: None,
58 }
59 }
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct VaultConfig {
65 pub url: String,
67 pub auth: VaultAuthConfig,
69 pub namespace: Option<String>,
71 #[serde(default = "default_vault_mount")]
73 pub mount_path: String,
74 #[serde(default = "default_vault_api_version")]
76 pub api_version: String,
77 #[serde(default)]
79 pub tls: VaultTlsConfig,
80 #[serde(default)]
82 pub connection: VaultConnectionConfig,
83}
84
85#[derive(Clone, Serialize, Deserialize)]
87#[serde(tag = "method", rename_all = "lowercase")]
88pub enum VaultAuthConfig {
89 Token {
91 token: String,
93 },
94 AppRole {
96 role_id: String,
98 secret_id: String,
100 #[serde(default = "default_approle_mount")]
102 mount_path: String,
103 },
104 Kubernetes {
106 #[serde(default = "default_k8s_token_path")]
108 token_path: String,
109 role: String,
111 #[serde(default = "default_k8s_mount")]
113 mount_path: String,
114 },
115 Aws {
117 region: String,
119 role: String,
121 #[serde(default = "default_aws_mount")]
123 mount_path: String,
124 },
125}
126
127impl std::fmt::Debug for VaultAuthConfig {
128 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129 match self {
130 VaultAuthConfig::Token { .. } => f
131 .debug_struct("VaultAuthConfig::Token")
132 .field("token", &"[REDACTED]")
133 .finish(),
134 VaultAuthConfig::AppRole {
135 role_id,
136 mount_path,
137 ..
138 } => f
139 .debug_struct("VaultAuthConfig::AppRole")
140 .field("role_id", role_id)
141 .field("secret_id", &"[REDACTED]")
142 .field("mount_path", mount_path)
143 .finish(),
144 VaultAuthConfig::Kubernetes {
145 token_path,
146 role,
147 mount_path,
148 } => f
149 .debug_struct("VaultAuthConfig::Kubernetes")
150 .field("token_path", token_path)
151 .field("role", role)
152 .field("mount_path", mount_path)
153 .finish(),
154 VaultAuthConfig::Aws {
155 region,
156 role,
157 mount_path,
158 } => f
159 .debug_struct("VaultAuthConfig::Aws")
160 .field("region", region)
161 .field("role", role)
162 .field("mount_path", mount_path)
163 .finish(),
164 }
165 }
166}
167
168impl std::fmt::Display for VaultAuthConfig {
173 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174 match self {
175 VaultAuthConfig::Token { .. } => write!(f, "VaultAuthConfig::Token(redacted)"),
176 VaultAuthConfig::AppRole { role_id, .. } => {
177 write!(f, "VaultAuthConfig::AppRole(role_id={})", role_id)
178 }
179 VaultAuthConfig::Kubernetes { role, .. } => {
180 write!(f, "VaultAuthConfig::Kubernetes(role={})", role)
181 }
182 VaultAuthConfig::Aws { region, role, .. } => {
183 write!(f, "VaultAuthConfig::Aws(region={}, role={})", region, role)
184 }
185 }
186 }
187}
188
189#[cfg(test)]
190mod vault_auth_redaction_tests {
191 use super::*;
192
193 #[test]
194 fn debug_redacts_token() {
195 let cfg = VaultAuthConfig::Token {
196 token: "s.VERY_SECRET_TOKEN_1234".to_string(),
197 };
198 let rendered = format!("{:?}", cfg);
199 assert!(!rendered.contains("VERY_SECRET_TOKEN"));
200 assert!(rendered.contains("[REDACTED]"));
201 }
202
203 #[test]
204 fn debug_redacts_approle_secret_id() {
205 let cfg = VaultAuthConfig::AppRole {
206 role_id: "pub-role".to_string(),
207 secret_id: "THIS_IS_VERY_SECRET".to_string(),
208 mount_path: "approle".to_string(),
209 };
210 let rendered = format!("{:?}", cfg);
211 assert!(!rendered.contains("THIS_IS_VERY_SECRET"));
212 assert!(rendered.contains("pub-role"));
213 assert!(rendered.contains("[REDACTED]"));
214 }
215
216 #[test]
217 fn display_does_not_leak_token() {
218 let cfg = VaultAuthConfig::Token {
219 token: "s.DISPLAY_SHOULD_NOT_PRINT_THIS".to_string(),
220 };
221 let rendered = format!("{}", cfg);
222 assert!(!rendered.contains("DISPLAY_SHOULD_NOT_PRINT_THIS"));
223 }
224
225 #[test]
226 fn display_does_not_leak_approle_secret() {
227 let cfg = VaultAuthConfig::AppRole {
228 role_id: "pub-role".to_string(),
229 secret_id: "SECRET_ID_DO_NOT_LEAK".to_string(),
230 mount_path: "approle".to_string(),
231 };
232 let rendered = format!("{}", cfg);
233 assert!(!rendered.contains("SECRET_ID_DO_NOT_LEAK"));
234 assert!(rendered.contains("pub-role"));
235 }
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize, Default)]
240pub struct VaultTlsConfig {
241 #[serde(default)]
249 pub skip_verify: bool,
250 pub ca_cert: Option<PathBuf>,
252 pub client_cert: Option<PathBuf>,
254 pub client_key: Option<PathBuf>,
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize)]
260pub struct VaultConnectionConfig {
261 #[serde(default = "default_max_connections")]
263 pub max_connections: usize,
264 #[serde(default = "default_connection_timeout")]
266 pub connection_timeout_seconds: u64,
267 #[serde(default = "default_request_timeout")]
269 pub request_timeout_seconds: u64,
270}
271
272impl Default for VaultConnectionConfig {
273 fn default() -> Self {
274 Self {
275 max_connections: default_max_connections(),
276 connection_timeout_seconds: default_connection_timeout(),
277 request_timeout_seconds: default_request_timeout(),
278 }
279 }
280}
281
282#[derive(Debug, Clone, Serialize, Deserialize)]
284pub struct FileConfig {
285 pub path: PathBuf,
287 #[serde(default = "default_file_format")]
289 pub format: FileFormat,
290 #[serde(default)]
292 pub encryption: FileEncryptionConfig,
293 pub permissions: Option<u32>,
295 #[serde(default)]
297 pub watch_for_changes: bool,
298 #[serde(default)]
300 pub backup: FileBackupConfig,
301}
302
303#[derive(Debug, Clone, Serialize, Deserialize)]
305#[serde(rename_all = "lowercase")]
306pub enum FileFormat {
307 Json,
309 Yaml,
311 Toml,
313 Env,
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize)]
319pub struct FileEncryptionConfig {
320 #[serde(default)]
322 pub enabled: bool,
323 #[serde(default = "default_encryption_algorithm")]
325 pub algorithm: String,
326 #[serde(default = "default_kdf")]
328 pub kdf: String,
329 #[serde(default)]
331 pub key: FileKeyConfig,
332}
333
334#[derive(Debug, Clone, Serialize, Deserialize)]
336pub struct FileKeyConfig {
337 #[serde(default = "default_key_provider")]
339 pub provider: String,
340 pub env_var: Option<String>,
342 pub service: Option<String>,
344 pub account: Option<String>,
346 pub file_path: Option<PathBuf>,
348}
349
350impl Default for FileKeyConfig {
351 fn default() -> Self {
352 Self {
353 provider: default_key_provider(),
354 env_var: None,
355 service: None,
356 account: None,
357 file_path: None,
358 }
359 }
360}
361
362impl Default for FileEncryptionConfig {
363 fn default() -> Self {
364 Self {
365 enabled: false,
366 algorithm: default_encryption_algorithm(),
367 kdf: default_kdf(),
368 key: FileKeyConfig::default(),
369 }
370 }
371}
372
373#[derive(Debug, Clone, Serialize, Deserialize)]
375pub struct FileBackupConfig {
376 #[serde(default)]
378 pub enabled: bool,
379 pub backup_dir: Option<PathBuf>,
381 #[serde(default = "default_max_backups")]
383 pub max_backups: usize,
384 #[serde(default = "default_backup_before_write")]
386 pub backup_before_write: bool,
387}
388
389impl Default for FileBackupConfig {
390 fn default() -> Self {
391 Self {
392 enabled: false,
393 backup_dir: None,
394 max_backups: default_max_backups(),
395 backup_before_write: default_backup_before_write(),
396 }
397 }
398}
399
400fn default_timeout() -> u64 {
402 30
403}
404fn default_max_retries() -> u32 {
405 3
406}
407fn default_enable_cache() -> bool {
408 true
409}
410fn default_cache_ttl() -> u64 {
411 300
412}
413fn default_vault_mount() -> String {
414 "secret".to_string()
415}
416fn default_vault_api_version() -> String {
417 "v2".to_string()
418}
419fn default_approle_mount() -> String {
420 "approle".to_string()
421}
422fn default_k8s_token_path() -> String {
423 "/var/run/secrets/kubernetes.io/serviceaccount/token".to_string()
424}
425fn default_k8s_mount() -> String {
426 "kubernetes".to_string()
427}
428fn default_aws_mount() -> String {
429 "aws".to_string()
430}
431fn default_max_connections() -> usize {
432 10
433}
434fn default_connection_timeout() -> u64 {
435 10
436}
437fn default_request_timeout() -> u64 {
438 30
439}
440fn default_file_format() -> FileFormat {
441 FileFormat::Json
442}
443fn default_encryption_algorithm() -> String {
444 "AES-256-GCM".to_string()
445}
446fn default_kdf() -> String {
447 "PBKDF2".to_string()
448}
449fn default_key_provider() -> String {
450 "env".to_string()
451}
452fn default_max_backups() -> usize {
453 5
454}
455fn default_backup_before_write() -> bool {
456 true
457}
458
459impl SecretsConfig {
460 pub fn vault_with_token(url: String, token: String) -> Self {
462 Self {
463 backend: SecretsBackend::Vault(VaultConfig {
464 url,
465 auth: VaultAuthConfig::Token { token },
466 namespace: None,
467 mount_path: default_vault_mount(),
468 api_version: default_vault_api_version(),
469 tls: VaultTlsConfig::default(),
470 connection: VaultConnectionConfig::default(),
471 }),
472 common: CommonSecretsConfig::default(),
473 }
474 }
475
476 pub fn file_json(path: PathBuf) -> Self {
478 Self {
479 backend: SecretsBackend::File(FileConfig {
480 path,
481 format: FileFormat::Json,
482 encryption: FileEncryptionConfig::default(),
483 permissions: Some(0o600),
484 watch_for_changes: false,
485 backup: FileBackupConfig::default(),
486 }),
487 common: CommonSecretsConfig::default(),
488 }
489 }
490
491 pub fn backend_type(&self) -> &'static str {
493 match &self.backend {
494 SecretsBackend::Vault(_) => "vault",
495 SecretsBackend::File(_) => "file",
496 }
497 }
498
499 pub fn timeout(&self) -> Duration {
501 Duration::from_secs(self.common.timeout_seconds)
502 }
503
504 pub fn cache_ttl(&self) -> Duration {
506 Duration::from_secs(self.common.cache_ttl_seconds)
507 }
508}
509
510#[cfg(test)]
511mod tests {
512 use super::*;
513
514 #[test]
515 fn test_vault_config_creation() {
516 let config = SecretsConfig::vault_with_token(
517 "https://vault.example.com".to_string(),
518 "hvs.token123".to_string(),
519 );
520
521 assert_eq!(config.backend_type(), "vault");
522 if let SecretsBackend::Vault(vault_config) = &config.backend {
523 assert_eq!(vault_config.url, "https://vault.example.com");
524 if let VaultAuthConfig::Token { token } = &vault_config.auth {
525 assert_eq!(token, "hvs.token123");
526 } else {
527 panic!("Expected token auth");
528 }
529 } else {
530 panic!("Expected vault backend");
531 }
532 }
533
534 #[test]
535 fn test_file_config_creation() {
536 let path = PathBuf::from("/etc/secrets/app.json");
537 let config = SecretsConfig::file_json(path.clone());
538
539 assert_eq!(config.backend_type(), "file");
540 if let SecretsBackend::File(file_config) = &config.backend {
541 assert_eq!(file_config.path, path);
542 assert!(matches!(file_config.format, FileFormat::Json));
543 } else {
544 panic!("Expected file backend");
545 }
546 }
547
548 #[test]
549 fn test_common_config_defaults() {
550 let config = CommonSecretsConfig::default();
551 assert_eq!(config.timeout_seconds, 30);
552 assert_eq!(config.max_retries, 3);
553 assert!(config.enable_cache);
554 assert_eq!(config.cache_ttl_seconds, 300);
555 }
556
557 #[test]
558 fn test_timeout_conversion() {
559 let config = SecretsConfig::file_json(PathBuf::from("/test"));
560 assert_eq!(config.timeout(), Duration::from_secs(30));
561 assert_eq!(config.cache_ttl(), Duration::from_secs(300));
562 }
563}