Skip to main content

yeti_types/
secrets.rs

1//! Pluggable secrets backend interface — ADR-014 §5.
2//!
3//! The host owns the secrets interface; customers reference secrets by name
4//! (`vault://prod/tls/kafka`, `file://kafka-prod`). The secrets backend
5//! resolves references to opaque handles consumed by `transport.tls`.
6
7/// Zeroizing secret string — debug output is redacted.
8#[derive(Clone)]
9pub struct SecretString(String);
10
11impl SecretString {
12    /// Create from a plain string value.
13    #[must_use]
14    pub fn new(value: impl Into<String>) -> Self {
15        Self(value.into())
16    }
17
18    /// Read the secret value (use minimally; zero the variable afterward).
19    #[must_use]
20    pub fn expose(&self) -> &str {
21        &self.0
22    }
23}
24
25impl std::fmt::Debug for SecretString {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        write!(f, "SecretString([REDACTED])")
28    }
29}
30
31// TODO(YTC-430): use the `zeroize` crate to clear secret bytes on drop instead
32// of relying on Rust's default dealloc (which may leave copies in freed pages).
33// Blocked on adding `zeroize = { workspace = true }` to the dep tree.
34// Standard Drop for String is used until then — safe for a dev secrets backend,
35// but should be hardened before production secrets handling lands.
36
37/// Rotation notification emitted when a secret is rotated.
38#[derive(Debug, Clone)]
39pub struct RotationEvent {
40    /// Secret name.
41    pub name: String,
42    /// Tenant scope (empty = global).
43    pub tenant: String,
44    /// Unix timestamp of the rotation.
45    pub rotated_at: u64,
46}
47
48/// Opaque TLS material handle — consumed by `transport.tls::connect_tls`.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
50pub struct TlsHandle(pub u64);
51
52/// Opaque credential handle — consumed by auth providers.
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
54pub struct CredentialHandle(pub u64);
55
56/// Pluggable secrets backend trait — ADR-014 §5.
57///
58/// Implemented by `plugin-secrets-vault` and `plugin-secrets-file`.
59/// The host registers one backend at startup; it resolves all secret
60/// references throughout the deployment.
61///
62/// `#[async_trait]` is used to make the trait dyn-compatible.
63#[async_trait::async_trait]
64pub trait SecretsBackend: Send + Sync + 'static {
65    /// Resolve a named TLS profile to an opaque handle.
66    /// `name` uses scheme-routing: `vault://prod/tls/kafka`, `file://kafka-prod`.
67    async fn resolve_tls_profile(
68        &self,
69        name: &str,
70        tenant: &str,
71    ) -> crate::error::Result<TlsHandle>;
72
73    /// Resolve a named credential.
74    async fn get_credential(
75        &self,
76        name: &str,
77        tenant: &str,
78    ) -> crate::error::Result<CredentialHandle>;
79
80    /// Read a named secret as a zeroizing string.
81    async fn get_string(&self, name: &str, tenant: &str) -> crate::error::Result<SecretString>;
82
83    /// Subscribe to rotation events. Implementations that don't support
84    /// rotation return an empty stream.
85    fn subscribe(
86        &self,
87        name: &str,
88        tenant: &str,
89    ) -> std::pin::Pin<Box<dyn futures::Stream<Item = RotationEvent> + Send>>;
90}
91
92/// No-op secrets backend used when no real backend is configured.
93///
94/// Returns `Err("no secrets backend configured")` for every call.
95/// Suitable for deployments that don't use raw transport or managed credentials.
96#[derive(Debug, Default)]
97pub struct NullSecretsBackend;
98
99#[async_trait::async_trait]
100impl SecretsBackend for NullSecretsBackend {
101    async fn resolve_tls_profile(
102        &self,
103        name: &str,
104        _tenant: &str,
105    ) -> crate::error::Result<TlsHandle> {
106        Err(crate::error::YetiError::Internal(format!(
107            "no secrets backend configured (requested: {name})"
108        )))
109    }
110
111    async fn get_credential(
112        &self,
113        name: &str,
114        _tenant: &str,
115    ) -> crate::error::Result<CredentialHandle> {
116        Err(crate::error::YetiError::Internal(format!(
117            "no secrets backend configured (requested: {name})"
118        )))
119    }
120
121    async fn get_string(&self, name: &str, _tenant: &str) -> crate::error::Result<SecretString> {
122        Err(crate::error::YetiError::Internal(format!(
123            "no secrets backend configured (requested: {name})"
124        )))
125    }
126
127    fn subscribe(
128        &self,
129        _name: &str,
130        _tenant: &str,
131    ) -> std::pin::Pin<Box<dyn futures::Stream<Item = RotationEvent> + Send>> {
132        Box::pin(futures::stream::empty())
133    }
134}