diff --git a/AGENTS.md b/AGENTS.md index 3d06456..d64f6e6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,7 +24,7 @@ If blocked, pick the simplest viable default, record it in `DECISIONS.md`, and m `CONSTITUTION.md` holds the full invariants; do not modify it without a `DECISIONS.md` entry. No "temporary hacks" that violate it. ## Active track -- **Now:** S12.4 HIL appliance v0 physical loop (serial observer first, then power/reset actuation), feeding S13 metal HIL graduation. S14 USB xHCI + HID is deferred to a design pass. +- **Now:** S12.4 HIL appliance v0 physical loop (serial observer first, then power/reset actuation), feeding the preferred appliance-mediated S13 metal HIL graduation path. Standalone golden-machine `PASS/METAL` must be provenance-stamped separately. S14 USB xHCI + HID is deferred to a design pass. - **Authoritative pair:** `CURRENT_STATUS.md` + `NEXT_TASKS.md` (deferred decisions in `ROADMAP.md` §13). `SLICES.md` has slice history. - **Keep green:** `just s11`, `just s12`, `just s13`, and `just foundry-org-governance-g0` when touching org/research planning. diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f234fb..35c9926 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ ## [Unreleased] ### Changed +- Made HIL metal evidence path-disclosing: per-gate HIL JSON now carries + `claim_path` plus appliance metadata, and docs distinguish standalone + `operator-golden-machine` PASS/METAL from `appliance-mediated` graduation. +- Corrected POSIX runner security claims to match the default + host-portable-rlimits-only runtime profile; seccomp/chroot/namespace helpers + remain tested but are not default-wired. - Reworked the documentation entry points so `CURRENT_STATUS.md` records landed state, `NEXT_TASKS.md` is an executable queue, and `ROADMAP.md` stays directional. @@ -14,6 +20,8 @@ S13, HIL appliance, security, risk, and contributor documentation. ### Added +- Added Foundry checks for per-gate HIL `claim_path` evidence and for the POSIX + runner default sandbox profile contract. - G0 RamenOrg / research-backed OS scaffold: - Added `docs/plans/2026-06-23-research-backed-ramenorg.md` to make RamenOrg and research-backed development first-class planning tracks without displacing S12.4/S13 HIL work. - Added `docs/org/` with RamenOrg constitution, role charter, authority levels, heartbeats, `WorkOrderV0`, `HandoffPacketV0`, `BoardVoteV0`, and claim safety. diff --git a/CURRENT_STATUS.md b/CURRENT_STATUS.md index 0d06169..f57f57b 100644 --- a/CURRENT_STATUS.md +++ b/CURRENT_STATUS.md @@ -7,9 +7,11 @@ ## Active Execution Track S12.4 is building the physical HIL appliance loop: serial observation first, -then power/reset actuation. Once that loop is stable, S13 metal HIL graduation -runs through the appliance on Tier-1 or lab hardware. S14 USB xHCI and HID stays -deferred until the appliance loop is proven. +then power/reset actuation. Once that loop is stable, the preferred S13 metal +HIL graduation path runs through the appliance on Tier-1 or lab hardware. +Standalone golden-machine graduation remains a distinct `PASS/METAL` path only +when per-gate evidence stamps `claim_path: operator-golden-machine`. S14 USB +xHCI and HID stays deferred until the appliance loop is proven. The next executable step is maintained in [NEXT_TASKS.md](NEXT_TASKS.md). Medium-range sequencing and deferred decisions live in [ROADMAP.md](ROADMAP.md). @@ -39,6 +41,9 @@ see [EVIDENCE_LEVELS.md](EVIDENCE_LEVELS.md). - S12.4.0 HIL appliance manifest, evidence wrapper, and inventory gate. - S12.4.1 serial-observer scaffold with run-id validation, empty-transcript rejection, and replay/live evidence separation. +- Per-gate HIL evidence now stamps `claim_path` and appliance metadata so + standalone golden-machine runs cannot be mistaken for appliance-mediated + graduation. - S13.0 persistent-storage contract and `harness.block` IDL. - S13.2-S13.5 virtio-blk Oracle capture and replay scoreboards. - S13.6 runtime `harness.block` sector I/O in QEMU. diff --git a/DECISIONS.md b/DECISIONS.md index be08a60..229f26d 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1,6 +1,6 @@ # DECISIONS (ADR-lite) -**Last Updated:** 2026-06-23 +**Last Updated:** 2026-06-24 **Status:** Active ## 2026-02-03 — Monorepo with hard boundaries @@ -170,6 +170,44 @@ Manual reboot/capture workflows do not scale to agentic OS development or hardwa **CI policy:** Default CI validates docs/manifests only. Hardware controller checks require `RAMEN_HIL_APPLIANCE=1`. Graduation runs require `RAMEN_HIL_APPLIANCE=1 RAMEN_HIL_GRADUATION=1` and disallow stale serial-log replay. +## 2026-06-24 — HIL metal claims must disclose the run path +The evidence taxonomy allows standalone Tier-1 golden-machine `PASS/METAL` +when `RAMEN_HIL_GRADUATION=1`, live serial capture, target-emitted +`hil_evidence:` markers, and per-gate evidence JSON are present. The active +S12.4/S13 execution queue still prefers appliance-mediated graduation before +claiming the S13 slice complete. + +**Chosen:** Keep standalone golden-machine graduation legitimate, but make the +path machine-readable. Per-gate HIL JSON now carries `claim_path`: +`operator-golden-machine` for standalone live graduation and +`appliance-mediated` when `RAMEN_HIL_APPLIANCE=1`. Appliance-mediated runs also +include an `appliance` object with controller identity and evidence references. + +**Rejected:** Silently treating every `PASS/METAL` as appliance-mediated, or +requiring the appliance for all future `PASS/METAL` claims before S12.4.2 power +actuation has landed. + +**Gate:** `just hil-appliance` validates both claim paths in per-gate evidence +fixtures. + +## 2026-06-24 — POSIX runner default profile is rlimits-only +The POSIX compatibility runner remains a development scaffold. Its default +`posix_run_v0_sandboxed` profile uses a host-portable configuration: +`seccomp=false`, `namespaces=false`, `chroot=false`, and `rlimits=true`. +Seccomp, namespace, and chroot helpers remain implemented and tested in +`sandbox.rs`, but are not wired into the default runner path because they are +not portable on unprivileged CI hosts. + +**Chosen:** Report the actual profile in runtime logs and documentation instead +of claiming full sandbox containment. Keep `RAMEN_POSIX_RUNNER_ACK_RISK=1` as +the explicit execution gate and track full default sandboxing as future work. + +**Rejected:** Enabling seccomp/chroot/namespaces in this honesty patch without a +new portable pre-exec design and gate-first rollout. + +**Gate:** `just foundry-s7-posix-runner-security` now checks the logged default +profile and a unit contract for the rlimits-only configuration. + ## 2026-06-23 — RamenOrg starts as an Org Kernel, not an ambient AI board The project already uses agents heavily, but the founder is still often acting as the message bus between planning, implementation, review, evidence, and status diff --git a/EVIDENCE_LEVELS.md b/EVIDENCE_LEVELS.md index 7f2a189..3af0d75 100644 --- a/EVIDENCE_LEVELS.md +++ b/EVIDENCE_LEVELS.md @@ -1,6 +1,6 @@ # Evidence Levels -**Last Updated:** 2026-06-22 +**Last Updated:** 2026-06-24 **Status:** Authoritative for HIL gate reporting Foundry gates may print `PASS`, but **PASS is not one thing**. Use these levels in gates, docs, and evidence JSON. @@ -13,7 +13,7 @@ Foundry gates may print `PASS`, but **PASS is not one thing**. Use these levels | `PASS/HIL-LOG` | Operator-provided serial log replay (`RAMEN_HIL_SERIAL_LOG`) | Development convenience; **not** metal graduation | | `PASS/HIL-LIVE` | Serial captured from `RAMEN_HIL_SERIAL_DEV` during this gate run | Lab evidence; still weaker than graduation | | `PASS/HIL-APPLIANCE` | Live serial captured by the appliance plus controller power/reset transcript | Appliance-mediated lab evidence; bridge toward autonomous CI | -| `PASS/METAL` | `RAMEN_HIL_GRADUATION=1` + live serial + `hil_evidence:` provenance markers + evidence JSON bundle | Tier-1 / golden-machine graduation | +| `PASS/METAL` | `RAMEN_HIL_GRADUATION=1` + live serial + `hil_evidence:` provenance markers + evidence JSON bundle with `claim_path` | Tier-1 / golden-machine graduation | ## Graduation mode @@ -23,14 +23,29 @@ Set for serious metal runs: export RAMEN_HIL_GOLDEN_MACHINE=1 export RAMEN_HIL_GRADUATION=1 export RAMEN_HIL_SERIAL_DEV=/dev/ttyUSB0 -# optional: export RAMEN_HIL_APPLIANCE=1 +# optional: export RAMEN_HIL_APPLIANCE=1 # stamps claim_path=appliance-mediated # optional: export RAMEN_HIL_APPLIANCE_ID=pi-hil-01 # optional: export RAMEN_HIL_MACHINE_ID=amd-ryzen-lab # default: intel-nuc-12-reference ``` `RAMEN_HIL_GRADUATION=1` **disallows** `RAMEN_HIL_SERIAL_LOG` (stale/copied logs). -When `RAMEN_HIL_APPLIANCE=1`, the evidence bundle must also include controller identity and power/reset events from `hardware/hil_appliance_v0.toml`. +Every HIL evidence JSON must include `claim_path` so a standalone operator +golden-machine graduation cannot be mistaken for an appliance-mediated run: + +- `operator-golden-machine` — `PASS/METAL` from live serial and target-emitted + provenance markers without the appliance controller. +- `appliance-mediated` — `PASS/METAL` from live serial and target-emitted + provenance markers while `RAMEN_HIL_APPLIANCE=1`. +- `development-log-replay`, `operator-live`, `appliance-live`, and + `qemu-or-scaffold` — lower-evidence paths that must not be reported as metal + graduation. + +When `RAMEN_HIL_APPLIANCE=1`, the per-gate evidence bundle must also include an +`appliance` object with controller identity and controller evidence references +from `hardware/hil_appliance_v0.toml`. Power/reset events may be empty during +the serial-observer-only S12.4.1 scaffold, but must be populated once actuation +is part of the run. ## Serial provenance markers @@ -66,6 +81,24 @@ HIL gates write JSON under `out/evidence/`: The appliance wrapper may correlate per-gate evidence files via `gate_evidence`, but it must not replace those gate-owned evidence files. +Per-gate HIL JSON includes: + +```json +{ + "evidence_level": "PASS/METAL", + "claim_path": "operator-golden-machine", + "appliance": { + "enabled": false, + "appliance_id": "", + "target_id": "", + "controller_evidence": "", + "controller_log": "", + "controller_log_sha256": "", + "power_events": [] + } +} +``` + ## Claim safety (S13) | Safe claim | Unsafe claim | @@ -75,5 +108,6 @@ The appliance wrapper may correlate per-gate evidence files via `gate_evidence`, | A/B UEFI metadata recognized (S13.8 scaffold) | Atomic update/rollback proved on metal | | Appliance captured target-emitted evidence | Appliance observations are target truth | | S13.7/S13.8 **gate scaffolds** complete | S13 slice **complete** | +| S13 `PASS/METAL` with `claim_path=operator-golden-machine` | S13 appliance-mediated graduation complete | S13 is **complete** only after `PASS/METAL` on Tier-1 class hardware for both S13.7 and S13.8 with the full two-boot atomic-update protocol (future hardening). Appliance evidence improves reproducibility but does not replace target-emitted provenance markers. diff --git a/NEXT_TASKS.md b/NEXT_TASKS.md index caee10b..f665c47 100644 --- a/NEXT_TASKS.md +++ b/NEXT_TASKS.md @@ -8,13 +8,13 @@ ## Active Execution Track -**Now:** Implement the S12.4 HIL appliance v0 physical loop: stabilize the serial observer, then add the power/reset actuator. Run S13 metal HIL graduation through the appliance once that loop is stable. +**Now:** Implement the S12.4 HIL appliance v0 physical loop: stabilize the serial observer, then add the power/reset actuator. Run S13 metal HIL graduation through the appliance once that loop is stable; standalone golden-machine `PASS/METAL` remains valid only when evidence JSON stamps `claim_path: operator-golden-machine`. | Priority | Task | Completion signal | |----------|------|-------------------| | P0 | S12.4.1 HIL appliance serial observer | `RAMEN_HIL_APPLIANCE=1 just hil-appliance` captures live serial and emits valid controller evidence | | P1 | S12.4.2 HIL appliance power/reset actuator | Power and reset scripts are fail-safe, dry-run tested, and represented in controller evidence JSON | -| P2 | S13 metal HIL graduation through the appliance | `RAMEN_HIL_APPLIANCE=1 RAMEN_HIL_GRADUATION=1 just s13-hil` produces valid live provenance | +| P2 | S13 metal HIL graduation through the appliance | `RAMEN_HIL_APPLIANCE=1 RAMEN_HIL_GRADUATION=1 just s13-hil` produces valid live provenance with `claim_path: appliance-mediated` | | P3 | S12 physical graduation through the appliance | `RAMEN_HIL_APPLIANCE=1 RAMEN_HIL_GOLDEN_MACHINE=1 just s12-hil` produces valid live provenance | | P4 | S14 USB xHCI and HID design pass | Approved short plan, IDL boundary, and Foundry gate definition before implementation | @@ -28,6 +28,9 @@ - `just hil-appliance` remains green in its default docs/manifest mode and its opt-in appliance mode. - No `PASS/METAL` claim is emitted without the required target provenance. +- S13 per-gate evidence distinguishes standalone + `claim_path: operator-golden-machine` from appliance-mediated + `claim_path: appliance-mediated` runs. ### P1 Acceptance Criteria diff --git a/SECURITY_STATUS.md b/SECURITY_STATUS.md index 0cfdf57..607a814 100644 --- a/SECURITY_STATUS.md +++ b/SECURITY_STATUS.md @@ -20,7 +20,7 @@ implementations. | Artifact identity | Strict content IDs and signature-aware Store paths | | Store access | Credential/capability checks and domain-scoped visibility | | Native execution | Typed manifests and capability-broker grants | -| POSIX compatibility | Explicit opt-in plus Linux sandbox controls where supported | +| POSIX compatibility | Explicit opt-in plus a host-portable rlimits-only default profile; seccomp/chroot/namespace helpers are tested but not default-wired | | Kernel fast paths | Capability kind, generation, and rights validation | | Wire formats | Versioned IDL and fail-closed length/encoding checks | | Shared memory | Typed control plane, kernel validation, and domain accounting | @@ -40,8 +40,9 @@ The detailed POSIX operating constraints remain in - **Static kernel limits:** fixed-size capability, shared-memory, and allocator structures can still produce controlled denial of service. - **Hardware trust:** S12/S13 do not yet have full `PASS/METAL` evidence. -- **Compatibility isolation:** seccomp, namespaces, and chroot are platform- - dependent layers, not proof of containment against kernel compromise. +- **Compatibility isolation:** the default POSIX runner profile is rlimits-only; + seccomp, namespaces, and chroot are helper controls, not current default + containment. - **Security assurance:** no formal verification, independent audit, or stable release threat model has been completed. diff --git a/docs/HIL_APPLIANCE_EVIDENCE_V0.md b/docs/HIL_APPLIANCE_EVIDENCE_V0.md index c577a65..c4b9fd2 100644 --- a/docs/HIL_APPLIANCE_EVIDENCE_V0.md +++ b/docs/HIL_APPLIANCE_EVIDENCE_V0.md @@ -1,6 +1,6 @@ # HIL Appliance Evidence V0 -**Last Updated:** 2026-06-22 +**Last Updated:** 2026-06-24 **Status:** Scaffold schema for S12.4 / S13.9 **Gate:** `tools/ci/foundry_hil_appliance_s12_4.sh` @@ -32,6 +32,12 @@ hil_appliance_.json Per-gate evidence remains claim-bearing. Appliance evidence records the run context and correlates logs, power events, and controller observations. See `EVIDENCE_LEVELS.md` for the canonical claim-safety language. +Per-gate HIL evidence also carries `claim_path`. Appliance wrapper evidence uses +`PASS/HIL-APPLIANCE`; per-gate S13 metal evidence uses `PASS/METAL` with +`claim_path: appliance-mediated` when the appliance is present, or +`claim_path: operator-golden-machine` for standalone live golden-machine +graduation. + ## Required JSON shape ```json @@ -39,6 +45,7 @@ Per-gate evidence remains claim-bearing. Appliance evidence records the run cont "schema_version": 1, "evidence_kind": "hil_appliance_run_v0", "evidence_level": "PASS/HIL-APPLIANCE", + "claim_path": "appliance-mediated", "run_id": "hil_appliance_20260622T131700Z_pi-hil-01_s13-hil", "appliance_id": "pi-hil-01", "target_id": "intel-nuc-12-reference", diff --git a/runtime_supervisor/POSIX_RUNNER_SECURITY.md b/runtime_supervisor/POSIX_RUNNER_SECURITY.md index c8b458d..1ff93a2 100644 --- a/runtime_supervisor/POSIX_RUNNER_SECURITY.md +++ b/runtime_supervisor/POSIX_RUNNER_SECURITY.md @@ -1,78 +1,65 @@ # POSIX Runner Security Guide -**⚠️ SECURITY NOTICE:** The POSIX runner executes shell scripts and carries inherent security risks. Read this document carefully before use. +**SECURITY NOTICE:** The POSIX runner executes shell scripts and carries inherent +security risks. Read this document carefully before use. ## Current Status -**S7 Security Hardening COMPLETE:** The POSIX runner now runs inside a sandbox with actual enforcement: - -### Seccomp Syscall Filtering (Linux only) -- ✅ **Actual enforcement** using `seccompiler` crate with `prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER)` -- ✅ **Allowlist approach**: Only explicitly whitelisted syscalls are permitted -- ✅ **Blocked syscalls**: - - Process creation: `execve`, `execveat`, `fork`, `vfork`, `clone`, `clone3` - - Network: `socket`, `socketpair`, `bind`, `listen`, `accept`, `connect`, `sendto`, `recvfrom` - - Privilege escalation: `setuid`, `setgid`, `seteuid`, `setegid`, `setresuid`, `setresgid`, `setfsuid`, `setfsgid` - - File creation: `creat`, `mkdir`, `mknod` (blocked via seccomp + namespace) - - Advanced IPC: `shmget`, `shmat`, `shmdt`, `semget`, `semop`, `msgget`, `msgrcv`, `msgsnd` - - Other dangerous: `ptrace`, `kexec_load`, `init_module`, `finit_module`, `userfaultfd` -- ✅ **Allowed syscalls** (whitelist): - - Basic I/O: `read`, `write`, `pread64`, `pwrite64`, `poll`, `select`, `ppoll`, `pselect6` - - Memory: `brk`, `mmap`, `mprotect`, `munmap`, `mremap` - - Process: `exit`, `exit_group`, `rt_sigreturn`, `rt_sigprocmask` - - Time: `clock_gettime`, `gettimeofday` - - Info: `uname`, `getuid`, `getgid`, `geteuid`, `getegid`, `getpid`, `getppid` - - FS (read-only): `fstat`, `lstat`, `stat`, `fstatat`, `access`, `faccessat`, `readlink` - - IPC: `pipe`, `pipe2` (for shell redirection) - - File descriptors: `dup`, `dup2`, `close` - - Misc: `ioctl` (limited for terminal handling), `getrlimit`, `getrusage` - -### Namespace Isolation (Linux only) -- ✅ **Actual enforcement** using `libc::unshare()` in child process before exec -- ✅ **Isolated namespaces**: - - `CLONE_NEWNS`: Mount namespace - isolates filesystem mounts - - `CLONE_NEWUTS`: UTS namespace - isolates hostname and domain name - - `CLONE_NEWIPC`: IPC namespace - isolates System V IPC and POSIX message queues - - `CLONE_NEWNET`: Network namespace - isolates network interfaces -- ⚠️ **Note**: PID namespace (`CLONE_NEWPID`) is not used due to `Command::spawn()` pattern limitations - -### Chroot Filesystem Restriction (Linux only) -- ✅ **Actual enforcement** using `libc::chroot()` in child process before exec -- ✅ **Followed by `chdir("/")`** to set working directory inside chroot -- ✅ **Creates minimal filesystem structure** in chroot directory -- ⚠️ **Limitations**: - - Requires `CAP_SYS_CHROOT` capability or root privileges - - Does not provide complete isolation - only affects path resolution - - Process can escape chroot if it has root privileges and can create device nodes - - Must be combined with namespaces, seccomp, and rlimits for proper isolation - -### Resource Limits (Linux only) -- ✅ **Actual enforcement** using `libc::setrlimit()` in child process before exec -- ✅ **Conservative limits**: - - `RLIMIT_NOFILE`: 64 (max open file descriptors) - - `RLIMIT_NPROC`: 1 (prevents fork bombs and child processes) - - `RLIMIT_FSIZE`: 1,048,576 bytes (1MB max file writes) - - `RLIMIT_AS`: 268,435,456 bytes (256MB max virtual memory) - - `RLIMIT_CPU`: 30 seconds (max CPU time) -- ⚠️ **Note**: Failures to set limits are logged but don't fail execution (defense-in-depth) - -**Risk Level:** MEDIUM (reduced from HIGH with actual enforcement) +The default POSIX runner profile is **host-portable-rlimits-only**: + +```text +seccomp=false namespaces=false chroot=false rlimits=true +``` + +The runtime still requires `RAMEN_POSIX_RUNNER_ACK_RISK=1`, logs every launch, +and applies resource limits on Linux. The seccomp, namespace, and chroot helpers +exist in `runtime_supervisor/src/sandbox.rs` and have focused tests, but they +are not wired into the default runner path because they are not portable on +unprivileged CI hosts. + +### Default Runtime Controls + +- **Kill-switch:** execution is blocked unless + `RAMEN_POSIX_RUNNER_ACK_RISK=1` is set. +- **Artifact path:** store-integrated calls verify the artifact through + `store_service` before execution. +- **Resource limits:** Linux `setrlimit()` hooks are configured for open files, + process count, file size, address space, and CPU time. Limit failures are + logged but do not fail execution. +- **Not default-wired:** seccomp filtering, namespace isolation, and chroot. + +### Helper Controls + +The following helpers are implemented and tested in isolation: + +- **Seccomp syscall filtering:** allowlist-based BPF filter via `seccompiler`. +- **Namespace isolation:** `libc::unshare()` for mount, UTS, IPC, and network + namespaces. PID namespaces are not used by the current `Command::spawn()` + pattern. +- **Chroot filesystem restriction:** `libc::chroot()` followed by `chdir("/")`. + +These helpers are not a claim about the default POSIX runner profile until a +future gate wires them through `posix_run_v0_sandboxed`. + +**Risk Level:** HIGH. This remains a compatibility-only development scaffold +that can execute arbitrary shell scripts once the explicit risk gate is set. ## What This Means ### Protected Against -- ✅ Arbitrary command execution -- ✅ Process creation (fork bombs) -- ✅ Network access -- ✅ Filesystem access outside sandbox -- ✅ Resource exhaustion attacks + +- Accidental execution without the explicit risk acknowledgment. +- Some resource exhaustion paths on Linux when resource limits apply. +- Unsigned or invalid artifacts on store-integrated verified paths. ### NOT Protected Against -- ⚠️ Kernel exploits -- ⚠️ Compromised parent process (runtime_supervisor) -- ⚠️ Side-channel attacks -- ⚠️ Hardware vulnerabilities (Spectre, etc.) -- ⚠️ Repeated invocation DoS + +- Arbitrary shell behavior after `RAMEN_POSIX_RUNNER_ACK_RISK=1`. +- Network access, filesystem traversal, or child process creation in the + default profile. +- Kernel exploits, compromised parent process, side channels, or hardware + vulnerabilities. +- Repeated invocation DoS. See `docs/plans/posix_runner_remaining_risks.md` for complete risk analysis. @@ -127,7 +114,8 @@ Before using the POSIX runner, ensure: ## Migration Path ### Short-Term (Current) -Use the sandboxed POSIX runner for development only. +Use the POSIX runner for development only. Treat the default profile as +rlimits-only, not as a seccomp/chroot container. ### Medium-Term (S9.3) Integrate with store service for artifact validation: @@ -161,28 +149,27 @@ The Foundry gate now includes: 1. **Kill-switch enforcement**: POSIX runner refuses execution without `RAMEN_POSIX_RUNNER_ACK_RISK=1` 2. **Sandbox-disabled warning**: Explicit warning when `RAMEN_POSIX_RUNNER_DISABLE_SANDBOX=1` is set 3. **ACK allows execution**: Execution succeeds with proper acknowledgment -4. **Sandbox enabled by default**: Sandbox is enabled unless explicitly disabled -5. **Seccomp filter enforcement**: Integration tests verify dangerous syscalls are blocked -6. **Resource limit enforcement**: Integration tests verify rlimits are applied -7. **Chroot confinement**: Integration tests verify filesystem access is restricted +4. **Default profile honesty**: the runtime log and unit contract report + `seccomp=false namespaces=false chroot=false rlimits=true` +5. **Seccomp helper enforcement**: helper tests verify dangerous syscalls are blocked +6. **Resource limit helper enforcement**: helper tests verify rlimits are applied +7. **Chroot helper confinement**: helper tests verify filesystem access is restricted ### Integration Tests -The following integration tests verify actual security enforcement: +The following tests verify helper behavior, not the default runner profile: - `seccomp_filter_blocks_execve_syscall`: Verifies `execve` is blocked - `seccomp_filter_blocks_socket_syscall`: Verifies `socket` is blocked - `rlimit_enforces_process_limit`: Verifies `RLIMIT_NPROC` prevents fork - `chroot_confines_filesystem_access`: Verifies files outside chroot are inaccessible -- `sandbox_full_enforcement`: Verifies all controls work together -- `sandbox_blocks_dangerous_operations`: Verifies dangerous operations are blocked +- `sandbox_full_enforcement`: Verifies all controls work together when explicitly configured +- `sandbox_blocks_dangerous_operations`: Verifies dangerous operations are blocked when explicitly configured ## Platform Support -- ✅ **Linux**: Full sandbox support with actual enforcement - - Seccomp BPF filtering via `prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER)` - - Namespace isolation via `libc::unshare()` - - Chroot filesystem restriction via `libc::chroot()` - - Resource limits via `libc::setrlimit()` +- **Linux**: default runner profile applies the configured resource-limit path. + Seccomp, namespace, and chroot helpers are available only when explicitly + wired by tests or future runner profiles. - ⚠️ **macOS**: Partial support - No seccomp support (not available on macOS) - Sandbox returns error on non-Linux platforms diff --git a/runtime_supervisor/src/posix_runner.rs b/runtime_supervisor/src/posix_runner.rs index fe5752c..d188459 100644 --- a/runtime_supervisor/src/posix_runner.rs +++ b/runtime_supervisor/src/posix_runner.rs @@ -45,8 +45,8 @@ fn parse_env_flag(name: &str) -> bool { /// POSIX runner v0: execute an artifact blob as a shell script. /// -/// S7 Security Hardening: Runtime enforcement with sandbox by default. -/// - Sandbox is enabled by default (not just feature flag) +/// S7 Security Hardening: Runtime enforcement with an explicit compatibility risk gate. +/// - The default portable profile applies resource limits only. /// - Runtime kill-switch requires RAMEN_POSIX_RUNNER_ACK_RISK=1 /// - All executions are logged with full context for forensic analysis /// @@ -56,11 +56,10 @@ fn parse_env_flag(name: &str) -> bool { /// /// # Security Model (S7 Hardening) /// -/// The sandbox provides defense-in-depth protection: -/// - **Seccomp filtering**: Blocks execve, fork, clone, socket syscalls -/// - **Namespace isolation**: PID, mount, UTS, network namespaces -/// - **Chroot**: Filesystem restricted to temporary directory -/// - **Resource limits**: CPU time, memory, file descriptors, processes +/// The current default runner profile is host-portable and applies resource +/// limits only. Seccomp, namespace, and chroot helpers exist in `sandbox.rs`, +/// but are not wired into this default path because they are not portable on +/// unprivileged CI hosts. /// /// # Environment Variables /// @@ -122,24 +121,24 @@ fn posix_run_v0_sandboxed( log_path.map(|p| p.display().to_string()) ); eprintln!("posix_runner: - Caller PID: {}", std::process::id()); - eprintln!("posix_runner: - Sandbox: ENABLED"); - - // Create temporary chroot directory + // Create temporary chroot directory for profiles that enable chroot later. let chroot_dir = std::env::temp_dir().join(format!("ramen_posix_sandbox_{}", std::process::id())); fs::create_dir_all(&chroot_dir)?; - // Set up the host-portable sandbox profile used by the dev-only POSIX path. + let sandbox_config = default_posix_sandbox_config(chroot_dir.clone()); + + eprintln!("posix_runner: - Sandbox profile: host-portable-rlimits-only"); + eprintln!( + "posix_runner: - Sandbox controls configured: seccomp={} namespaces={} chroot={} rlimits={}", + sandbox_config.seccomp, + sandbox_config.namespaces, + sandbox_config.chroot, + sandbox_config.rlimits + ); + // Namespace/chroot/seccomp helpers remain separately testable, but applying // them before Command::exec is not portable on unprivileged CI runners. - #[cfg_attr(not(target_os = "linux"), allow(unused_variables))] - let sandbox_config = SandboxConfig { - seccomp: false, - namespaces: false, - chroot: false, - rlimits: true, - chroot_dir: Some(chroot_dir.clone()), - }; // Build command with sandbox wrappers let mut cmd = Command::new("sh"); @@ -161,7 +160,7 @@ fn posix_run_v0_sandboxed( #[cfg(not(target_os = "linux"))] { eprintln!( - "posix_runner: WARNING: Sandboxing is only supported on Linux. Running with reduced security." + "posix_runner: WARNING: Only resource-limit sandboxing is configured in the portable profile." ); } @@ -185,6 +184,16 @@ fn posix_run_v0_sandboxed( cmd.spawn() } +fn default_posix_sandbox_config(chroot_dir: std::path::PathBuf) -> SandboxConfig { + SandboxConfig { + seccomp: false, + namespaces: false, + chroot: false, + rlimits: true, + chroot_dir: Some(chroot_dir), + } +} + /// V-006 Phase 3: Execute an artifact blob fetched from store service /// /// S7 Security Hardening: Logs all executions with content_id for audit trail. @@ -544,6 +553,17 @@ mod tests { let _ = fs::remove_file(script); } + #[test] + fn posix_run_v0_default_profile_is_rlimits_only() { + let config = default_posix_sandbox_config(test_path("sandbox_profile")); + + assert!(!config.seccomp); + assert!(!config.namespaces); + assert!(!config.chroot); + assert!(config.rlimits); + assert!(config.chroot_dir.is_some()); + } + #[test] fn posix_run_v0_allows_execution_when_sandbox_disabled() { let _guard = env_lock().lock().expect("env lock poisoned"); diff --git a/runtime_supervisor/src/sandbox.rs b/runtime_supervisor/src/sandbox.rs index 659d989..4bc59f1 100644 --- a/runtime_supervisor/src/sandbox.rs +++ b/runtime_supervisor/src/sandbox.rs @@ -543,6 +543,7 @@ fn apply_chroot(cmd: &mut Command, chroot_dir: Option<&Path>) -> io::Result<()> /// Cleanup sandbox resources /// /// Should be called after process exits to clean up temporary directories +#[allow(dead_code)] pub fn cleanup_sandbox(config: &SandboxConfig) -> io::Result<()> { if let Some(chroot_dir) = &config.chroot_dir { if chroot_dir.exists() { diff --git a/tools/ci/foundry_hil_appliance_s12_4.sh b/tools/ci/foundry_hil_appliance_s12_4.sh index f85bcf1..59e1ef2 100644 --- a/tools/ci/foundry_hil_appliance_s12_4.sh +++ b/tools/ci/foundry_hil_appliance_s12_4.sh @@ -80,6 +80,8 @@ grep -q 'serial_markers_observed' "$SERIAL_CAPTURE_SCRIPT" \ || fail "SERIAL_CAPTURE_MARKER_SCAN_MISSING" "serial capture script must scan serial markers" grep -q 'target_hil_evidence_markers' "$SERIAL_CAPTURE_SCRIPT" \ || fail "SERIAL_CAPTURE_HIL_MARKER_SCAN_MISSING" "serial capture script must parse hil_evidence markers" +grep -q 'claim_path' "$EVIDENCE_LEVELS" \ + || fail "CLAIM_PATH_DOCTRINE_MISSING" "EVIDENCE_LEVELS must require per-gate claim_path disambiguation" grep -q 'manifest = "hardware/hil_appliance_v0.toml"' "$GOLDEN_MANIFEST" \ || fail "GOLDEN_APPLIANCE_LINK_MISSING" "golden machine manifest must link appliance manifest" @@ -154,6 +156,65 @@ fi echo "$GATE_ID: METRIC docs_manifest=pass" +echo "$GATE_ID: INFO step=per_gate_evidence_contract" +PER_GATE_TMP="$(mktemp -d "${TMPDIR:-/tmp}/ramen-hil-gate-evidence.XXXXXX")" +PER_GATE_JSON="$PER_GATE_TMP/s13_7_nvme_boot_evidence.json" +PER_GATE_SERIAL="$PER_GATE_TMP/serial.log" +cat >"$PER_GATE_SERIAL" <<'EOF' +RAMEN OS +persistent_storage: nvme_boot ok +hil_evidence: git_sha=fixture +hil_evidence: init_profile=nvme_boot +hil_evidence: machine_id=intel-nuc-12-reference +hil_evidence: storage_manifest_sha256=fixture +hil_evidence: kernel_efi_sha256=fixture +hil_evidence: init_img_sha256=fixture +hil_evidence: boot_epoch_nonce=fixture +EOF + +RAMEN_HIL_GRADUATION=1 \ +RAMEN_HIL_APPLIANCE=1 \ +RAMEN_HIL_APPLIANCE_ID=pi-hil-fixture \ +RAMEN_HIL_TARGET_ID=intel-nuc-fixture \ +RAMEN_HIL_CONTROLLER_EVIDENCE="$PER_GATE_TMP/hil_appliance_fixture.json" \ + bash -c 'source tools/hil/hil_gate_common.sh; ramen_hil_emit_evidence_json "$0" "foundry_s13_nvme_boot_s13_7" "PASS/METAL" "$1" "persistent_storage: nvme_boot ok" "" ""' \ + "$PER_GATE_JSON" "$PER_GATE_SERIAL" + +python3 - "$PER_GATE_JSON" <<'PY' +import json +import sys + +with open(sys.argv[1], encoding="utf-8") as f: + payload = json.load(f) + +assert payload["evidence_level"] == "PASS/METAL" +assert payload["claim_path"] == "appliance-mediated" +appliance = payload["appliance"] +assert appliance["enabled"] is True +assert appliance["appliance_id"] == "pi-hil-fixture" +assert appliance["target_id"] == "intel-nuc-fixture" +assert appliance["controller_evidence"] != "" +PY + +RAMEN_HIL_GRADUATION=1 \ + bash -c 'source tools/hil/hil_gate_common.sh; ramen_hil_emit_evidence_json "$0" "foundry_s13_nvme_boot_s13_7" "PASS/METAL" "$1" "persistent_storage: nvme_boot ok" "" ""' \ + "$PER_GATE_JSON" "$PER_GATE_SERIAL" + +python3 - "$PER_GATE_JSON" <<'PY' +import json +import sys + +with open(sys.argv[1], encoding="utf-8") as f: + payload = json.load(f) + +assert payload["evidence_level"] == "PASS/METAL" +assert payload["claim_path"] == "operator-golden-machine" +assert payload["appliance"]["enabled"] is False +PY + +rm -rf "$PER_GATE_TMP" +echo "$GATE_ID: METRIC per_gate_evidence_contract=pass" + echo "$GATE_ID: INFO step=serial_observer_contract" SERIAL_OBSERVER_TMP="$(mktemp -d "${TMPDIR:-/tmp}/ramen-hil-serial.XXXXXX")" SERIAL_OBSERVER_FIXTURE="$SERIAL_OBSERVER_TMP/fixture.serial.log" diff --git a/tools/ci/foundry_s7_posix_runner_security.sh b/tools/ci/foundry_s7_posix_runner_security.sh index 3c91cc9..067b02a 100755 --- a/tools/ci/foundry_s7_posix_runner_security.sh +++ b/tools/ci/foundry_s7_posix_runner_security.sh @@ -5,7 +5,7 @@ # - POSIX runner refuses to execute without RAMEN_POSIX_RUNNER_ACK_RISK=1 # - POSIX runner logs security warnings when RAMEN_POSIX_RUNNER_DISABLE_SANDBOX=1 is set # - POSIX runner executes successfully with proper acknowledgment -# - Sandbox is enabled by default +# - Default portable sandbox profile reports its actual controls set -euo pipefail @@ -86,8 +86,8 @@ cargo test -p runtime_supervisor --features posix_runner_v0_dev posix_run_v0_all echo "PASS: Execution succeeds with acknowledgment" -# Test 4: Sandbox enabled by default -echo "Test 4: Verifying sandbox is enabled by default..." +# Test 4: Default portable sandbox profile is honest about enabled controls +echo "Test 4: Verifying default sandbox profile reports actual controls..." cargo test -p runtime_supervisor --features posix_runner_v0_dev posix_run_v0_uses_sandbox_by_default -- --nocapture \ > "$EVIDENCE_DIR/test4_sandbox_default.log" 2>&1 || { echo "FAIL: Sandbox-default test failed" @@ -95,8 +95,14 @@ cargo test -p runtime_supervisor --features posix_runner_v0_dev posix_run_v0_use exit 1 } -grep -q "Sandbox: ENABLED" "$EVIDENCE_DIR/test4_sandbox_default.log" || { - echo "FAIL: Expected sandbox enabled marker not found" +grep -q "Sandbox profile: host-portable-rlimits-only" "$EVIDENCE_DIR/test4_sandbox_default.log" || { + echo "FAIL: Expected host-portable sandbox profile marker not found" + cat "$EVIDENCE_DIR/test4_sandbox_default.log" + exit 1 +} + +grep -q "Sandbox controls configured: seccomp=false namespaces=false chroot=false rlimits=true" "$EVIDENCE_DIR/test4_sandbox_default.log" || { + echo "FAIL: Expected actual default sandbox controls not found" cat "$EVIDENCE_DIR/test4_sandbox_default.log" exit 1 } @@ -107,7 +113,23 @@ if grep -q "WARNING: SANDBOX DISABLED" "$EVIDENCE_DIR/test4_sandbox_default.log" exit 1 fi -echo "PASS: Sandbox is enabled by default" +echo "PASS: Default sandbox profile is honest" + +echo "Test 4b: Verifying default sandbox config unit contract..." +cargo test -p runtime_supervisor --features posix_runner_v0_dev posix_run_v0_default_profile_is_rlimits_only -- --nocapture \ + > "$EVIDENCE_DIR/test4b_sandbox_profile.log" 2>&1 || { + echo "FAIL: Sandbox profile unit contract failed" + cat "$EVIDENCE_DIR/test4b_sandbox_profile.log" + exit 1 +} + +grep -q "test result: ok" "$EVIDENCE_DIR/test4b_sandbox_profile.log" || { + echo "FAIL: Sandbox profile unit contract did not pass" + cat "$EVIDENCE_DIR/test4b_sandbox_profile.log" + exit 1 +} + +echo "PASS: Default sandbox profile unit contract verified" # Test 5: Seccomp filter enforcement echo "Test 5: Verifying seccomp filter blocks dangerous syscalls..." @@ -160,22 +182,22 @@ grep -q "test result: ok" "$EVIDENCE_DIR/test7_chroot_confinement.log" || { echo "PASS: Chroot confines filesystem access" -# Test 8: Full sandbox enforcement -echo "Test 8: Verifying all sandbox controls work together..." +# Test 8: Full helper sandbox enforcement +echo "Test 8: Verifying all helper sandbox controls work together when explicitly configured..." cargo test -p runtime_supervisor --features posix_runner_v0_dev sandbox_full_enforcement -- --nocapture \ > "$EVIDENCE_DIR/test8_full_sandbox.log" 2>&1 || { - echo "FAIL: Full sandbox test failed" + echo "FAIL: Full helper sandbox test failed" cat "$EVIDENCE_DIR/test8_full_sandbox.log" exit 1 } grep -q "test result: ok" "$EVIDENCE_DIR/test8_full_sandbox.log" || { - echo "FAIL: Full sandbox test did not pass" + echo "FAIL: Full helper sandbox test did not pass" cat "$EVIDENCE_DIR/test8_full_sandbox.log" exit 1 } -echo "PASS: Full sandbox enforcement verified" +echo "PASS: Full helper sandbox enforcement verified" # Generate evidence summary cat > "$EVIDENCE_DIR/summary.md" < "$EVIDENCE_DIR/summary.md" < "$EVIDENCE_DIR/summary.md" < str: @@ -139,10 +158,28 @@ def sha256_file(path: str) -> str: h.update(chunk) return h.hexdigest() +def parse_power_events(): + raw = os.environ.get("RAMEN_HIL_POWER_EVENTS_JSON", "").strip() + if not raw: + return [] + try: + parsed = json.loads(raw) + except json.JSONDecodeError: + return [{"kind": "parse_error", "result": "invalid_power_events_json"}] + return parsed if isinstance(parsed, list) else [{"kind": "parse_error", "result": "power_events_not_array"}] + +appliance_enabled = os.environ.get("RAMEN_HIL_APPLIANCE", "") == "1" +controller_evidence = os.environ.get( + "RAMEN_HIL_CONTROLLER_EVIDENCE", + os.environ.get("RAMEN_HIL_APPLIANCE_EVIDENCE", ""), +) +controller_log = os.environ.get("RAMEN_HIL_CONTROLLER_LOG", "") + payload = { "schema_version": 1, "gate_id": gate_id, "evidence_level": evidence_level, + "claim_path": claim_path, "timestamp_utc": datetime.now(timezone.utc).isoformat(), "git_sha": os.environ.get("RAMEN_GIT_SHA", "unknown"), "machine_id": os.environ.get("RAMEN_MACHINE_ID", "unknown"), @@ -152,9 +189,21 @@ payload = { "serial_log": serial_log, "marker": marker, "graduation_mode": os.environ.get("RAMEN_HIL_GRADUATION", "") == "1", + "appliance": { + "enabled": appliance_enabled, + "appliance_id": os.environ.get("RAMEN_HIL_APPLIANCE_ID", "") if appliance_enabled else "", + "target_id": os.environ.get( + "RAMEN_HIL_TARGET_ID", + os.environ.get("RAMEN_MACHINE_ID", ""), + ) if appliance_enabled else "", + "controller_evidence": controller_evidence if appliance_enabled else "", + "controller_log": controller_log if appliance_enabled else "", + "controller_log_sha256": sha256_file(controller_log) if appliance_enabled and controller_log else "", + "power_events": parse_power_events() if appliance_enabled else [], + }, } with open(out_path, "w", encoding="utf-8") as f: json.dump(payload, f, indent=2) f.write("\n") PY -} \ No newline at end of file +}