Repository navigation
feat(docker): enforce pinned Linux egress allowlists - #132
Conversation
Add assertions for the execution_location accessor on the local and SSH hosts, and on the SSH host's inner host, so the new reporting behaviour is exercised for local, remote, and unknown cases. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
Introduce a HostExecutionLocation enum and an execution_location method on the Host trait so security-sensitive controllers can determine whether a host runs commands locally or remotely instead of inferring it from the host's registration name. Custom hosts default to Unknown until they explicitly attest to their locality. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
Implement execution_location on LocalHost so callers can tell that commands run on the local machine rather than a remote host. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
SshHost now implements execution_location and returns Remote, so callers can tell that commands run on the far machine rather than locally. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add tests that verify the egress controller prepares the allowlist before the container starts and cleans up after destroy, and that a controller failure rolls back the box record before it is dropped. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
Register the new egress_enforcement module and re-export DockerEgressController and NativeLinuxDockerEgressController so callers can enforce egress policy through the native Linux backend. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
The Docker sandbox now accepts an optional egress controller that can rewrite docker argv before execution, letting callers install per-box allowlist filtering. Without a controller the sandbox keeps refusing NetworkPolicy::Allowlist as before. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
The remove_container helper now passes its argv straight to run_docker instead of wrapping it with docker_argv, since the caller already supplies the full command. This removes a redundant layer of argument construction with no change in behaviour. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add a validate_host method to the DockerEgressController trait with a default no-op implementation, and have the native Linux controller check that the host is supported, the target is Linux, and the engine and firewall are available. Also skip forcing the local rootful docker env prefix when the command already starts with env. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
Wrap the host in a ControllerHost that rewrites docker argv through the egress controller before execution, so observed hosts and sandboxes with a controller attached get the same egress handling as the primary host. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
The docker sandbox now installs an egress filter through the egress controller when a box requests an allowlist network policy, instead of refusing the policy outright. The controller validates the host and prepares the default-drop filter before the container starts, and any failure during create tears the egress state back down so no filter is left behind. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
Reformat the egress enforcement and sandbox modules to satisfy rustfmt, wrapping long argument lists and error strings and collapsing short ones. No behaviour changes. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
The firewall output helper now converts the lossy stdout to an owned value before returning it, so the result no longer borrows from the command output. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
Failed container creates now run a shared rollback path that tears down egress rules and records the attempt for later cleanup when that teardown itself fails, instead of silently discarding the failure record. Egress enforcement also installs allowlist rules only on the forward chain and drops all host-origin traffic on the input chain, so allowlisted destinations no longer apply to traffic originating from the host. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
…identity Add a test asserting that the native Linux egress controller refuses to create a sandbox when the host has no explicit local identity, returning ConstraintNotEnforced for the network constraint without issuing any host commands. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
Update the test resolver to return std::io::Result so it matches the EgressResolver trait signature. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
Extend the live Docker test imports with BoxId, EgressAllowlist, and the egress controller types so the suite can exercise network policy enforcement against a real daemon. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add a live test that runs two listeners in a test-owned bridge container and checks the native firewall controller permits the allowlisted port while blocking other raw TCP, using a fixed resolver so no public service or Docker host path is involved. The test tears down the sandbox and listener container before asserting so failures do not leak resources. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add a unit test asserting that a failed egress cleanup keeps the box record so a later cleanup retry can find it, and extend the live allowlist test to prove that direct raw TCP to the host gateway is refused while the pinned destination still connects. The CI workflow now runs the new live test. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
The contract and runtime specs now describe the NativeLinuxDockerEgressController, which lets a verified local rootful Linux Docker Engine enforce Allowlist by pinning resolved addresses into default-drop IPv4/IPv6 filters before the workload starts. Other backends and unsupported daemon configurations continue to refuse Allowlist, and the allowlist semantics were reworded to clarify that filtering pins firewall authorization addresses rather than DNS answers. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
The native egress controller test now exercises both unknown and remote execution locations instead of only the default, and the scripted host gains a configurable execution location so the refusal path is verified for each case. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
Reformat the tinybox-core import list and the host execution location array in the sandbox tests to satisfy rustfmt. No behaviour changes. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
The live allowlist test now clears HTTP_PROXY and related environment variables before running its nc probes, so an inherited proxy setting cannot redirect the connections and mask the allowlist behaviour under test. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Tiny Sweeper reviewReviewed head: FindingsNo active actionable findings. Resolved this pass
Verdict: Ready for maintainer review · Priority: none Diagnostics and supporting detailsFull summaryPull request purpose (title): feat(docker): enforce pinned Linux egress allowlists. The whole-pull-request change adds an opt-in native Linux Docker Engine egress controller that resolves and pins allowlist destinations once, creates a per-box bridge, verifies the local bridge and IPv4/IPv6 DOCKER-USER/INPUT hooks plus Docker-assigned IPAM subnets, and installs default-drop filters before the workload starts; setup failure rolls back owned resources and retains a retryable record when cleanup also fails. The change also adds a HostExecutionLocation capability to the Host trait so the controller can refuse hosts that do not attest to local execution, normalizes IPv4-mapped IPv6 resolver answers and refuses IPv6 link-local targets, routes every controller Docker command through a single ControllerHost argv rewrite, and updates the contract and runtime specs. The default Docker sandbox still has no controller and refuses allowlists. The latest-push delta is unavailable, so the whole-pull-request diff is described rather than presented as a push delta. Unreviewed paths
Review snapshot
Completeness: Complete What changedLatest-push delta: unavailable. The review state has the prior reviewed head (a7daa81) and rendered evidence but no trusted base-to-head diff comparison, so the whole-pull-request diff is described below rather than presented as a push delta. Features
Tests
Before mergeNone. How this fits togetherflowchart LR
n0["PinnedEgressTarget<br/>changed"]:::changed
n1["normalize_host<br/>changed"]:::changed
n2["plan_egress_allowlist<br/>changed"]:::changed
n3["...sses_when_the_policy_does_not_forbid_them<br/>changed"]:::changed
n4["io"]:::impacted
n5["StaticResolver"]:::impacted
n6["EgressResolver"]:::impacted
n7["addresses"]:::impacted
n8["clone"]:::impacted
n2 -->|uses| n0
n2 -->|calls| n1
n2 -->|uses| n6
n2 -->|calls| n8
n3 -->|calls| n2
n3 -->|tests| n2
n3 -->|calls| n7
n3 -->|tests| n7
n5 -->|implements| n6
n6 -->|uses| n4
n7 -->|calls| n5
n7 -->|uses| n5
classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Agent review detailscritique
security
tests
commits
description
Evidence and run details
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7be9921765
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The cleanup path now always attempts to remove the Docker network instead of guarding the removal behind a prior existence check, since the removal command already tolerates a missing network. The Uuid import is also gated behind cfg(not(test)) to avoid an unused import warning in test builds. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
…cases The single atomic-chain test was replaced by two focused tests: one asserting that cleanup reclaims an empty attempt chain after a restart, and one asserting that cleanup refuses a nonempty unmarked chain and issues no delete, flush, or chain-removal commands. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
…reates The ownership check used by ambiguous network create reconciliation now also compares the attempt label, so a network left behind by an earlier create attempt for the same box is no longer mistaken for this attempt's network. Inspection and verification both parse the extra label and treat a mismatch as a foreign network to leave alone. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add a Linux-only test asserting that when a network create fails after its side effect, cleanup inspects the attempt label and leaves a network belonging to an older attempt untouched instead of removing it. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
The attempt-label assertion now spells out the full label string instead of relying on the shared constant, and two prepared native hosts are bound as mutable so the tests can adjust their state. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
Two test bindings had the wrong mutability, so the host is now declared mutable where its fields are assigned and immutable where they are not. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
Two test bindings had the wrong mutability, so the first now declares the prepared host as mutable and the second no longer does. This keeps the tests compiling without changing what they exercise. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
The README now documents that each attempt gets a fresh random token stored on the Docker network and folded into chain names, and that chains are created exclusively before receiving their owner marker. It also explains the reclaim rules for unmarked chains after a restart and that ambiguous orphans fail closed. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
The fake host now records which chains have rules added and reports missing rules for -C checks, so the egress enforcement tests can exercise the rule verification path. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
The test host now resolves the iptables or ip6tables binary once per invocation and reuses it for the marked-chain and chains-with-rules bookkeeping, instead of re-deriving it inline at each call site. A chains_with_rules removal was also added to the -D path so deletions stay consistent with insertions. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
The test host now clones the firewall identifier for the marked-chains removal and reuses the original value for the chains-with-rules removal, instead of re-deriving it from argv. This keeps both removals keyed on the same firewall value. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
Reformatted the egress enforcement module and its tests to satisfy rustfmt, wrapping long call chains and assertions and collapsing short conditions onto single lines. No behaviour changed. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
The firewall binary lookup was computed twice in a row, so the first binding was dropped and the second reused. The remaining binding is now cloned at the insertion site to keep the value available. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
The cleanup test now also records the forward and input chains as having rules and carrying the owner marker, so the refusal path is exercised against chains that look genuinely owned by another instance. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
Rename the test to make clear it covers rollback of both the created chain and the network when the owner marker install fails. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add a Linux-only test asserting that cleanup succeeds and issues only the two expected chain-existence probes when the box network is already absent and no matching hooks remain, without running any delete, flush, or remove commands. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
The cleanup test asserted exactly two `-S` invocations, which is brittle against extra read-only listing calls. It now checks that at least two occur, keeping the intent while tolerating additional chain inspections. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add a test that makes the owned network removal fail after an ambiguous create, verifying the cleanup error is retained and the network is inspected twice. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add a fault injection hook to the prepared native host so a single `docker network rm` invocation can be made to fail, letting tests exercise the cleanup path when network removal errors out. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
The ambiguous create rollback test now binds the prepare result and includes it along with the recorded commands in the assertion message, so a failure shows what the controller actually returned instead of only the mismatch. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
The ambiguous create test now asserts on the simulated lost command response message instead of the owned egress network rollback failure, so the assertion matches the error the test actually injects. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
The test now compiles the network-removal assertion only on Linux, since the behaviour it exercises is platform-specific and broke builds elsewhere. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
The no_matching_hooks field and its initializer are now behind a cfg(target_os = "linux") attribute, matching the other Linux-only fields in PreparedNativeHost. This avoids an unused-field warning on other platforms. Auto-committed-on: cuttlefish Co-authored-by: Medulla <medulla@tinyhumans.ai>
b9a5a70
into
tinybox-27-pinned-egress-plan
Refs #27
Part of tinyhumansai/openhuman#7328
Stacked on PR #131 (
tinybox-27-metadata-ip-alias), which carries the earlier TinyBox #27 phases.Scope
Adds opt-in native Linux Docker Engine allowlist enforcement through
NativeLinuxDockerEgressController. The controller resolves and pins every hostname once, creates a per-box bridge, verifies the bridge in the local namespace and both IPv4/IPv6 FORWARD-to-DOCKER-USER hooks, inspects the assigned IPAM subnets, then installs default-drop firewall rules beforedocker run. It refuses and rolls back if any pinned destination falls inside the new Docker bridge subnet, preventing on-link ARP routing around the intended gateway path.The filter permits only pinned TCP destination IP/port pairs. It allows established/related return traffic for host-initiated connections, including published-port requests. New traffic from the managed bridge to the Docker host is dropped, except IPv6 Neighbor Solicitation needed for address resolution. Metadata and loopback destinations are refused by the planner and explicit firewall rules; other link-local destinations can be allowed by the pin plan. IPv6 link-local destinations (
fe80::/10) are refused as Unsupported because the current pinning model cannot preserve an interface scope ID; IPv4 link-local destinations remain eligible for explicit allowlisting.Each prepare attempt gets a fresh token stored as a Docker network label and incorporated into its firewall chain names. Cleanup verifies the network owner, scope, and attempt labels and preflights all chain markers before changing firewall state. If a process exits after exclusive chain creation but before writing its marker, a later cleanup can reclaim that chain only when the matching network labels prove its attempt, the chain is empty, and no firewall hook references it. Nonempty or unverified orphan chains are left untouched. Setup rollback tracks only chains this attempt created, so marker-install failure cannot flush another chain. Failed rollback keeps a retryable record; ordinary network-create conflicts do not trigger cleanup of another box's resources. Ambiguous create transport failures are reconciled only when inspected owner, scope, and attempt labels match the current attempt; nonzero Docker command results preserve the original error without reconciliation.
The chain marker uses a length-delimited SHA-256 token over namespace, box ID, and attempt (
sha2is a direct dependency for this purpose). The sandbox applies each controller Docker-argv rewrite once throughControllerHost;run_dockerno longer applies it a second time. A non-idempotent test controller pins this contract.Pinned hostname/IP pairs are added to the container's static
/etc/hosts; container DNS points only to its own loopback resolver, with no Docker embedded DNS forwarding fallback. This uses Docker's per-container DNS setting as documented in Docker networking, and addresses the container-network DNS forwarding issue described in Moby GHSA-mq39-4gv4-mvpx. Unlisted names do not resolve through an upstream service.Docker Desktop, Windows, remote/unknown hosts, rootless daemons, and unavailable or unverifiable firewall support remain Unsupported. Native filter behavior tests are Linux-only; non-Linux targets test refusal before host commands. The default Docker sandbox continues to refuse allowlists without an explicitly installed controller.
Verification
Fake-Host tests cover pinned rule construction and ordering, cleanup ownership and retry, empty orphan recovery and nonempty orphan refusal after restart, bridge-link and firewall-hook verification, marker-install rollback, TCP-only rules, DNS arguments, persisted allowlist destroy refusal, IPv4 and IPv6 subnet-overlap rejection with network rollback, and create-response reconciliation for current-attempt, old-attempt, absent, foreign, malformed, and unavailable inspection results. A non-idempotent controller test confirms exactly one argv rewrite.
The live proof runs only in the hosted Ubuntu Docker job. It checks an allowlisted hostname over IPv4 and probes the resolved IPv6 literal directly, blocks a different raw TCP port, blocks an explicitly allowed host-gateway listener, blocks unlisted DNS and raw UDP/53, verifies a published-port TCP response, and checks that forbidden listeners were not hit. Every successfully created test box is destroyed even if a probe or assertion fails. No firewall-mutating test was run on the shared development host; hosted Ubuntu is the only real firewall proof.
Local validation: formatting, strict workspace Clippy,
cargo test --all-features, andcargo doc --no-deps --all-featureswithRUSTDOCFLAGS=-D warningspass. Focused egress tests pass (27/27).cargo llvm-cov --package tinybox-docker --libreportsegress_enforcement.rsat 91.75% line coverage (845/921). The fullscripts/check-coverage.shlocal run hit a Rust 1.99.1 compiler ICE (no resolution for an import) during coverage compilation; the hosted exact-head coverage job will provide the full-workspace result. Windows-target compilation, cargo-deny, and all-target build passed on the immediately preceding implementation head.The Tokio lockfile is updated to 1.53.3 for RUSTSEC-2026-0386 (fixed starting at 1.53.3).