@@ -17,6 +17,7 @@ use crate::backend::{
1717use crate :: config:: MountSpec ;
1818use crate :: error:: RunError ;
1919use firma_config_loader:: { CONFIG_DIR_NAME , CONFIG_FILE_NAME } ;
20+ use firma_runtime_state:: RunEntryLayout ;
2021
2122const BWRAP_ROOTFS_MODE_ENV : & str = "FIRMA_RUN_BWRAP_ROOTFS_MODE" ;
2223const BWRAP_RUNTIME_HOME_ENV : & str = "FIRMA_RUN_BWRAP_RUNTIME_HOME" ;
@@ -215,14 +216,22 @@ impl BwrapMountPlan {
215216 handle. runtime_dir. display( )
216217 ) ,
217218 } ) ?;
219+ // Derived from the runtime layout rather than the launch environment:
220+ // an operator-supplied `SSL_CERT_FILE` could otherwise aim the CA bind
221+ // at a signing key elsewhere in the control-plane runtime.
222+ let run_entry = runtime_layout. run_entry_layout ( & handle. identity . sandbox_id ) ;
223+ let runtime_paths = PlanRuntimePaths {
224+ control_plane : & control_plane_runtime,
225+ sandbox : & sandbox_runtime,
226+ run_entry : & run_entry,
227+ } ;
218228 let mounts = validate_mounts ( handle, & control_plane_runtime, & sandbox_runtime) ?;
219229 let mut plan = Self :: empty ( ) ;
220230 append_filesystem_layout (
221231 & mut plan,
222232 handle,
223233 & mounts,
224- & control_plane_runtime,
225- & sandbox_runtime,
234+ & runtime_paths,
226235 launch,
227236 hardening,
228237 ) ?;
@@ -373,8 +382,7 @@ fn append_filesystem_layout(
373382 plan : & mut BwrapMountPlan ,
374383 handle : & SandboxHandle ,
375384 mounts : & [ ValidatedMount ] ,
376- control_plane_runtime : & Path ,
377- sandbox_runtime : & Path ,
385+ runtime_paths : & PlanRuntimePaths < ' _ > ,
378386 launch : & LaunchSpec ,
379387 hardening : & BwrapHardening ,
380388) -> Result < ( ) , RunError > {
@@ -417,17 +425,36 @@ fn append_filesystem_layout(
417425 . collect :: < Vec < _ > > ( ) ;
418426 project_mount_aliases ( & mut plan. config_seals , & overlay_specs, masked) ;
419427
420- mask_control_plane_runtime ( plan, mounts, control_plane_runtime, sandbox_runtime, launch) ?;
428+ mask_control_plane_runtime (
429+ plan,
430+ mounts,
431+ runtime_paths. control_plane ,
432+ runtime_paths. sandbox ,
433+ launch,
434+ ) ?;
435+ mount_ca_material ( plan, runtime_paths. run_entry ) ;
421436 Ok ( ( ) )
422437}
423438
439+ /// Host paths that anchor one sandbox filesystem plan.
440+ struct PlanRuntimePaths < ' a > {
441+ /// Canonical control-plane runtime root (`FIRMA_STATE_DIR`).
442+ control_plane : & ' a Path ,
443+ /// Canonical private runtime for the sandbox being planned.
444+ sandbox : & ' a Path ,
445+ /// Layout of this run's entry inside the control-plane runtime.
446+ run_entry : & ' a RunEntryLayout ,
447+ }
448+
424449/// Hide host-side Firma runtime state from the wrapped process tree.
425450///
426451/// The read-only host-root bind prevents mutation but not disclosure. The
427452/// runtime root contains per-run Sidecar and Authority sockets, configuration,
428453/// metadata, signing keys, and capability seeds, none of which the wrapped
429454/// process needs. The sandbox-local bwrap runtime remains available separately
430- /// because the proxy bridge and egress guard require its sockets.
455+ /// because the proxy bridge and egress guard require its sockets, and
456+ /// [`mount_ca_material`] mounts the Sidecar's public CA material back over
457+ /// this mask.
431458fn mask_control_plane_runtime (
432459 plan : & mut BwrapMountPlan ,
433460 mounts : & [ ValidatedMount ] ,
@@ -473,6 +500,37 @@ fn mask_control_plane_runtime(
473500 Ok ( ( ) )
474501}
475502
503+ /// Mount the Sidecar's public CA material through the control-plane mask.
504+ ///
505+ /// [`mask_control_plane_runtime`] tmpfs-masks the whole control-plane runtime,
506+ /// but the launch environment points `SSL_CERT_FILE`, `CURL_CA_BUNDLE`,
507+ /// `REQUESTS_CA_BUNDLE`, `NODE_EXTRA_CA_CERTS`, and `GIT_SSL_CAINFO` at CA
508+ /// files inside it. Without this restoration the wrapped process opens an
509+ /// unreadable trust store, silently falls back to the system roots, and every
510+ /// MITM-intercepted handshake fails with `certificate signed by unknown
511+ /// authority`.
512+ ///
513+ /// Each file is bound individually and read-only. Binding
514+ /// [`RunEntryLayout::ca_dir`] as a directory would also expose
515+ /// [`RunEntryLayout::ca_key`] and let the wrapped process mint certificates
516+ /// trusted by anything configured to trust the Sidecar CA.
517+ ///
518+ /// Missing files are skipped: with HTTPS MITM disabled no CA is generated, and
519+ /// the bundle exists only under `ca_trust_mode = "append_system_roots"`.
520+ fn mount_ca_material ( plan : & mut BwrapMountPlan , run_entry : & RunEntryLayout ) {
521+ for source in [ run_entry. ca_cert ( ) , run_entry. ca_bundle ( ) ] {
522+ if !source. is_file ( ) {
523+ continue ;
524+ }
525+ plan. sandbox_runtime . bind (
526+ BwrapPlanRole :: SandboxInfrastructure ,
527+ source. clone ( ) ,
528+ source,
529+ BwrapBindMode :: ReadOnly ,
530+ ) ;
531+ }
532+ }
533+
476534/// Resolves every prepared mount to the exact host source that will be emitted
477535/// and enforces the source, target, and placement constraints associated with
478536/// its authority.
@@ -1380,6 +1438,165 @@ mod tests {
13801438 ) ;
13811439 }
13821440
1441+ /// Sandbox handle with no mounts, used by the CA restoration tests.
1442+ #[ cfg( target_os = "linux" ) ]
1443+ fn handle_with (
1444+ runtime_dir : std:: path:: PathBuf ,
1445+ identity : crate :: identity:: RunIdentity ,
1446+ ) -> crate :: backend:: SandboxHandle {
1447+ crate :: backend:: SandboxHandle {
1448+ backend : crate :: backend:: BackendKind :: Bwrap ,
1449+ runtime_dir,
1450+ identity,
1451+ mounts : vec ! [ ] ,
1452+ network_policy : crate :: config:: NetworkPolicy {
1453+ enforce_network_namespace : false ,
1454+ fail_closed : true ,
1455+ } ,
1456+ }
1457+ }
1458+
1459+ #[ test]
1460+ #[ cfg( target_os = "linux" ) ]
1461+ fn filesystem_layout_restores_public_ca_material_over_control_plane_mask ( ) {
1462+ // The control-plane mask hides the whole runtime root, but the launch
1463+ // environment aims `SSL_CERT_FILE` and friends at the run entry's CA.
1464+ // Each public file must be restored individually and after the mask;
1465+ // the signing key must stay hidden, so the CA directory is never bound
1466+ // as a whole.
1467+ let temp = tempfile:: tempdir ( ) . expect ( "tempdir" ) ;
1468+ let cwd = temp. path ( ) . join ( "workspace" ) ;
1469+ std:: fs:: create_dir_all ( & cwd) . expect ( "mkdir workspace" ) ;
1470+ let runtime_dir = temp. path ( ) . join ( "runtime" ) ;
1471+ std:: fs:: create_dir_all ( & runtime_dir) . expect ( "mkdir runtime" ) ;
1472+
1473+ let identity =
1474+ crate :: identity:: RunIdentity :: new ( crate :: identity:: test_agent_id ( ) , "generic" ) ;
1475+ let control_plane = temp. path ( ) . join ( "control-plane" ) ;
1476+ let runtime_layout = firma_runtime_state:: RuntimeLayout :: from_root ( control_plane. clone ( ) ) ;
1477+ let run_entry = runtime_layout. run_entry_layout ( & identity. sandbox_id ) ;
1478+ std:: fs:: create_dir_all ( run_entry. ca_dir ( ) ) . expect ( "mkdir CA dir" ) ;
1479+ for path in [
1480+ run_entry. ca_cert ( ) ,
1481+ run_entry. ca_bundle ( ) ,
1482+ run_entry. ca_key ( ) ,
1483+ ] {
1484+ std:: fs:: write ( & path, "" ) . expect ( "write CA material" ) ;
1485+ }
1486+
1487+ let handle = handle_with ( runtime_dir, identity) ;
1488+ let launch = launch_with_cwd_and_config ( cwd, None ) ;
1489+ let hardening = super :: BwrapHardening :: from_env ( & launch. env ) ;
1490+
1491+ let plan = super :: BwrapMountPlan :: build ( & runtime_layout, & handle, & launch, & hardening)
1492+ . expect ( "build mount plan" ) ;
1493+
1494+ let rendered = rendered_plan ( plan) ;
1495+ let mask = rendered
1496+ . iter ( )
1497+ . position ( |arg| arg == & canonical ( & control_plane) )
1498+ . expect ( "control-plane runtime masked" ) ;
1499+ for public in [ run_entry. ca_cert ( ) , run_entry. ca_bundle ( ) ] {
1500+ let path = public. display ( ) . to_string ( ) ;
1501+ let bind = rendered
1502+ . windows ( 3 )
1503+ . position ( |win| win[ 0 ] == "--ro-bind" && win[ 1 ] == path && win[ 2 ] == path)
1504+ . unwrap_or_else ( || panic ! ( "{path} bound read-only through the mask" ) ) ;
1505+ assert ! (
1506+ mask < bind,
1507+ "{path} must be restored after the control-plane mask"
1508+ ) ;
1509+ }
1510+ let key = run_entry. ca_key ( ) . display ( ) . to_string ( ) ;
1511+ assert ! (
1512+ !rendered. iter( ) . any( |arg| arg == & key) ,
1513+ "the CA signing key must never be exposed to the sandbox"
1514+ ) ;
1515+ let ca_dir = run_entry. ca_dir ( ) . display ( ) . to_string ( ) ;
1516+ assert ! (
1517+ !rendered. iter( ) . any( |arg| arg == & ca_dir) ,
1518+ "binding the CA directory would also expose the signing key"
1519+ ) ;
1520+ }
1521+
1522+ #[ test]
1523+ #[ cfg( target_os = "linux" ) ]
1524+ fn filesystem_layout_skips_absent_ca_material ( ) {
1525+ // With HTTPS MITM disabled no CA is generated, and the bundle exists
1526+ // only under `ca_trust_mode = "append_system_roots"`. Binding a missing
1527+ // source would make bwrap abort the launch.
1528+ let temp = tempfile:: tempdir ( ) . expect ( "tempdir" ) ;
1529+ let cwd = temp. path ( ) . join ( "workspace" ) ;
1530+ std:: fs:: create_dir_all ( & cwd) . expect ( "mkdir workspace" ) ;
1531+ let runtime_dir = temp. path ( ) . join ( "runtime" ) ;
1532+ std:: fs:: create_dir_all ( & runtime_dir) . expect ( "mkdir runtime" ) ;
1533+
1534+ let identity =
1535+ crate :: identity:: RunIdentity :: new ( crate :: identity:: test_agent_id ( ) , "generic" ) ;
1536+ let runtime_layout =
1537+ firma_runtime_state:: RuntimeLayout :: from_root ( temp. path ( ) . join ( "control-plane" ) ) ;
1538+ let run_entry = runtime_layout. run_entry_layout ( & identity. sandbox_id ) ;
1539+ std:: fs:: create_dir_all ( run_entry. root ( ) ) . expect ( "mkdir run entry" ) ;
1540+
1541+ let handle = handle_with ( runtime_dir, identity) ;
1542+ let launch = launch_with_cwd_and_config ( cwd, None ) ;
1543+ let hardening = super :: BwrapHardening :: from_env ( & launch. env ) ;
1544+
1545+ let plan = super :: BwrapMountPlan :: build ( & runtime_layout, & handle, & launch, & hardening)
1546+ . expect ( "build mount plan" ) ;
1547+
1548+ let rendered = rendered_plan ( plan) ;
1549+ let ca_dir = run_entry. ca_dir ( ) . display ( ) . to_string ( ) ;
1550+ assert ! (
1551+ !rendered. iter( ) . any( |arg| arg. starts_with( & ca_dir) ) ,
1552+ "no CA path should be bound when the material is absent: {rendered:?}"
1553+ ) ;
1554+ }
1555+
1556+ #[ test]
1557+ #[ cfg( target_os = "linux" ) ]
1558+ fn filesystem_layout_restores_only_the_current_run_ca ( ) {
1559+ // Run entries are siblings under `<runtime>/run`. A concurrent run's CA
1560+ // must stay behind the mask: the plan is keyed on this sandbox's
1561+ // identity, not on whatever CA files happen to exist.
1562+ let temp = tempfile:: tempdir ( ) . expect ( "tempdir" ) ;
1563+ let cwd = temp. path ( ) . join ( "workspace" ) ;
1564+ std:: fs:: create_dir_all ( & cwd) . expect ( "mkdir workspace" ) ;
1565+ let runtime_dir = temp. path ( ) . join ( "runtime" ) ;
1566+ std:: fs:: create_dir_all ( & runtime_dir) . expect ( "mkdir runtime" ) ;
1567+
1568+ let identity =
1569+ crate :: identity:: RunIdentity :: new ( crate :: identity:: test_agent_id ( ) , "generic" ) ;
1570+ let other = crate :: identity:: RunIdentity :: new ( crate :: identity:: test_agent_id ( ) , "generic" ) ;
1571+ let runtime_layout =
1572+ firma_runtime_state:: RuntimeLayout :: from_root ( temp. path ( ) . join ( "control-plane" ) ) ;
1573+ let run_entry = runtime_layout. run_entry_layout ( & identity. sandbox_id ) ;
1574+ let other_entry = runtime_layout. run_entry_layout ( & other. sandbox_id ) ;
1575+ for entry in [ & run_entry, & other_entry] {
1576+ std:: fs:: create_dir_all ( entry. ca_dir ( ) ) . expect ( "mkdir CA dir" ) ;
1577+ std:: fs:: write ( entry. ca_cert ( ) , "" ) . expect ( "write CA cert" ) ;
1578+ }
1579+
1580+ let handle = handle_with ( runtime_dir, identity) ;
1581+ let launch = launch_with_cwd_and_config ( cwd, None ) ;
1582+ let hardening = super :: BwrapHardening :: from_env ( & launch. env ) ;
1583+
1584+ let plan = super :: BwrapMountPlan :: build ( & runtime_layout, & handle, & launch, & hardening)
1585+ . expect ( "build mount plan" ) ;
1586+
1587+ let rendered = rendered_plan ( plan) ;
1588+ let own_cert = run_entry. ca_cert ( ) . display ( ) . to_string ( ) ;
1589+ let other_cert = other_entry. ca_cert ( ) . display ( ) . to_string ( ) ;
1590+ assert ! (
1591+ rendered. iter( ) . any( |arg| arg == & own_cert) ,
1592+ "this run's CA certificate must be restored"
1593+ ) ;
1594+ assert ! (
1595+ !rendered. iter( ) . any( |arg| arg == & other_cert) ,
1596+ "another run's CA certificate must stay behind the mask"
1597+ ) ;
1598+ }
1599+
13831600 #[ test]
13841601 #[ cfg( target_os = "linux" ) ]
13851602 fn mask_firma_dir_masks_home_firma_outside_cwd_ancestry ( ) {
0 commit comments