Skip to content

feat(runtime): user-namespace remap behind a default-off flag (#126, phase 1) - #181

Open
luthermonson wants to merge 2 commits into
mainfrom
feat/userns-remap-126
Open

feat(runtime): user-namespace remap behind a default-off flag (#126, phase 1)#181
luthermonson wants to merge 2 commits into
mainfrom
feat/userns-remap-126

Conversation

@luthermonson

Copy link
Copy Markdown
Contributor

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

  • 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).
  • specoci.WithUserNamespace(uid/gid maps) via usernsSpecOpts, Linux-only, appended with the other spec-hardening opts.
  • snapshotclient.WithRemapperLabels on WithNewSnapshot, 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

  • per-job runner-overlay lowerdir ownership for custom-image jobs (shared RO runner tree must present mapped ownership);
  • dind sibling-container mapping consistency + the bind-source chown (createUnder/translateBindSource in pkg/dind), so container: / -v jobs work under the remap;
  • the macOS virtio-fs ContainerDataDir path.

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).
  • config load + 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 #126 userns check is expected to flip PASS on a pool with [runtime.userns] enabled = true running a standard-image job.

…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).
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