Skip to main content

symbi_runtime/secrets/
config.rs

1//! Configuration structures for secrets backends
2//!
3//! This module defines the configuration structures that can be deserialized
4//! from symbiont.toml for different secrets backend types.
5
6use serde::{Deserialize, Serialize};
7use std::path::PathBuf;
8use std::time::Duration;
9
10/// Configuration for secrets management
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct SecretsConfig {
13    /// The secrets backend configuration
14    #[serde(flatten)]
15    pub backend: SecretsBackend,
16    /// Common configuration options
17    #[serde(default)]
18    pub common: CommonSecretsConfig,
19}
20
21/// Enumeration of supported secrets backends
22#[derive(Debug, Clone, Serialize, Deserialize)]
23#[serde(tag = "type", rename_all = "lowercase")]
24pub enum SecretsBackend {
25    /// HashiCorp Vault backend
26    Vault(VaultConfig),
27    /// File-based secrets backend
28    File(FileConfig),
29}
30
31/// Common configuration options for all secrets backends
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct CommonSecretsConfig {
34    /// Timeout for secrets operations (in seconds)
35    #[serde(default = "default_timeout")]
36    pub timeout_seconds: u64,
37    /// Maximum number of retry attempts
38    #[serde(default = "default_max_retries")]
39    pub max_retries: u32,
40    /// Enable caching of secrets
41    #[serde(default = "default_enable_cache")]
42    pub enable_cache: bool,
43    /// Cache TTL in seconds
44    #[serde(default = "default_cache_ttl")]
45    pub cache_ttl_seconds: u64,
46    /// Audit configuration for secret operations
47    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/// Configuration for HashiCorp Vault backend
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct VaultConfig {
65    /// Vault server URL
66    pub url: String,
67    /// Authentication method configuration
68    pub auth: VaultAuthConfig,
69    /// Vault namespace (optional)
70    pub namespace: Option<String>,
71    /// Default mount path for KV secrets engine
72    #[serde(default = "default_vault_mount")]
73    pub mount_path: String,
74    /// API version for KV secrets engine (v1 or v2)
75    #[serde(default = "default_vault_api_version")]
76    pub api_version: String,
77    /// TLS configuration
78    #[serde(default)]
79    pub tls: VaultTlsConfig,
80    /// Connection pool settings
81    #[serde(default)]
82    pub connection: VaultConnectionConfig,
83}
84
85/// Vault authentication configuration
86#[derive(Clone, Serialize, Deserialize)]
87#[serde(tag = "method", rename_all = "lowercase")]
88pub enum VaultAuthConfig {
89    /// Token-based authentication
90    Token {
91        /// Vault token (can be from environment variable)
92        token: String,
93    },
94    /// AppRole authentication
95    AppRole {
96        /// Role ID
97        role_id: String,
98        /// Secret ID
99        secret_id: String,
100        /// Mount path for AppRole auth
101        #[serde(default = "default_approle_mount")]
102        mount_path: String,
103    },
104    /// Kubernetes authentication
105    Kubernetes {
106        /// Service account token path
107        #[serde(default = "default_k8s_token_path")]
108        token_path: String,
109        /// Kubernetes role
110        role: String,
111        /// Mount path for Kubernetes auth
112        #[serde(default = "default_k8s_mount")]
113        mount_path: String,
114    },
115    /// AWS IAM authentication
116    Aws {
117        /// AWS region
118        region: String,
119        /// Vault role name
120        role: String,
121        /// Mount path for AWS auth
122        #[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
168/// Vault TLS configuration
169#[derive(Debug, Clone, Serialize, Deserialize, Default)]
170pub struct VaultTlsConfig {
171    /// Skip TLS certificate verification (insecure)
172    #[serde(default)]
173    pub skip_verify: bool,
174    /// Path to CA certificate file
175    pub ca_cert: Option<PathBuf>,
176    /// Path to client certificate file
177    pub client_cert: Option<PathBuf>,
178    /// Path to client private key file
179    pub client_key: Option<PathBuf>,
180}
181
182/// Vault connection configuration
183#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct VaultConnectionConfig {
185    /// Maximum number of connections in the pool
186    #[serde(default = "default_max_connections")]
187    pub max_connections: usize,
188    /// Connection timeout in seconds
189    #[serde(default = "default_connection_timeout")]
190    pub connection_timeout_seconds: u64,
191    /// Request timeout in seconds
192    #[serde(default = "default_request_timeout")]
193    pub request_timeout_seconds: u64,
194}
195
196impl Default for VaultConnectionConfig {
197    fn default() -> Self {
198        Self {
199            max_connections: default_max_connections(),
200            connection_timeout_seconds: default_connection_timeout(),
201            request_timeout_seconds: default_request_timeout(),
202        }
203    }
204}
205
206/// Configuration for file-based secrets backend
207#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct FileConfig {
209    /// Path to the secrets file or directory
210    pub path: PathBuf,
211    /// File format for secrets storage
212    #[serde(default = "default_file_format")]
213    pub format: FileFormat,
214    /// Encryption configuration
215    #[serde(default)]
216    pub encryption: FileEncryptionConfig,
217    /// File permissions (Unix only)
218    pub permissions: Option<u32>,
219    /// Watch for file changes and reload
220    #[serde(default)]
221    pub watch_for_changes: bool,
222    /// Backup configuration
223    #[serde(default)]
224    pub backup: FileBackupConfig,
225}
226
227/// Supported file formats for secrets storage
228#[derive(Debug, Clone, Serialize, Deserialize)]
229#[serde(rename_all = "lowercase")]
230pub enum FileFormat {
231    /// JSON format
232    Json,
233    /// YAML format
234    Yaml,
235    /// TOML format
236    Toml,
237    /// Plain text (key=value pairs)
238    Env,
239}
240
241/// File encryption configuration
242#[derive(Debug, Clone, Serialize, Deserialize)]
243pub struct FileEncryptionConfig {
244    /// Enable encryption of secrets file
245    #[serde(default)]
246    pub enabled: bool,
247    /// Encryption algorithm
248    #[serde(default = "default_encryption_algorithm")]
249    pub algorithm: String,
250    /// Key derivation function
251    #[serde(default = "default_kdf")]
252    pub kdf: String,
253    /// Key provider configuration
254    #[serde(default)]
255    pub key: FileKeyConfig,
256}
257
258/// Configuration for encryption key retrieval
259#[derive(Debug, Clone, Serialize, Deserialize)]
260pub struct FileKeyConfig {
261    /// Key provider type
262    #[serde(default = "default_key_provider")]
263    pub provider: String,
264    /// Environment variable containing encryption key (for 'env' provider)
265    pub env_var: Option<String>,
266    /// Keychain service name (for 'os_keychain' provider)
267    pub service: Option<String>,
268    /// Keychain account name (for 'os_keychain' provider)
269    pub account: Option<String>,
270    /// Path to key file (for 'file' provider)
271    pub file_path: Option<PathBuf>,
272}
273
274impl Default for FileKeyConfig {
275    fn default() -> Self {
276        Self {
277            provider: default_key_provider(),
278            env_var: None,
279            service: None,
280            account: None,
281            file_path: None,
282        }
283    }
284}
285
286impl Default for FileEncryptionConfig {
287    fn default() -> Self {
288        Self {
289            enabled: false,
290            algorithm: default_encryption_algorithm(),
291            kdf: default_kdf(),
292            key: FileKeyConfig::default(),
293        }
294    }
295}
296
297/// File backup configuration
298#[derive(Debug, Clone, Serialize, Deserialize)]
299pub struct FileBackupConfig {
300    /// Enable automatic backups
301    #[serde(default)]
302    pub enabled: bool,
303    /// Directory for backup files
304    pub backup_dir: Option<PathBuf>,
305    /// Maximum number of backup files to keep
306    #[serde(default = "default_max_backups")]
307    pub max_backups: usize,
308    /// Create backup before modifications
309    #[serde(default = "default_backup_before_write")]
310    pub backup_before_write: bool,
311}
312
313impl Default for FileBackupConfig {
314    fn default() -> Self {
315        Self {
316            enabled: false,
317            backup_dir: None,
318            max_backups: default_max_backups(),
319            backup_before_write: default_backup_before_write(),
320        }
321    }
322}
323
324// Default value functions
325fn default_timeout() -> u64 {
326    30
327}
328fn default_max_retries() -> u32 {
329    3
330}
331fn default_enable_cache() -> bool {
332    true
333}
334fn default_cache_ttl() -> u64 {
335    300
336}
337fn default_vault_mount() -> String {
338    "secret".to_string()
339}
340fn default_vault_api_version() -> String {
341    "v2".to_string()
342}
343fn default_approle_mount() -> String {
344    "approle".to_string()
345}
346fn default_k8s_token_path() -> String {
347    "/var/run/secrets/kubernetes.io/serviceaccount/token".to_string()
348}
349fn default_k8s_mount() -> String {
350    "kubernetes".to_string()
351}
352fn default_aws_mount() -> String {
353    "aws".to_string()
354}
355fn default_max_connections() -> usize {
356    10
357}
358fn default_connection_timeout() -> u64 {
359    10
360}
361fn default_request_timeout() -> u64 {
362    30
363}
364fn default_file_format() -> FileFormat {
365    FileFormat::Json
366}
367fn default_encryption_algorithm() -> String {
368    "AES-256-GCM".to_string()
369}
370fn default_kdf() -> String {
371    "PBKDF2".to_string()
372}
373fn default_key_provider() -> String {
374    "env".to_string()
375}
376fn default_max_backups() -> usize {
377    5
378}
379fn default_backup_before_write() -> bool {
380    true
381}
382
383impl SecretsConfig {
384    /// Create a Vault configuration with token authentication
385    pub fn vault_with_token(url: String, token: String) -> Self {
386        Self {
387            backend: SecretsBackend::Vault(VaultConfig {
388                url,
389                auth: VaultAuthConfig::Token { token },
390                namespace: None,
391                mount_path: default_vault_mount(),
392                api_version: default_vault_api_version(),
393                tls: VaultTlsConfig::default(),
394                connection: VaultConnectionConfig::default(),
395            }),
396            common: CommonSecretsConfig::default(),
397        }
398    }
399
400    /// Create a file-based configuration with JSON format
401    pub fn file_json(path: PathBuf) -> Self {
402        Self {
403            backend: SecretsBackend::File(FileConfig {
404                path,
405                format: FileFormat::Json,
406                encryption: FileEncryptionConfig::default(),
407                permissions: Some(0o600),
408                watch_for_changes: false,
409                backup: FileBackupConfig::default(),
410            }),
411            common: CommonSecretsConfig::default(),
412        }
413    }
414
415    /// Get the backend type as a string
416    pub fn backend_type(&self) -> &'static str {
417        match &self.backend {
418            SecretsBackend::Vault(_) => "vault",
419            SecretsBackend::File(_) => "file",
420        }
421    }
422
423    /// Get timeout as Duration
424    pub fn timeout(&self) -> Duration {
425        Duration::from_secs(self.common.timeout_seconds)
426    }
427
428    /// Get cache TTL as Duration
429    pub fn cache_ttl(&self) -> Duration {
430        Duration::from_secs(self.common.cache_ttl_seconds)
431    }
432}
433
434#[cfg(test)]
435mod tests {
436    use super::*;
437
438    #[test]
439    fn test_vault_config_creation() {
440        let config = SecretsConfig::vault_with_token(
441            "https://vault.example.com".to_string(),
442            "hvs.token123".to_string(),
443        );
444
445        assert_eq!(config.backend_type(), "vault");
446        if let SecretsBackend::Vault(vault_config) = &config.backend {
447            assert_eq!(vault_config.url, "https://vault.example.com");
448            if let VaultAuthConfig::Token { token } = &vault_config.auth {
449                assert_eq!(token, "hvs.token123");
450            } else {
451                panic!("Expected token auth");
452            }
453        } else {
454            panic!("Expected vault backend");
455        }
456    }
457
458    #[test]
459    fn test_file_config_creation() {
460        let path = PathBuf::from("/etc/secrets/app.json");
461        let config = SecretsConfig::file_json(path.clone());
462
463        assert_eq!(config.backend_type(), "file");
464        if let SecretsBackend::File(file_config) = &config.backend {
465            assert_eq!(file_config.path, path);
466            assert!(matches!(file_config.format, FileFormat::Json));
467        } else {
468            panic!("Expected file backend");
469        }
470    }
471
472    #[test]
473    fn test_common_config_defaults() {
474        let config = CommonSecretsConfig::default();
475        assert_eq!(config.timeout_seconds, 30);
476        assert_eq!(config.max_retries, 3);
477        assert!(config.enable_cache);
478        assert_eq!(config.cache_ttl_seconds, 300);
479    }
480
481    #[test]
482    fn test_timeout_conversion() {
483        let config = SecretsConfig::file_json(PathBuf::from("/test"));
484        assert_eq!(config.timeout(), Duration::from_secs(30));
485        assert_eq!(config.cache_ttl(), Duration::from_secs(300));
486    }
487}