Skip to main content

tatara_engine/secrets/
mod.rs

1//! Secret provider abstraction — fetches secrets from external vaults
2//! at allocation time. Values are never stored in Raft state.
3
4pub mod env;
5pub mod sops;
6
7use anyhow::Result;
8use async_trait::async_trait;
9use serde::{Deserialize, Serialize};
10use tatara_core::domain::secret::{SecretProvider, SecretRef};
11use tracing::{debug, info};
12
13/// A fetched secret value (zeroized on drop if the zeroize feature is enabled).
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct SecretValue {
16    pub value: String,
17    pub version: String,
18}
19
20/// Trait for secret provider backends.
21#[async_trait]
22pub trait SecretFetcher: Send + Sync {
23    /// Fetch a secret by its provider-specific key.
24    async fn fetch(&self, key: &str) -> Result<SecretValue>;
25
26    /// Check if a secret has been rotated since the given version.
27    async fn needs_rotation(&self, key: &str, current_version: &str) -> Result<bool> {
28        let current = self.fetch(key).await?;
29        Ok(current.version != current_version)
30    }
31}
32
33/// Resolve a set of secret refs into env vars and file mounts.
34pub struct SecretResolver {
35    env_fetcher: env::EnvSecretFetcher,
36    sops_fetcher: sops::SopsSecretFetcher,
37}
38
39impl SecretResolver {
40    pub fn new() -> Self {
41        Self {
42            env_fetcher: env::EnvSecretFetcher,
43            sops_fetcher: sops::SopsSecretFetcher,
44        }
45    }
46
47    /// Resolve all secret refs, returning env vars to inject and files to write.
48    pub async fn resolve(&self, refs: &[SecretRef]) -> Result<ResolvedSecrets> {
49        let mut env_vars = std::collections::HashMap::new();
50        let mut files = Vec::new();
51
52        for secret_ref in refs {
53            let fetcher: &dyn SecretFetcher = match secret_ref.provider {
54                SecretProvider::Env => &self.env_fetcher,
55                SecretProvider::Sops => &self.sops_fetcher,
56                SecretProvider::Akeyless => {
57                    // Akeyless provider would be wired here when available
58                    anyhow::bail!("Akeyless provider not yet configured");
59                }
60            };
61
62            let value = fetcher.fetch(&secret_ref.key).await?;
63            debug!(name = %secret_ref.name, provider = ?secret_ref.provider, "resolved secret");
64
65            if let Some(env_var) = &secret_ref.env_var {
66                env_vars.insert(env_var.clone(), value.value.clone());
67            }
68
69            if let Some(mount_path) = &secret_ref.mount_path {
70                files.push(SecretFile {
71                    path: mount_path.clone(),
72                    content: value.value.clone(),
73                });
74            }
75        }
76
77        info!(
78            env_count = env_vars.len(),
79            file_count = files.len(),
80            "secrets resolved"
81        );
82        Ok(ResolvedSecrets { env_vars, files })
83    }
84}
85
86impl Default for SecretResolver {
87    fn default() -> Self {
88        Self::new()
89    }
90}
91
92/// Resolved secrets ready for injection into a task.
93#[derive(Debug, Default)]
94pub struct ResolvedSecrets {
95    /// Environment variables to inject.
96    pub env_vars: std::collections::HashMap<String, String>,
97    /// Files to write into the allocation directory.
98    pub files: Vec<SecretFile>,
99}
100
101/// A secret file to write.
102#[derive(Debug)]
103pub struct SecretFile {
104    pub path: String,
105    pub content: String,
106}