Skip to content

Commit dfafdb3

Browse files
author
Marco Napetti
committed
feat: adversarial review
1 parent e4484aa commit dfafdb3

8 files changed

Lines changed: 448 additions & 128 deletions

File tree

.github/workflows/installer-smoke.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ jobs:
3636
./install.sh --install-dir "${INSTALL_DIR}"
3737
"${INSTALL_DIR}/firma" --version
3838
"${INSTALL_DIR}/firma" help config
39+
test -x "${INSTALL_DIR}/firma-secret-shim"
3940
- name: Re-run (already installed exit path)
4041
env:
4142
FIRMA_NO_BREW: '1'
@@ -81,6 +82,7 @@ jobs:
8182
.\install.ps1 -InstallDir $dir
8283
& (Join-Path $dir 'firma.exe') --version
8384
& (Join-Path $dir 'firma.exe') help config
85+
if (-not (Test-Path (Join-Path $dir 'firma-secret-shim.exe'))) { exit 1 }
8486
- name: Re-run (already installed exit path)
8587
shell: pwsh
8688
env:

crates/firma-run/src/config.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,22 @@ pub(crate) fn resolve_profile_with_layout(
691691
);
692692
}
693693

694+
// CLI secret providers are mediated by bind-mounting the
695+
// `firma-secret-shim` binary over the real tool and routing it to a
696+
// host-side broker socket. Only the Linux bwrap backend implements both
697+
// the mount and the host-socket reachability; on any other backend the
698+
// real tool would run unmediated, so fail closed instead.
699+
let has_cli_providers = resolved
700+
.secret_providers
701+
.values()
702+
.any(|spec| spec.as_cli().is_some());
703+
if has_cli_providers && resolved.backend != BackendKind::Bwrap {
704+
return Err(RunError::ConfigValidation(format!(
705+
"secret_providers with a CLI entry is unsupported for backend '{}'; CLI secret mediation requires backend 'bwrap'. Remove the CLI entries or use an HTTP provider",
706+
resolved.backend
707+
)));
708+
}
709+
694710
resolved.validate()?;
695711
Ok(resolved)
696712
}
@@ -2862,6 +2878,59 @@ fail_closed = true
28622878
);
28632879
}
28642880

2881+
#[test]
2882+
fn cli_secret_providers_on_non_bwrap_backend_are_rejected() {
2883+
let tmpdir = tempfile::tempdir().unwrap();
2884+
let config_path = tmpdir.path().join(CONFIG_FILE_NAME);
2885+
let backend = non_bwrap_backend_for_current_host();
2886+
let toml = format!(
2887+
r#"
2888+
[run.profiles.generic]
2889+
backend = "{backend}"
2890+
2891+
[run.defaults]
2892+
secret_providers = ["bws"]
2893+
"#
2894+
);
2895+
fs::write(&config_path, toml).unwrap();
2896+
2897+
let mut run_args = args("generic");
2898+
run_args.config = Some(config_path);
2899+
2900+
let error = resolve_profile(&run_args).expect_err("expected validation error");
2901+
assert!(
2902+
error.to_string().contains("CLI secret mediation requires backend 'bwrap'"),
2903+
"unexpected error: {error}"
2904+
);
2905+
}
2906+
2907+
#[test]
2908+
fn http_secret_providers_are_allowed_on_non_bwrap_backends() {
2909+
let tmpdir = tempfile::tempdir().unwrap();
2910+
let config_path = tmpdir.path().join(CONFIG_FILE_NAME);
2911+
let backend = non_bwrap_backend_for_current_host();
2912+
let toml = format!(
2913+
r#"
2914+
[run.profiles.generic]
2915+
backend = "{backend}"
2916+
2917+
[run.defaults]
2918+
secret_providers = [
2919+
{{ type = "http", provider_id = "aws-secrets-manager", host = "secretsmanager.*.amazonaws.com", matchers = [
2920+
{{ type = "sensitive_command", path = "/GetSecretValue", matcher = {{ type = "json", record_path = "$", value_path = "$.SecretString", name = {{ source = "path", path = "$.Name" }} }} }},
2921+
] }},
2922+
]
2923+
"#
2924+
);
2925+
fs::write(&config_path, toml).unwrap();
2926+
2927+
let mut run_args = args("generic");
2928+
run_args.config = Some(config_path);
2929+
2930+
let resolved = resolve_profile(&run_args).expect("HTTP provider must not be backend-gated");
2931+
assert!(resolved.secret_providers.contains_key("aws-secrets-manager"));
2932+
}
2933+
28652934
#[test]
28662935
#[cfg_attr(not(target_os = "linux"), ignore = "bwrap-only")]
28672936
fn seccomp_policy_resolves_when_configured_for_bwrap() {

crates/firma-run/src/runtime/mod.rs

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ pub mod vscode;
3030

3131
/// Secret-mediation shim injection (Unix/bwrap). Wires the broker + shim mounts
3232
/// into a launch when the profile lists `secret_providers`.
33-
mod secret_shims;
33+
pub mod secret_shims;
3434

3535
/// Lib-level input for [`execute_run`]. The CLI layer (in the `firma`
3636
/// host crate) builds this from its `clap`-derived args struct.
@@ -159,6 +159,11 @@ pub fn execute_run(args: &RunInput, hooks: &LaunchHooks<'_>) -> Result<i32, RunE
159159
profile: profile.clone(),
160160
working_dir: working_dir.clone(),
161161
})?);
162+
// Secret services (gateway + broker) for this run. Assigned once the
163+
// profile's providers are known inside the run closure below; dropped
164+
// only after sandbox teardown so the agent can resolve placeholders for
165+
// its whole lifetime while the plaintext store still cannot outlive the run.
166+
let mut secret_services = None;
162167

163168
let run_result = (|| {
164169
let handle_ref = handle
@@ -237,10 +242,21 @@ pub fn execute_run(args: &RunInput, hooks: &LaunchHooks<'_>) -> Result<i32, RunE
237242
let firma_exe = env::current_exe()
238243
.map_err(|e| RunError::Internal(format!("resolve current_exe: {e}")))?;
239244
// Start the secret services before the Sidecar so its gateway address
240-
// is available while the Sidecar config is synthesized.
241-
let gateway_binding = secret_shims::pre_bind_gateway(handle_ref, &profile)?;
242-
if let Some(binding) = &gateway_binding {
243-
flags.secret_gateway_addr = Some(binding.addr.clone());
245+
// is available while the Sidecar config is synthesized. The guard is
246+
// held for the whole run: dropping it stops the gateway and broker
247+
// and removes their sockets.
248+
secret_services = if profile.secret_providers.is_empty() {
249+
None
250+
} else {
251+
Some(secret_shims::SecretServices::start(
252+
&runtime_layout,
253+
handle_ref,
254+
&identity,
255+
&profile,
256+
)?)
257+
};
258+
if let Some(services) = &secret_services {
259+
flags.secret_gateway_addr = Some(services.gateway_addr.clone());
244260
}
245261
let mut prompt = crate::authority::StdAuthorityPrompt;
246262
let authority = crate::routing::resolve_authority(
@@ -340,7 +356,7 @@ pub fn execute_run(args: &RunInput, hooks: &LaunchHooks<'_>) -> Result<i32, RunE
340356
&mut env,
341357
&firma_exe,
342358
env::var_os("PATH").as_deref(),
343-
gateway_binding.as_ref(),
359+
secret_services.as_ref(),
344360
)?;
345361
let launch = LaunchSpec {
346362
executable,
@@ -382,6 +398,11 @@ pub fn execute_run(args: &RunInput, hooks: &LaunchHooks<'_>) -> Result<i32, RunE
382398
.take()
383399
.map_or(Ok(()), |real_handle| backend.teardown(real_handle));
384400

401+
// Stop the secret services after sandbox teardown so the agent can still
402+
// resolve placeholders while it exits, but before the run returns: the
403+
// gateway socket is removed and the plaintext per-run store is dropped.
404+
drop(secret_services);
405+
385406
combine_run_and_teardown_results(run_result, teardown_result)
386407
}
387408

0 commit comments

Comments
 (0)