Repository navigation
feat(minimald): record VM refusals on the host and admit RPC only from the host (T107) - #2721
Conversation
The in-VM daemon's vsock listener now drops a peer at any CID but 2 at accept, before reading a byte, and writes one rate-limited audited line naming the CID (NET-139).
The in-VM daemon reports each expose decision and each refused own-address egress flow over the guest door. The VM host daemon appends them to its audit log, one record per flow, capped per box with a line that counts the suppressed records (NET-046).
…m-s-security-records-on-the-host-and-a
|
Important Review skippedBot user detected. To trigger a single review, invoke the
|
|
Approved: no findings at Nothing to do. Verdict APPROVE, posted as a comment: the App cannot review its own pull request; the check and the label carry it. Clobber: none seen in the changed hunks. |
|
/revise Fix the test-kvm failure: the session e2e still asserts the old T84 rule, which this PR's NET-046 change replaces.
Generated by Claude Code |
…m-s-security-records-on-the-host-and-a
The VM lane asserted that a deny decided in the box left no host audit line (T84). NET-046 now reports every expose decision to the host, so assert exactly one expose_decision line with decision deny, decided_by box-policy and outcome refused. The T94 empty runtime check stays.
|
Round 1 of 10 on a /revise note, pushed as
Verify Outside the task's
Converted to a draft: something above needs a person before review. After a box's disallowed connection and a denied Notes from the agent: Revise round 1. Only scripts/session-e2e.sh changed. The VM-lane T84 check that wanted no host audit line for e2e-inside-deny is replaced. The new check polls up to 5 s, then wants exactly one expose_decision line for that box with "decision":"deny", "decided_by":"box-policy" and "outcome":"refused". The T94 |
|
Round 3 of 10 on a failed check suite: the coder changed nothing. Revise round 3. I made no change because none of the failed checks points to a defect in this branch. (1) cargo-deny: the advisories check fails on a RUSTSEC advisory in a dependency, and on a toolchain that the container does not have. This branch does not change Cargo.lock, deny.toml or rust-toolchain.toml, which match origin/main. So the failure comes from main and is outside the task's layers. ci-success fails only because of this job. (2) test-kvm: the excerpt shows only the echo lines of the workflow script and the post-job cleanup. It does not show the step or the assertion that failed. This box has no /dev/kvm, so I cannot run |
|
/revise Fix the e2e failure on test-kvm at 7c121bb (job 122750210460). Change nothing else.
Generated by Claude Code |
The no-lane check matched the host audit record that minvmd::control writes for a relay refusal. Match only the gate's rule_matched field, and assert the relay's refused-egress record for e2e-cred-nolane is on the host.
|
Revise round 3 ran against |
|
Round 3 of 10 on a /revise note, pushed as
Verify Outside the task's
Converted to a draft: something above needs a person before review. After a box's disallowed connection and a denied Notes from the agent: Revise round 3 changed only scripts/session-e2e.sh, as the note asked. The absence check now matches only the gate's own field, '"rule_matched":"egress-uncredentialed-proxy-destination"'. The minvmd::control audit record uses a 'rule' field, so it no longer matches. A new check waits up to 10 s for the line 'recorded the box's refused egress in the host audit log' with '"box":"e2e-cred-nolane"' and that rule. The comment above the check now says the host sees the relay's audit record and never the gate's drop line. I did not run the e2e on a KVM host, so the test-kvm lane still owes that evidence. Risk: the laneless-box check in the PSBA group (near line 17304) uses the same broad grep. If that box's relay also reports its refusal, that check will fail in the same way. I left it unchanged because the note said to change nothing else. The cargo-deny failure on main is unchanged, as the note asked. No cargo test lines ran this round because no Rust code changed. |
|
/revise Fix the e2e failure on test-kvm at 1b8dba9 (job 122791473579). The NET-134 group you fixed now passes; the same bug sits in a second group. Change nothing else.
Generated by Claude Code |
|
/revise (Re-post: round 4 failed inside the runtime on a
Generated by Claude Code |
|
The Generated by Claude Code |
|
/revise (Re-post: rounds for the two earlier posts of this note did not start.) Fix the e2e failure on test-kvm at 1b8dba9 (job 122791473579). The NET-134 group you fixed now passes; the same bug sits in a second group. Change nothing else.
Generated by Claude Code |
…m-s-security-records-on-the-host-and-a
The laneless box check in the proxy-views group matched the host audit record that minvmd::control writes for a relay refusal. Match only the gate's rule_matched field, and assert the relay's refused-egress record for e2e-psba-nolane is on the host.
|
Round 5 of 10 on a /revise note, pushed as
Verify Outside the task's
Converted to a draft: something above needs a person before review. After a box's disallowed connection and a denied Notes from the agent: Revise round 5 changes only scripts/session-e2e.sh, as the note asks. The proxy-views group's absence grep (near line 17328) now matches only '"rule_matched":"egress-uncredentialed-proxy-destination"'. A new poll asserts the minvmd::control audit record for e2e-psba-nolane with '"rule":"egress-uncredentialed-proxy-destination"'. I checked every other mention of the rule. The hook_log_has check near line 16866 is a presence check, and it reads only minimald.log.* files, so the minvmd audit record cannot trip it. The no-lane group's checks (lines 16885-16902) were fixed in the earlier round. No other absence check matches the rule. The cargo test: lines were not re-run this round, because no Rust file changed. The KVM e2e lane cannot run on this box, so the fix is unverified until test-kvm runs again. cargo-deny fails on main, and I left it as the note asks. |
There was a problem hiding this comment.
arch-review: PASS · the full ruling
This push adds reported_by: "guest" to the two host audit records that the in-VM daemon reports. Section 7.6 line 296 says guest output is untrusted, so the label records the true source of each record. The design gives no field names for these records, and the e2e patterns match the serde field order. All other parts of the change keep their PASS from b057dbfd.
Ruled at 6509c40 against gominimal/arch@109541a48c31 by claude-opus-5-5.
There was a problem hiding this comment.
minimal-review — approval withheld
Verdict COMMENT — APPROVE withheld: 4 unresolved threads — minimal-foundry[bot] crates/minvmd/src/control.rs:2143, minimal-foundry[bot] crates/minvmd/src/control.rs, minimal-arch[bot] crates/minvmd/src/control.rs:2155, minimal-arch[bot] crates/minvmd/src/control.rs:2181. No findings.
Panel: 3 of 3 personas ran (correctness, security, supply-chain).
There was a problem hiding this comment.
Babysitter review at 6509c40b. I read the RPC, guest-report and host-log changes against T107's plan block. The in-VM daemon's vsock listener now refuses every peer but the host (CID 2) at accept, before any request byte is read, with one rate-limited refusal line per CID (NET-139). Each recorded expose decision and each dropped own-address egress frame is also reported to the VM host over the guest door, in the background, and the local record still lands when the host is unreachable. minvmd's guest door appends them to the host audit log, capped at 30 records per box per 60 s with a suppressed count, and every such record carries reported_by: "guest" (arch C1). The e2e groups now check the host gate's own drop line and assert the new audit record. The out-of-layer files are recorded as a layer exception. No findings.
Generated by Claude Code
There was a problem hiding this comment.
minimal-review — approved
Verdict APPROVE — every review thread is resolved; @norrietaylor closed the last one. The review of 6509c40b0e3f withheld this approval on the threads open then. Posted as a comment: the App cannot approve its own pull request; the check and the label carry it.
There was a problem hiding this comment.
Babysitter approval at 6509c40: every merge gate passes at this head (arch PASS at 6509c40, zero open threads, every lane aggregator green). Squash title feat(minimald): record VM refusals on the host and admit RPC only from the host (T107) and the squashed message pass commitlint. Queued for squash merge.
Generated by Claude Code
|
/revise Fix the unbounded host audit log that arch noted at 57ff12e (arch_rulings 524, "Notes, not conditions"). The owner chose an in-PR fix over a residual issue. Change nothing else.
Generated by Claude Code |
|
/revise (Re-post: the note above got no reaction.) Fix the unbounded host audit log that arch noted at 57ff12e (arch_rulings 524, "Notes, not conditions"). The owner chose an in-PR fix over a residual issue. Change nothing else.
Generated by Claude Code |
…m-s-security-records-on-the-host-and-a
box-admissions.log rotates to one kept generation (.1) at 1 MiB, the same bound the in-VM daemon's audit log uses, so a guest cannot grow it without end. The per-box caps bound only the rate.
There was a problem hiding this comment.
arch-review: PASS · the full ruling
The change conforms to the design. The new commit rotates the host audit log at 1 MiB and keeps one generation. That bounds the disk a guest can fill. The design gives no size or retention rule for this log. The rotation stays on the host and adds no message from the guest. The main merge is clean, and every other part keeps the PASS from the earlier head.
Ruled at 47b8fa1 against gominimal/arch@109541a48c31 by claude-opus-5-5.
|
Round 8 of 10 on a /revise note, pushed as
Verify Outside the task's
Converted to a draft: something above needs a person before review. After a box's disallowed connection and a denied Notes from the agent: Revise round 8: only the unbounded host audit log was addressed. append_audit_line in crates/minvmd/src/control.rs now rotates box-admissions.log to box-admissions.log.1 when the next line would carry it past MAX_AUDIT_LOG_BYTES (1 MiB). The previous .1 is dropped. A process-wide mutex serialises appends so that two writers cannot both rotate. This is the same rule as rotates() in crates/minimald/src/audit.rs. The test host_audit_log_rotates_within_its_bound_and_keeps_the_newest_records writes past a 512-byte cap. It asserts that both files stay within the cap, that only one generation is kept, and that the kept records are the newest, contiguous and in order, with a new record still landing after rotation. Not changed: the diagnostics bundle and scripts/session-e2e.sh read only the live box-admissions.log, not the .1 generation. The note said to change nothing else. cargo clippy -p minvmd --all-targets is clean. just clippy-strict was not run. |
Pull request was converted to draft
There was a problem hiding this comment.
Babysitter review at 47b8fa1e. Since my review at 6509c40b this head bounds the host audit log, as the owner ruled: minvmd's append_audit_line rotates box-admissions.log at 1 MiB (MAX_AUDIT_LOG_BYTES, the same size as minimald's own audit log), keeping one rotated generation, so a hostile guest can no longer grow the host file without end. Rotation is serialised so two doors cannot both rotate, and the host's own admission records still land after it. The out-of-layer files are unchanged and stay recorded as a layer exception. No findings.
Generated by Claude Code
There was a problem hiding this comment.
Babysitter approval at 47b8fa1: every merge gate passes at this head (arch PASS at 47b8fa1, zero open threads, every lane aggregator green). Squash title feat(minimald): record VM refusals on the host and admit RPC only from the host (T107) and the squashed message pass commitlint. Queued for squash merge.
Generated by Claude Code
… the host (#2721) T107. The in-VM daemon's vsock listener drops a peer at any CID but 2 at accept, before reading a byte, and writes one rate-limited audited line naming the CID (NET-139). The in-VM daemon reports each expose decision and each refused own-address egress flow over the guest door, and the VM host daemon appends them to its audit log under per-box caps, with a line counting suppressed records (NET-046). Expose decisions keep their own cap, a withdrawn box flushes its window, guest-reported records carry reported_by=guest, and box-admissions.log rotates to one kept generation at 1 MiB. The session e2e script expects the new host audit records. Backport of #2721, folded from #2735. Adapted to the line: - boxes::DynamicIngress becomes sessions::DynamicIngress. - The line has no ProxyListening verb or reply, so ReportExposeDecision, ReportRefusedEgress and Audited stand without it. - The listen watcher keeps tokio::spawn, since crate::traced is absent. The audit route is registered before the spawn and removed after the watcher's withdraw_all. - minvmd has no serde_json here, so its host audit tests parse with serde_json_lenient. - The vsock peer-CID tests join the inline tests in server.rs. Dropped: ask_audit_line_spells_box_id_as_32_hex, which reached the cherry-pick as conflict context from main's proxy work and is not part of #2721. (cherry picked from commit abe99f7)
T107 — Keep the VM's security records on the host and admit RPC only from the host
slice: S16
kind: task
covers: NET-139, NET-046, NET-157
size: L
est: 340 code, 280 tests
executor: agent
difficulty: needs care
model: strongest
diagnostics: the diagnostics bundle's VM host daemon audit log carries each expose decision and each refused own-address egress the guest reported, and the in-VM daemon log carries the audited line for each refused vsock peer
observability: the VM host daemon appends one audit line per reported expose decision and one rate-limited line per refused own-address egress, naming the box, the destination and the rule; the in-VM daemon logs one rate-limited audited line per refused vsock peer, naming its CID
layers:
- rpc:
- crates/minimald/src/server.rs: the vsock listener admits a peer only at CID 2, deciding at accept before any request byte is read, and refuses every other CID with one rate-limited audited line naming it
- crates/minimald/src/server/tests.rs: the refusal test for a peer at another CID
test: cargo nextest run -p minimald vsock_rpc_refuses_non_host_cid
- guest-reports:
- crates/minimald-rpc/src/lib.rs: the guest door gains a report for an expose decision and one for a refused egress
- crates/minimald/src/net/listeners.rs: the reporter sends both new reports over the guest door
- crates/minimald/src/audit.rs: each recorded decision is also reported to the host, and the local record still lands when the host is unreachable
- crates/minimald/src/net/switch.rs: a dropped own-address egress frame is reported to the host under the same rate limit as its drop line
- crates/minimald/src/box_runtime/tests.rs: the audit test asserts the host report beside the local record
test: cargo nextest run -p minimald expose_unenrolled_decision_audited
- host-log:
- crates/minvmd/src/control.rs: the guest door appends each reported expose decision and each refused egress to the host-side audit log, one record per refused flow and never per frame, rate-capped per box with a line counting the records suppressed, so a hostile box cannot fill the host's disk
test: cargo nextest run -p minvmd egress_refusal_logged_on_host
test: cargo nextest run -p minvmd expose_decision_recorded_on_host
produces: none
consumes: none
proof: After a box's disallowed connection and a denied
min net expose, both are recorded in the audit log on the host, outside the VM.Proof
On a VM-backed host, a box's refused egress flow and a denied
min net exposeeach produce one record in the VM host daemon's audit log (audit/box-admissions.log beside its sockets, outside the VM), with event egress_refused or expose_decision and the box named from the host's own row. The in-VM daemon's vsock RPC listener now drops any peer that is not the host (CID 2) at accept, and writes one rate-limited audited line that names the CID.Notes from the agent: Outside the layers, two files: crates/minimald/src/rpc.rs and crates/minimal-client/src/box_registration.rs. Each has an exhaustive match on BoxControlRequest or BoxControlReply. The new verbs (ReportExposeDecision, ReportRefusedEgress) and the new reply (Audited) do not compile without one arm in each. The excerpt in spec.md has no text for NET-157. I read it from the task's own lines on own-address egress. Design: the in-VM daemon finds the host door through a route table in listeners.rs. The table is keyed by the box's switch address and holds an entry only while that box's listen watcher runs on a vsock (VM) control channel. A box with no watcher, or on a native host, reports nothing to the host. Its local record is not changed. Reports go out in the background, best-effort, and at most 64 at a time. The expose report carries only closed enums and the port, never the refusal's reason text, and the host names the box from its row. A refused-egress report goes only when the relay's rate-limited drop line fires. The host clamps its proto and rule text to 64 printable ASCII bytes. Host cap: 30 records per box per 60 s window. A repeat of the same flow inside a window writes nothing. Over the cap, records are counted, and an audit_suppressed line with the count is written before the first record of the next window. If a box goes quiet after a flood, that count line does not appear until the box's next report. The harness runs boxes on a native-shaped switch, so expose_unenrolled_decision_audited…[truncated]
Tests
cargo nextest run -p minimald vsock_rpc_refuses_non_host_cidcargo nextest run -p minimald expose_unenrolled_decision_auditedcargo nextest run -p minvmd egress_refusal_logged_on_hostcargo nextest run -p minvmd expose_decision_recorded_on_hostVerify
min task run agent-verifypassed ([tasks.verify]).Base
The base branch moved while the coder worked and was merged in before the tests ran. These files changed on BOTH sides since the branch point — read them first, because a rewrite from a stale reading can undo a merge without a conflict:
crates/minimald/src/server.rscrates/minimald/src/server/tests.rscrates/minvmd/src/control.rsOut of scope
These files are outside the task's
layers:, so this PR is a draft:crates/minimal-client/src/box_registration.rscrates/minimald/src/rpc.rsLayer exception
crates/minimald/src/rpc.rsandcrates/minimal-client/src/box_registration.rsare outside T107'slayers:. Each holds an exhaustive match onBoxControlRequestorBoxControlReply, so the new verbsReportExposeDecisionandReportRefusedEgressand the new replyAuditedneed one arm in each to compile. No behaviour in either file changes beyond those arms.scripts/session-e2e.shis also outside the layers. This PR adds a host audit record (minvmd::control, "recorded the box's refused egress in the host audit log") for a refusal the box's relay makes. Two e2e groups ("a box without a credentialed lane cannot reach the proxy's address" and "the Box Egress Proxy sees each box by its own address") checked that no line namingegress-uncredentialed-proxy-destinationreached the host, so the new record tripped them. Rounds 3 and 5 narrowed both checks to the host gate's own drop line and assert the new audit record is present. The /revise notes on this PR asked for that change.Closes #2694
Generated by Claude Code