Skip to main content

microsandbox_network/secrets/
config.rs

1//! Secret injection configuration types.
2
3use serde::{Deserialize, Serialize};
4
5//--------------------------------------------------------------------------------------------------
6// Types
7//--------------------------------------------------------------------------------------------------
8
9/// Configuration for secret injection in a sandbox.
10#[derive(Debug, Clone, Default, Serialize, Deserialize)]
11pub struct SecretsConfig {
12    /// List of secrets to inject.
13    #[serde(default)]
14    pub secrets: Vec<SecretEntry>,
15
16    /// Action on secret violation (placeholder leaked to disallowed host).
17    #[serde(default)]
18    pub on_violation: ViolationAction,
19}
20
21/// A single secret entry (serializable form passed to the network engine).
22#[derive(Clone, Serialize, Deserialize)]
23pub struct SecretEntry {
24    /// Environment variable name exposed to the sandbox (holds the placeholder).
25    pub env_var: String,
26
27    /// The actual secret value (never enters the sandbox).
28    pub value: String,
29
30    /// Placeholder string the sandbox sees instead of the real value.
31    pub placeholder: String,
32
33    /// Hosts allowed to receive this secret.
34    #[serde(default)]
35    pub allowed_hosts: Vec<HostPattern>,
36
37    /// Where the secret can be injected.
38    #[serde(default)]
39    pub injection: SecretInjection,
40
41    /// Require verified TLS identity before substituting (default: true).
42    /// When true, secret is only substituted if the connection uses TLS
43    /// interception (not bypass) and the SNI matches an allowed host.
44    #[serde(default = "default_true")]
45    pub require_tls_identity: bool,
46}
47
48/// Host pattern for secret allowlist.
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub enum HostPattern {
51    /// Exact hostname match.
52    Exact(String),
53    /// Wildcard match (e.g., `*.openai.com`).
54    Wildcard(String),
55    /// Any host (dangerous — secret can be exfiltrated).
56    Any,
57}
58
59/// Where in the HTTP request the secret can be injected.
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct SecretInjection {
62    /// Substitute in HTTP headers (default: true).
63    #[serde(default = "default_true")]
64    pub headers: bool,
65
66    /// Substitute in HTTP Basic Auth (default: true).
67    #[serde(default = "default_true")]
68    pub basic_auth: bool,
69
70    /// Substitute in URL query parameters (default: false).
71    #[serde(default)]
72    pub query_params: bool,
73
74    /// Substitute in request body (default: false).
75    #[serde(default)]
76    pub body: bool,
77}
78
79/// Action when a secret placeholder is detected going to a disallowed host.
80#[derive(Debug, Clone, Default, Serialize, Deserialize)]
81pub enum ViolationAction {
82    /// Block the request silently.
83    Block,
84    /// Block and log (default).
85    #[default]
86    BlockAndLog,
87    /// Block and terminate the sandbox.
88    BlockAndTerminate,
89}
90
91//--------------------------------------------------------------------------------------------------
92// Methods
93//--------------------------------------------------------------------------------------------------
94
95impl std::fmt::Debug for SecretEntry {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        f.debug_struct("SecretEntry")
98            .field("env_var", &self.env_var)
99            .field("value", &"[REDACTED]")
100            .field("placeholder", &self.placeholder)
101            .field("allowed_hosts", &self.allowed_hosts)
102            .field("injection", &self.injection)
103            .field("require_tls_identity", &self.require_tls_identity)
104            .finish()
105    }
106}
107
108impl HostPattern {
109    /// Check if a hostname matches this pattern.
110    ///
111    /// Uses ASCII case-insensitive comparison to avoid `to_lowercase()`
112    /// allocations (DNS hostnames are ASCII per RFC 4343).
113    pub fn matches(&self, hostname: &str) -> bool {
114        match self {
115            HostPattern::Exact(h) => hostname.eq_ignore_ascii_case(h),
116            HostPattern::Wildcard(pattern) => {
117                if let Some(suffix) = pattern.strip_prefix("*.") {
118                    hostname.eq_ignore_ascii_case(suffix)
119                        || (hostname.len() > suffix.len() + 1
120                            && hostname.as_bytes()[hostname.len() - suffix.len() - 1] == b'.'
121                            && hostname[hostname.len() - suffix.len()..]
122                                .eq_ignore_ascii_case(suffix))
123                } else {
124                    hostname.eq_ignore_ascii_case(pattern)
125                }
126            }
127            HostPattern::Any => true,
128        }
129    }
130}
131
132//--------------------------------------------------------------------------------------------------
133// Trait Implementations
134//--------------------------------------------------------------------------------------------------
135
136impl Default for SecretInjection {
137    fn default() -> Self {
138        Self {
139            headers: true,
140            basic_auth: true,
141            query_params: false,
142            body: false,
143        }
144    }
145}
146
147//--------------------------------------------------------------------------------------------------
148// Functions
149//--------------------------------------------------------------------------------------------------
150
151fn default_true() -> bool {
152    true
153}
154
155//--------------------------------------------------------------------------------------------------
156// Tests
157//--------------------------------------------------------------------------------------------------
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn exact_host_match() {
165        let p = HostPattern::Exact("api.openai.com".into());
166        assert!(p.matches("api.openai.com"));
167        assert!(p.matches("API.OpenAI.com"));
168        assert!(!p.matches("evil.com"));
169    }
170
171    #[test]
172    fn wildcard_host_match() {
173        let p = HostPattern::Wildcard("*.openai.com".into());
174        assert!(p.matches("api.openai.com"));
175        assert!(p.matches("openai.com"));
176        assert!(!p.matches("evil.com"));
177    }
178
179    #[test]
180    fn any_host_match() {
181        let p = HostPattern::Any;
182        assert!(p.matches("anything.com"));
183    }
184
185    #[test]
186    fn default_injection_scopes() {
187        let inj = SecretInjection::default();
188        assert!(inj.headers);
189        assert!(inj.basic_auth);
190        assert!(!inj.query_params);
191        assert!(!inj.body);
192    }
193
194    #[test]
195    fn default_require_tls_identity() {
196        let entry = SecretEntry {
197            env_var: "K".into(),
198            value: "v".into(),
199            placeholder: "$K".into(),
200            allowed_hosts: vec![],
201            injection: SecretInjection::default(),
202            require_tls_identity: true,
203        };
204        assert!(entry.require_tls_identity);
205    }
206}