Skip to content

test(ci): adversarial containment suite per arch/OS - #134

Open
luthermonson wants to merge 13 commits into
mainfrom
test/containment-suite
Open

test(ci): adversarial containment suite per arch/OS#134
luthermonson wants to merge 13 commits into
mainfrom
test/containment-suite

Conversation

@luthermonson

Copy link
Copy Markdown
Contributor

Gap: nothing verifies containment on real hardware. pkg/runtime/hardening_test.go inspects the OCI spec we ask for; the three smoke workflows print \uname -a. The spec and reality diverge exactly when it matters — an AppArmor profile that failed to load, a knob an older daemon parsed and ignored, a node that missed a release.

What it does: every step asserts the SAFE outcome and exits non-zero on the unsafe one. Probes, not exploits — each checks the door is locked rather than walking through it.

Linux (x64 + arm64): dangerous capabilities absent from CapEff (the kernel's effective set, not the request); seccomp in filter mode; AppArmor confined not unconfined; host ephemerd config/PEM/containerd socket unreadable; /proc/1/root not traversable; /proc/sys, sysrq-trigger, uevent_helper, cgroup release_agent not writable; raw devices and mknod denied; \docker run --privileged\ refused; -v /:/hostroot\ cannot read the host config; Proxmox/Incus/Grafana unreachable through the container firewall; /actions-runner read-only.

Windows: Hyper-V isolation active (not process isolation), host ephemerd files unreachable, management planes blocked. macOS: job is inside a per-job VM rather than the bare mini, host credentials unreachable.

When to run: after every fleet uplift, as the evidence that a release's security changes reached the metal — plus weekly, so drift is caught without a release. The highest-value single check is host \config.toml\ readability: it carries the GitHub PAT and the cloudflared tunnel token.

luthermonson added a commit that referenced this pull request Aug 7, 2026
…ners (#136)

Windows jobs could reach the fleet's management planes (Incus, Grafana)
because installFirewallRules was a no-op on Windows and the per-endpoint
HNS/VFP ACLs alone did not contain a Hyper-V-isolated job on the real
runner (found by the containment suite, #134).

Implement the Windows counterpart of firewall_linux.go: Windows Defender
Firewall rules installed on the host, where WinNAT routes every container
flow regardless of vSwitch policy. Outbound blocks cover RFC1918 +
link-local with the container subnet (gateway, DNS, NAT, container-to-
container) subtracted via range arithmetic — Windows Firewall has no rule
ordering and Block beats Allow, so the gateway can never appear inside a
blocked range. Inbound rules block container→gateway traffic on the
ephemerd control ports, mirroring controlPlaneInputRules.

Rules are named ephemerd-egress-* and installed delete-before-add, so
reinstallation is idempotent and Cleanup removes the exact set. Install
failure surfaces as an error that callers already treat as a warning, so
a host without firewall privileges degrades to the endpoint ACLs instead
of refusing to start.

Closes #135
pkg/runtime/hardening_test.go asserts the OCI spec we ASK for; the smoke workflows only print uname. Nothing verified that containment actually holds on real hardware -- which is where an AppArmor profile that failed to load, a knob an older daemon parsed and ignored, or a node missing a release silently differs from the spec.

Each step asserts the SAFE outcome and exits non-zero on the unsafe one (probes, not exploits). Linux x64+arm64 cover capabilities via CapEff, seccomp filter mode, AppArmor confinement, host-filesystem and ephemerd-credential reachability, /proc + /sys + cgroup writability, raw devices and mknod, privileged-container refusal, dind bind-mount translation, and firewall isolation from the Proxmox/Incus/Grafana management planes. Windows asserts Hyper-V isolation and host config unreachability; macOS asserts the job is in a VM rather than on the bare mini.

Runs on dispatch, on changes to itself, and weekly so drift is caught without a release. Intended to run after every fleet uplift as the evidence a release's security changes reached the metal.
The Proxmox probes used a guessed 192.168.11.10, which resolves to nothing — a probe that always passes is worse than no probe, because it reads as coverage. coyotes is 192.168.5.1 and kings 192.168.5.2; both are now probed on 8006.
The existing linux job checks doors are locked; this adds a breakout job
that rattles the handles — each step runs a concrete escape and fails if
it WORKS. Aimed at what just shipped:

- no_new_privs (#128): a setuid-root helper must not reach euid 0
- AppArmor (#124): mount(2) and sysrq-trigger writes must be DENIED, not
  merely that a profile is attached
- cgroup release_agent: the canonical escape, incl. mounting a fresh
  hierarchy to get a writable one
- core_pattern: |host-binary crash handler must be unwritable
- user-namespace cap regain: unshare -Ur must not yield a working mount
- /dev/kmsg + dmesg: host kernel log must not leak
- containerd socket: must not be reachable from a job

Separate job so a regression reads as 'an escape opened' rather than 'a
policy check drifted'. x64 + arm64.
@luthermonson
luthermonson force-pushed the test/containment-suite branch from ece62bd to 8fe7736 Compare August 8, 2026 19:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant