feat(msb): mirror the sbx balanced network policy as the msb default egress - #295
feat(msb): mirror the sbx balanced network policy as the msb default egress#295mogul wants to merge 7 commits into
balanced network policy as the msb default egress#295Conversation
Mirror the sbx "balanced" (local-policy) network allow-list as a human-auditable data file. The msb backend will translate each entry into an msb --net-rule so msb sandboxes get the same egress as sbx "balanced" (msb defaults egress to none). Refs: ADR-0018
Add ACQ_MSB_BALANCED_EGRESS (default on) + ACQ_MSB_BALANCED_HOSTS_FILE. _acq_msb_balanced_rules_into translates the vendored host list into msb --net-rule tokens (**.->*. suffix, crl*.->parent suffix with a warning, :port->:tcp:port, plus allow@dns) and acq_backend_provision emits them with --net-default deny so egress is restricted to the balanced set, matching sbx 'balanced'. Helpers kept under the 50-line limit. Refs: ADR-0018
Add offline unit + provision tests for the balanced egress baseline (target translation, port validation, dual-port hosts, crl* broadening, malformed-line skip, real-file parse, and the default-on / =0-off provision paths). Fix: the documented ACQ_MSB_BALANCED_EGRESS=0 off-switch did not work — the guard tested `[ -n "0" ]` (non-empty, so on). Normalize the value to "1"/"" up front, treating 0/false/no/off/empty (case-insensitive) as off. Refs: ADR-0018
Add ADR-0018 (mirror the sbx "balanced" policy as msb's default egress), the ACQ_MSB_BALANCED_EGRESS / ACQ_MSB_BALANCED_HOSTS_FILE tunables and a network-egress section in BACKEND_GUIDE.md, and a KNOWN_FAILURE_MODES entry for host-list drift + the re-sync procedure.
- Accept sbx-'balanced' parity: registry.npmjs.org is in the balanced set, so every sandbox (incl. shell) reaches it when the baseline is on. Rescope test 8n4 to the kit-only mode (ACQ_MSB_BALANCED_EGRESS=0), which is where the agent-conditional npm gate still applies, and document the interaction in ADR-0018. - Reject leading-zero ports (e.g. 0443) in _acq_msb_balanced_port_ok. - Extract _acq_msb_balanced_parse_line so _acq_msb_balanced_rules_into drops from 51 to 42 lines (under the 50-line limit). Refs: ADR-0018
msb's semantic `allow@dns` macro hard-fails `msb create`/`msb run` on released builds (reproduced on 0.6.8) with: error: the `dns` target supports `tcp`, `udp`, or `any`, not `dns` The macro's parser advances past the `dns` target token only inside a `debug_assert_eq!`, which is compiled out of the release binary, so the same `dns` token is re-read as the protocol slot and rejected. This broke the balanced-egress baseline (ADR-0018), which emitted `allow@dns` under `--net-default deny`. Emit the expanded equivalent the macro is specified to produce — the gateway `host` group on UDP/TCP port 53 — via the ordinary, assert-free parse path, which is unaffected by the upstream bug: --net-rule allow@host:udp:53 --net-rule allow@host:tcp:53 Update the balanced-egress tests to assert the expanded rules and that the broken `allow@dns` macro is never emitted; document the upstream bug and workaround in KNOWN_FAILURE_MODES (#32), ADR-0018, and BACKEND_GUIDE. Verified: scripts/test-acq (759 passed, 0 failed); shellcheck clean. Co-authored-by: OpenCode [claude_4_8_opus] <bret.mogilefsky@gsa.gov>
|
Note this is all a seed for a larger epic. |
The balanced-egress baseline emitted the symmetric `--net-default deny`, which msb applies to BOTH directions. Published ports (`-p HOST:GUEST`) get no implicit ingress-allow, so every inbound connection fell through to the ingress default and was RST-rejected: a host `curl` to a create-time published port completed the TCP handshake via msb's host proxy, then reset on data (ERR_CONNECTION_RESET), even though the guest service was healthy and listening. Emit `--net-default-egress deny` instead. Egress stays deny-by-default + allowlist (ADR-0018's intent) while ingress keeps msb's baseline `allow`, so create-time published ports are reachable with no per-port rule. A future "strict" profile can layer ingress deny-default + explicit `allow:ingress@...` rules; the balanced default intentionally does not. Requires the `--net-default-egress`/`--net-default-ingress` split (confirmed on the pinned msb 0.6.8). - msb.sh: emit `--net-default-egress deny`; update baseline + DNS comments - test-acq: assert egress-only deny-default, and NOT a symmetric `--net-default deny` nor an ingress deny - ADR-0019: document the egress-only decision + strict-profile hook; amend ADR-0018; update BACKEND_GUIDE - scripts/verify-net-default-egress: live regression guard (create-time publish reachable + egress still locked)
Fix: create-time published ports were unreachable under the balanced-egress default (
|
Adversarial review — #295 (msb balanced egress)I read the emitter, the vendored host list, both ADRs, and the test deltas, then verified every upstream-msb claim against the microsandbox source on What I verified as CORRECT (receipts)
BLOCKING — version-floor gap makes the default break
|
Context
sbx ships a curated
balancednetwork policy (AI services, package registries, code/container hosts, cloud infra, OS package mirrors, and cert validation) and the sbx path in this repo assumes it. msb has no equivalent default — its low-level model is deny-by-default with an ordered rule list, and its high-level profiles (public/private/host) do not match the sbxbalancedhost set.Left alone, an
acq-created msb sandbox reached only the hosts the four built-in kits declare (api.gsa.usai.gov,api.github.com) plus npm when an agent is installed — dramatically more locked down than the equivalent sbx sandbox. A user who ranpip install,cargo build,apt-get, etc. inside an msb sandbox would fail on egress that "just works" on sbx.This branch gives every msb sandbox the same egress as sbx
balancedby default, composed on top of the kits' owncaps.network.allow. See ADR-0018 for the full decision record.I've also filed an upstream feature-request on
msbthat would make this way less messy.What changed
acq.backends/msb-balanced-hosts.txt, a human-auditable,#-commentable verbatim mirror of thehost:portrows fromsbx policy inspect local-policy. Kept un-translated so it stays trivially diffable against fresh sbx output; it is the single source of truth an operator edits when sbxbalancedchanges._acq_msb_balanced_rules_into(+ helpers_acq_msb_balanced_target/_acq_msb_balanced_port_ok/_acq_msb_balanced_parse_line) inacq.backends/msb.shtranslate each entry to an msb--net-ruleper the grammarallow[:egress]@<target>[:<proto>[:<ports>]]:**.host/*.host(sbx multi-label glob) → msb domain-suffix*.host(msb suffixes match the apex and any subdomain at any depth, so this loses no coverage).:PORT→:tcp:<port>; a host on both:80and:443yields two rules, preserved per-port.crl*.digicert.com(an intra-label glob msb cannot express) → broadened to*.digicert.com, with a one-time widening note (preserves CRL reachability on:80).--net-default deny. See the DNS note below.--net-default denyplus the generatedallow@…rules, so egress is restricted to the balanced set (deny-by-default + allowlist) — genuine parity with sbxbalanced, not "allow-all with extras".ACQ_MSB_BALANCED_EGRESS(default on) enables the baseline;0/false/no/off/empty disables it and falls back to kit-only egress (no deny-default emitted).ACQ_MSB_BALANCED_HOSTS_FILEoverrides the list path for a site-specific set.docs/BACKEND_GUIDE.md, anddocs/KNOWN_FAILURE_MODES.md(drift/re-sync procedure + the DNS-macro failure mode below).DNS: worked around an upstream msb bug
The baseline needs a gateway-DNS grant under
--net-default deny. It originally emitted msb's semanticallow@dnsmacro, which hard-failsmsb create/msb runon released msb (reproduced on 0.6.8):Root cause is an upstream msb parser bug, not an acq misconfiguration: the macro's parser advances past the
dnstarget token only inside adebug_assert_eq!, which is compiled out of a release binary — so the samednstoken is re-read as the protocol slot and rejected. A bareallow@dnstherefore fails on every release build that has the macro (0.6.7+).The adapter now emits the expanded equivalent — exactly what the macro is specified to produce, on the ordinary (assert-free) parse path:
Documented as failure mode 32 in
docs/KNOWN_FAILURE_MODES.md, with a quarterly re-verify note to collapse it back once upstream fixes the macro.Files
acq.backends/msb-balanced-hosts.txtbalancedhost list (verbatim sbx mirror)acq.backends/msb.shscripts/test-acqdocs/adr/0018-msb-balanced-egress-baseline.mddocs/BACKEND_GUIDE.mdACQ_MSB_BALANCED_EGRESS/ host-file env vars + egress modeldocs/KNOWN_FAILURE_MODES.mdallow@dnsmacro bug (32)Verification
./scripts/test-acq→ 759 passed, 0 failed. Covers target translation (**.,crl*, exact, single-label rejection), port validation, dual-port hosts, the expanded gateway-DNS rules (and that the brokenallow@dnsmacro is never emitted), malformed-line skipping, a full parse of the real vendored file with no skips, and the default-on /=0-off provision paths.shellcheck --severity=warning(one file per invocation, per the AGENTS.md hang note) — clean.msb run --net-default deny --net-rule "allow@dns" alpine -- truereproduces the upstream failure; the adapter's expanded rules avoid it.Live end-to-end validation — done (host, per ADR-0018 / ADR-0011)
The full
acq run→msb create→ egress-parity check needs a KVM-capable host and cannot run inside a sandbox (no nested sandboxes). Ran from the host on thereview/feat/msb-balanced-egresscheckout; all four observations confirmed:acq run/msb createthat previously aborted witherror: the dns target supports tcp, udp, or any, not dnsnow completes.allow@host:udp:53+allow@host:tcp:53rules do their job under--net-default deny.balanced-only host that no kit allows (e.g.pypi.org:443) is reachable from inside the sandbox.balanced, not allow-all).Rollback
Revert the branch (or, for just the DNS fix, revert
e1ecc1e). At runtime, setACQ_MSB_BALANCED_EGRESS=0to disable the baseline entirely and fall back to kit-only egress (no behavior change vs. pre-branch msb).Security impact
balancedset — but remains an allowlist under deny-by-default, not allow-all. Thecrl*→*.digicert.combroadening is the only place msb is intentionally wider than sbx, and it is logged.[A-Za-z0-9.*_-]), single-label suffixes (*.com) are rejected, and every port is integer-validated (1–65535) before it reaches argv.AI assistance
AI-assisted (OpenCode, model
claude_4_8_opus); see theCo-authored-by:trailer one1ecc1e. Human-owned and reviewed by@mogul.