Skip to content

Commit 252882f

Browse files
authored
feat(providers): serve sandbox config files on demand (NVIDIA#3832)
* feat(providers): serve sandbox config files on demand Signed-off-by: Drew Newberry <anewberry@nvidia.com> * docs(providers): defer managed file documentation Signed-off-by: Drew Newberry <anewberry@nvidia.com> * docs(providers): mark managed file api experimental Signed-off-by: Drew Newberry <anewberry@nvidia.com> * style(go): format provider profile fields Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(providers): preserve legacy environment with managed files Signed-off-by: Drew Newberry <anewberry@nvidia.com> --------- Signed-off-by: Drew Newberry <anewberry@nvidia.com>
1 parent 5c0c9e4 commit 252882f

26 files changed

Lines changed: 2264 additions & 953 deletions

File tree

‎crates/openshell-core/src/grpc_client.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1116,6 +1116,7 @@ fn provider_environment_result(
11161116
.collect::<Result<HashMap<_, _>>>()?;
11171117
Ok(ProviderEnvironmentResult {
11181118
environment: inner.environment,
1119+
files: inner.files,
11191120
provider_env_revision: inner.provider_env_revision,
11201121
provider_attachment_epoch: inner.provider_attachment_epoch,
11211122
policy_hash: inner.policy_hash,
@@ -1380,6 +1381,7 @@ mod settings_poll_tests {
13801381
/// Credential material and the authority snapshot that produced its bindings.
13811382
pub struct ProviderEnvironmentResult {
13821383
pub environment: HashMap<String, String>,
1384+
pub files: HashMap<String, String>,
13831385
pub provider_env_revision: u64,
13841386
/// Attachment identity captured with the delivered credential records.
13851387
pub provider_attachment_epoch: String,

‎crates/openshell-core/src/provider_credentials.rs‎

Lines changed: 59 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,14 @@ pub struct ChildEnvironmentSnapshot {
3636
pub revision: u64,
3737
/// Prepared environment used by future workload processes.
3838
pub environment: HashMap<String, String>,
39+
/// Complete desired set of non-secret files for this installation.
40+
pub files: HashMap<String, String>,
3941
}
4042

4143
#[derive(Debug)]
4244
struct ProviderCredentialStateInner {
4345
current: Arc<ProviderCredentialSnapshot>,
46+
files: HashMap<String, String>,
4447
generations: VecDeque<Arc<SecretResolver>>,
4548
current_resolver: Option<Arc<SecretResolver>>,
4649
combined_resolver: Option<Arc<SecretResolver>>,
@@ -117,6 +120,7 @@ impl ProviderCredentialState {
117120
Self {
118121
inner: Arc::new(RwLock::new(ProviderCredentialStateInner {
119122
current: snapshot.clone(),
123+
files: HashMap::new(),
120124
generations,
121125
current_resolver,
122126
combined_resolver,
@@ -172,6 +176,7 @@ impl ProviderCredentialState {
172176
Ok(Self {
173177
inner: Arc::new(RwLock::new(ProviderCredentialStateInner {
174178
current: snapshot.clone(),
179+
files: HashMap::new(),
175180
generations,
176181
current_resolver,
177182
combined_resolver,
@@ -205,6 +210,7 @@ impl ProviderCredentialState {
205210
Self {
206211
inner: Arc::new(RwLock::new(ProviderCredentialStateInner {
207212
current: snapshot.clone(),
213+
files: HashMap::new(),
208214
generations: VecDeque::new(),
209215
current_resolver: None,
210216
combined_resolver: None,
@@ -410,22 +416,19 @@ impl ProviderCredentialState {
410416
inner.current = Arc::new(env);
411417
}
412418

413-
/// Return `child_env` with GCP static config vars resolved to real values.
419+
/// Return `child_env` with explicitly non-secret config vars resolved.
414420
///
415-
/// The credential pipeline placeholderizes ALL env values, but GCP SDKs
416-
/// and coding agents read certain vars (project ID, region, metadata host)
417-
/// at process startup before any HTTP request flows through the proxy.
418-
/// This method overrides those vars with resolved real values while
419-
/// keeping secret credentials (like `GCP_ACCESS_TOKEN`) as placeholders.
421+
/// The credential pipeline placeholderizes all env values. Workloads need
422+
/// non-secret configuration, including provider file paths, at process
423+
/// startup before any HTTP request flows through the proxy. Credential
424+
/// values remain placeholders.
420425
///
421426
/// Three layers of env var injection:
422427
/// 1. **Synthetic vars** (`GCE_METADATA_IP`, `METADATA_SERVER_DETECTION`)
423428
/// — sandbox-internal config not from user
424429
/// input, inserted directly here with real values.
425-
/// 2. **`google_cloud::STATIC_CONFIG_KEYS`** — user-provided non-secret config
426-
/// (project ID, region, SA email) that was placeholderized by
427-
/// `ProviderPlugin::inject_env` → `SecretResolver`; un-placeholderized
428-
/// here so SDKs can read them at startup.
430+
/// 2. **Classified non-secret keys** — user-provided config and provider
431+
/// file paths un-placeholderized so workloads can read them at startup.
429432
/// 3. Everything else stays as placeholders for proxy-time resolution.
430433
pub fn child_env_with_gcp_resolved(&self) -> HashMap<String, String> {
431434
let inner = self
@@ -463,9 +466,18 @@ impl ProviderCredentialState {
463466
installation_id: inner.current.installation_id.clone(),
464467
revision,
465468
environment,
469+
files: inner.files.clone(),
466470
})
467471
}
468472

473+
/// Attach the complete file set to a prepared provider snapshot.
474+
pub fn set_managed_files(&self, files: HashMap<String, String>) {
475+
self.inner
476+
.write()
477+
.expect("provider credential state poisoned")
478+
.files = files;
479+
}
480+
469481
fn resolve_child_env_snapshot(
470482
inner: &ProviderCredentialStateInner,
471483
) -> (u64, HashMap<String, String>) {
@@ -477,11 +489,7 @@ impl ProviderCredentialState {
477489
&& inner
478490
.non_secret_environment_keys
479491
.contains("GCE_METADATA_HOST");
480-
let has_gcp_config = google_cloud::STATIC_CONFIG_KEYS
481-
.iter()
482-
.any(|key| env.contains_key(*key) && inner.non_secret_environment_keys.contains(*key));
483-
484-
if !has_gcp_metadata && !has_gcp_config {
492+
if !has_gcp_metadata && inner.non_secret_environment_keys.is_empty() {
485493
return (inner.current.revision, env);
486494
}
487495

@@ -506,16 +514,15 @@ impl ProviderCredentialState {
506514
);
507515
}
508516

509-
// Un-placeholderize non-secret config vars so SDKs can read them
510-
// at process startup before any HTTP flows through the proxy.
517+
// Only explicitly classified non-secret values may be unwrapped.
511518
if let Some(ref resolver) = inner.combined_resolver {
512-
for key in google_cloud::STATIC_CONFIG_KEYS
513-
.iter()
514-
.filter(|key| inner.non_secret_environment_keys.contains(**key))
515-
{
519+
for key in &inner.non_secret_environment_keys {
520+
if !env.contains_key(key) || (has_gcp_metadata && key == "GCE_METADATA_HOST") {
521+
continue;
522+
}
516523
let placeholder = crate::secrets::placeholder_for_env_key(key);
517524
if let Some(value) = resolver.resolve_placeholder(&placeholder) {
518-
env.insert(key.to_string(), value.to_string());
525+
env.insert(key.clone(), value.to_string());
519526
}
520527
}
521528
}
@@ -738,7 +745,7 @@ impl ProviderCredentialState {
738745
pub fn install_prepared(&self, prepared: &Self) -> usize {
739746
// Release the candidate lock before taking the live lock, including
740747
// when a caller passes another handle to the same state.
741-
let (snapshot, generations, current_resolver, bindings, non_secret_keys) = {
748+
let (snapshot, generations, current_resolver, bindings, non_secret_keys, files) = {
742749
let candidate = prepared
743750
.inner
744751
.read()
@@ -749,6 +756,7 @@ impl ProviderCredentialState {
749756
candidate.current_resolver.clone(),
750757
candidate.static_credential_bindings.clone(),
751758
candidate.non_secret_environment_keys.clone(),
759+
candidate.files.clone(),
752760
)
753761
};
754762
let mut inner = self
@@ -781,6 +789,7 @@ impl ProviderCredentialState {
781789
);
782790
inner.static_credential_bindings = bindings;
783791
inner.non_secret_environment_keys = non_secret_keys;
792+
inner.files = files;
784793
inner.body_inventory_available = true;
785794
inner
786795
.known_body_keys
@@ -2138,6 +2147,33 @@ mod tests {
21382147
assert!(!env.contains_key("CLAUDE_CODE_USE_VERTEX"));
21392148
}
21402149

2150+
#[test]
2151+
fn child_env_resolves_provider_file_path_but_keeps_credentials_placeholderized() {
2152+
let path = "/run/openshell/providers/acme/client.toml";
2153+
let state = ProviderCredentialState::from_bound_environment(
2154+
1,
2155+
HashMap::from([
2156+
("ACME_CONFIG_FILE".to_string(), path.to_string()),
2157+
("ACME_TOKEN".to_string(), "secret-token".to_string()),
2158+
]),
2159+
HashMap::new(),
2160+
HashMap::new(),
2161+
HashMap::from([(
2162+
"ACME_TOKEN".to_string(),
2163+
binding("api.acme.example", 443, "/**"),
2164+
)]),
2165+
vec!["ACME_CONFIG_FILE".to_string()],
2166+
)
2167+
.expect("classified provider environment");
2168+
2169+
let env = state.child_env_with_gcp_resolved();
2170+
assert_eq!(env.get("ACME_CONFIG_FILE").map(String::as_str), Some(path));
2171+
assert_eq!(
2172+
env.get("ACME_TOKEN").map(String::as_str),
2173+
Some("openshell:resolve:env:v1_ACME_TOKEN")
2174+
);
2175+
}
2176+
21412177
#[test]
21422178
fn child_env_with_gcp_resolved_overrides_gcp_static_vars() {
21432179
let state = ProviderCredentialState::from_bound_environment(

‎crates/openshell-isolation-interface/src/linux/seccomp_notify.rs‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,8 @@ pub fn install_listener(syscalls: &[i64]) -> io::Result<NotificationListener> {
432432
/// reconfigure an INET endpoint. Connected `send()`/null-destination
433433
/// `sendto()` retains the audited cBPF fast path.
434434
pub fn install_workload_listener() -> io::Result<NotificationListener> {
435-
install_listener(&[
435+
#[allow(unused_mut)] // SYS_open is unavailable on some architectures.
436+
let mut syscalls = vec![
436437
libc::SYS_socket,
437438
libc::SYS_connect,
438439
libc::SYS_bind,
@@ -447,7 +448,12 @@ pub fn install_workload_listener() -> io::Result<NotificationListener> {
447448
libc::SYS_kill,
448449
libc::SYS_tkill,
449450
libc::SYS_rt_sigqueueinfo,
450-
])
451+
libc::SYS_openat,
452+
libc::SYS_openat2,
453+
];
454+
#[cfg(target_arch = "x86_64")]
455+
syscalls.push(libc::SYS_open);
456+
install_listener(&syscalls)
451457
}
452458

453459
/// Run a no-capability conformance probe.

‎crates/openshell-isolation-interface/src/linux/workload_launcher.rs‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,27 @@ mod tests {
137137
})
138138
.expect("launcher result")
139139
.expect("spawn child");
140-
let notification = listener.receive().expect("receive child socket");
140+
let notification = loop {
141+
let notification = listener.receive().expect("receive child syscall");
142+
let syscall = i64::from(notification.syscall);
143+
if syscall == libc::SYS_socket {
144+
break notification;
145+
}
146+
if syscall == libc::SYS_openat || syscall == libc::SYS_openat2 {
147+
listener
148+
.respond_continue(notification.id)
149+
.expect("continue ordinary file open");
150+
continue;
151+
}
152+
#[cfg(target_arch = "x86_64")]
153+
if syscall == libc::SYS_open {
154+
listener
155+
.respond_continue(notification.id)
156+
.expect("continue ordinary file open");
157+
continue;
158+
}
159+
panic!("unexpected child syscall: {syscall}");
160+
};
141161
assert_eq!(i64::from(notification.syscall), libc::SYS_socket);
142162
assert!(
143163
std::path::Path::new(&format!("/proc/{}/task/{}", child.id(), notification.tid))

‎crates/openshell-providers/src/discovery.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ mod tests {
9292

9393
fn profile() -> ProviderTypeProfile {
9494
ProviderTypeProfile {
95+
files: Vec::new(),
9596
id: "custom".to_string(),
9697
resource_version: 0,
9798
annotations: std::collections::HashMap::new(),

0 commit comments

Comments
 (0)