[Senate] Egress allowlist firewall for sandboxed analyses done

← Resource Governance
Replace denylist sandbox_audit with hard nftables allowlist bound to sandbox cgroup; pinned-DNS, fail-closed, in-sandbox self-test.

Completion Notes

Auto-completed by supervisor after successful deploy to main

Git Commits (1)

[Senate] Egress allowlist firewall for sandboxed analyses [task:736c64b9-c24d-4a56-9497-92303cea908e] (#737)2026-04-27
Spec File

Goal

scidex/senate/cgroup_isolation.py plus the bwrap launcher
(scidex/senate/sandbox_audit.py is the in-process detector but does
not actually block) currently rely on a denylist posture for outbound
traffic — sandbox_audit logs net_connect events and the watchdog kills
runaways post-hoc. That is too late for the prompt-injection / token-
exfiltration threat model: a malicious notebook need only one
successful POST to leak an OAuth token. This task replaces the denylist
with a hard allowlist nftables ruleset bound to the sandbox UID/cgroup
and verifies it from inside the sandbox at runtime.

Effort: thorough

Acceptance Criteria

☐ New file scidex/senate/sandbox_egress.py:
- build_allowlist(profile: str) -> list[str] returns the FQDN
list for a profile. Profiles defined in
scidex/senate/sandbox_egress_profiles.yaml:
- defaultpubmed.ncbi.nlm.nih.gov,
eutils.ncbi.nlm.nih.gov, api.semanticscholar.org,
api.openalex.org, clinicaltrials.gov, rest.uniprot.org,
ebi.ac.uk, pypi.org (read-only), files.pythonhosted.org,
github.com (read-only via 443), raw.githubusercontent.com,
api.github.com.
- notebookdefaulthuggingface.co, cdn-lfs.huggingface.co.
- none — empty list, used for pure-CPU statistical analyses.
- apply_nft_ruleset(profile, sandbox_uid, sandbox_cgroup) -> str
renders an nft script that drops all egress for the cgroup
except DNS (53/udp to localhost), loopback, and TCP/443 to the
resolved A/AAAA records of the FQDN list. Returns the ruleset
text; caller writes to /etc/nftables.d/scidex-sandbox-<id>.nft
and nft -fs it.
- tear_down(sandbox_id) removes the ruleset on completion.
☐ DNS resolution is done once per sandbox launch and pinned;
the policy is L4 IP+port, not L7. This prevents
DNS-rebinding attacks. Re-resolution every 6h (env
SCIDEX_EGRESS_REPIN_INTERVAL) for long-lived sessions.
☐ Integration with cgroup_isolation.isolated_run:
pass egress_profile='default' (default) / 'notebook' / 'none'
kwarg; call apply_nft_ruleset before the bwrap exec and
tear_down in the finally block.
In-sandbox self-test — every isolated_run starts by hitting
one allowed host and one explicitly denied host
(api.evil.example); the launcher fails the run if the deny test
succeeds. Bound at < 200 ms.
☐ Migration migrations/20260428_sandbox_egress_event.sql — new
table sandbox_egress_event(id BIGSERIAL, analysis_id TEXT, ts
TIMESTAMPTZ, action TEXT CHECK (action IN ('allow','deny','panic')),
remote_ip INET, remote_port INT, fqdn_pinned TEXT, raw_pkts INT)
.
Populated by an nft monitor trace background reader (best-effort —
missing rows are not fatal).
Fail-closed: if the nftables apply call fails (no privileges,
kernel module missing), the launch is aborted and the analysis
is marked failed_safety_setup rather than running with no
firewall. There is no "fall back to denylist" path.
☐ Senate dashboard tile "Egress denies (24h)" enumerated by
remote_ip; non-zero rows mean a sandbox attempted exfiltration
and should auto-spawn a Senate review task (reuse the helper in
scidex/senate/sandbox_audit.py:emit_review_task).
☐ Tests tests/test_sandbox_egress.py: ruleset rendering snapshot
test, FQDN-resolution mock, profile lookup, apply-failure
fail-closed path, self-test pass/fail simulation.

Approach

  • Read cgroup_isolation.py and sandbox_audit.py end-to-end; locate
  • the bwrap exec call so the nft apply lands immediately before it.
  • Author the YAML profile catalogue with explicit rationale per host
  • (a comment naming the tool that needs it: PubMed → pubmed-search
    skill, OpenAlex → openalex-works, etc.).
  • Build the renderer; snapshot-test the rendered nft text.
  • Wire into isolated_run; add the in-sandbox self-test as a tiny
  • shell preamble baked into the bwrap command.
  • Implement tear_down; verify in a teardown unit test that the
  • ruleset file is removed and nft list ruleset no longer matches.
  • Add the trace-reader background thread; failure to start it logs a
  • WARNING but does not fail the run (we still have nftables itself
    denying — the trace is for forensics).
  • Smoke: launch a notebook that tries to curl https://api.evil.example
  • — expect connection refused and a sandbox_egress_event row with
    action='deny'.

    Dependencies

    • scidex/senate/sandbox_audit.py (already shipped) — provides the
    review-task emit helper this task reuses.

    Dependents

    • q-safety-runaway-circuit-breaker — uses egress denial counts as one
    signal of "agent is misbehaving".

    Work Log

    2026-04-27 15:30 UTC — Slot 0 (minimax:72)

    • Implemented all acceptance criteria:
    - Created scidex/senate/sandbox_egress_profiles.yaml with default, notebook, and none profiles
    - Created scidex/senate/sandbox_egress.py with build_allowlist, apply_nft_ruleset, apply_egress_ruleset, tear_down, egress_self_test, record_egress_event, get_recent_egress_denies, emit_egress_review_task, check_and_emit_egress_review_tasks
    - Created migrations/20260428_sandbox_egress_event.sql with sandbox_egress_event table
    - Integrated egress_profile kwarg into isolated_run, isolated_analysis_run, run_analysis_isolated in cgroup_isolation.py
    - Added self-test wrapper (_egress_self_test_command, _build_run_command) that runs inside sandbox
    - Added Security tile to Senate dashboard (_build_senate_page) showing egress denies by IP
    - Created tests/test_sandbox_egress.py with 30 passing tests
    • Key design decisions:
    - nft rules use ip[6] cgroupv2-path to match sandbox cgroup
    - DNS pinning: resolved IPs cached per-process with SCIDEX_EGRESS_REPIN_INTERVAL (6h)
    - Fail-closed: EgressSetupError raised if nft apply fails, aborting launch
    - Self-test: inline Python script runs && before actual command inside sandbox
    - Ruleset written to /etc/nftables.d/scidex-sandbox-<id>.nft
    • Tests: all 30 pass (0.16s)
    • Pre-existing syntax error in api.py at line 35657 (in main, not introduced by this task)

    Sibling Tasks in Quest (Resource Governance) ↗