feat(runtime): user-namespace remap behind a default-off flag (#126, phase 1) - #181
feat(runtime): user-namespace remap behind a default-off flag (#126, phase 1)#181luthermonson wants to merge 3 commits into
Conversation
…phase 1)
Adds the mechanism to run each runner container in a REMAPPED user
namespace so container uid 0 maps to a high, unprivileged host uid
instead of real host root. This is the structural layer beneath the
capability/seccomp/AppArmor containment: with it on, a container escape
(runc CVE, kernel LPE, the dind bind TOCTOU) lands as an unprivileged
host uid owning nothing, not as root on the VM.
Phase 1 — the core, safe to land:
- config: [runtime.userns] { enabled, base_uid, base_gid, size },
DEFAULT OFF. Resolved() supplies base 1000000000 / size 65536 so an
enabled-but-unconfigured pool can never map container root back onto
host uid 0 (which would be no remap at all).
- spec: oci.WithUserNamespace(uid/gid maps) via usernsSpecOpts, Linux
only, appended with the other spec-hardening opts.
- snapshot: client.WithRemapperLabels on WithNewSnapshot so the rootfs
snapshot presents mapped ownership (idmapped mount where the overlay
snapshotter supports it). FAIL-CLOSED: a host that cannot honor the
remap fails the snapshot prepare and the job errors, rather than
silently running unmapped with container root == host root.
Default-off means this is a no-op for every existing pool. Enable it on
one pool to validate on real hardware, per the issue.
NOT yet covered (follow-up, gated behind the same flag, needs on-metal
validation): the per-job runner overlay lowerdir ownership for
custom-image jobs, dind sibling-container mapping consistency and the
bind-source chown in pkg/dind, and the macOS virtio-fs path. Standard
Linux image jobs (runner pre-installed in the image, no runner-mount
overlay) are covered by the snapshot remap alone.
Tests: usernsSpecOpts mapping/namespace assertions (incl. the
resolved-default guard against a degenerate map onto uid 0); config
load + Resolved defaults + the load-bearing default-off assertion.
Review of #181 found the Userns field was only threaded into ONE of the two runtime.New(runtime.Config{}) construction sites — the in-VM worker branch. The host/native-Linux serve path (cmd/ephemerd/main.go:641), which is what actually runs job containers on a Proxmox Linux node, was missing it. A prior replace_all matched only the 3-tab-indented site, not the 2-tab one. Effect (no safety impact, but defeats the feature): enabling [runtime.userns] on a native-Linux pool left the value stranded at the zero (disabled) value, so every job ran UNMAPPED with no error and no log — the silent-no-op the fail-closed design is meant to avoid, and it would make on-metal validation on a Linux pool validate nothing. The missing field defaults OFF, the safe direction, so default-off integrity is unaffected. Also: collapse the double Userns.Resolved() call in the create log to one local, and add TestUsernsSnapshotOpts asserting the remapper labels are absent when disabled and carry the same base+size as the spec maps when enabled (guards a transposed/half-applied remap that unit-passes today).
|
Review: verified the two properties this PR promises. (1) Genuinely default-off: RuntimeUserns.Enabled is a plain bool whose zero value is false, an omitted [runtime.userns] block loads as off (pinned by TestLoad_RuntimeUserns_OmittedIsOff), and Resolved() never flips Enabled. (2) Inert when off: usernsSpecOpts/usernsSnapshotOpts both return nil for Enabled=false on every platform, so the OCI spec and WithNewSnapshot calls are byte-identical to today's; the only Create-path change when disabled is a variadic no-op. The enabled path's spec maps and snapshot remapper labels use the same base+size (pinned by test), and the fail-closed behavior on a snapshotter without idmapped-mount support is the right posture for a security control. Not merging right now for one reason only: after updating the branch with main, the refreshed CI run (33465274049) is stuck queued because the runner fleet currently has zero online runners (org runner list is empty; CI for #184 and the post-merge image build for #183 are queued behind the same outage). The pre-update pull_request run (32690416072) was fully green including the Windows and macOS build legs, and the branch is conflict-free with main. The Build (Windows amd64) failure visible in this PR's checks came from the v0.2.7-rc1 Release run and is runner-infra death ("A device which does not exist was specified" on the runner file commands), not a code failure. Once the fleet is back and this run goes green, this is mergeable as-is. |
Implements the mechanism for issue #126 — running each runner container in a remapped user namespace so container uid 0 maps to a high, unprivileged host uid instead of real host root. This is the structural layer beneath the capability/seccomp/AppArmor containment: with it on, a successful container escape (a runc CVE, a kernel LPE, the dind bind TOCTOU) lands as an unprivileged host uid owning nothing, rather than as root on the VM.
Phase 1 (this PR) — the core, safe to land default-off
[runtime.userns] { enabled, base_uid, base_gid, size }, DEFAULT OFF.Resolved()supplies base1000000000/ size65536, so an enabled-but-unconfigured pool can never map container root back onto host uid 0 (which would be no remap).oci.WithUserNamespace(uid/gid maps)viausernsSpecOpts, Linux-only, appended with the other spec-hardening opts.client.WithRemapperLabelsonWithNewSnapshot, so the rootfs snapshot presents mapped ownership (idmapped mount where the overlay snapshotter supports it). Fail-closed: a host that can't honor the remap fails the snapshot prepare and the job errors, rather than silently running unmapped with container root == host root.Default-off ⇒ a no-op for every existing pool. Validation is by enabling it on one pool on real hardware, exactly as the issue prescribes.
Not in this PR — follow-up, gated behind the same flag, needs on-metal validation
createUnder/translateBindSourceinpkg/dind), socontainer:/-vjobs work under the remap;ContainerDataDirpath.Standard Linux-image jobs (runner pre-installed in the image, no runner-mount overlay, no dind) are covered by the snapshot remap alone — that's the intended first canary target.
Tests
usernsSpecOpts: mapping + user-namespace assertions, incl. a guard that an enabled-but-unconfigured config resolves to a real base (never a degenerate map onto uid 0).Resolved()defaults + the load-bearing default-off assertion (an accidental default-on would remap every job on upgrade).Validated: builds + vet clean on linux/windows/darwin (amd64+arm64); config tests pass; runtime test binary compiles. The Containment suite's
#126userns check is expected to flip PASS on a pool with[runtime.userns] enabled = truerunning a standard-image job.