spike(macos): rootless containers in bottles — Docker impossible, podman works #449
Closed
didericis-codex
wants to merge 5 commits from
spike/rootless-docker-macos into main
pull from: spike/rootless-docker-macos
merge into: didericis:main
didericis:main
didericis:prd-secret-provider-encrypted-env
didericis:prd-nested-containers
didericis:feat/encrypted-egress-secrets
didericis:fix/ci-coverage-artifact-paths
didericis:claude-forward-host-credentials-rebased
didericis:fix-gateway-gitleaks-arch
didericis:fix/websocket-response-dlp-multitenant
didericis:orchestrator-agent-compose
didericis:orchestrator-gateway-ca
didericis:orchestrator-consolidated-launch
didericis:orchestrator-gateway-provision
didericis:orchestrator-gateway-network
didericis:orchestrator-client
didericis:orchestrator-gateway-net
didericis:orchestrator-gitgate-provision
didericis:orchestrator-registration
didericis:orchestrator-lifecycle
didericis:orchestrator-supervise-writers
didericis:orchestrator-supervise-multitenant
didericis:orchestrator-gitgate-multitenant
didericis:orchestrator-rename-gateway
didericis:orchestrator-slice8
didericis:orchestrator-slice7
didericis:orchestrator-slice6
didericis:prd-orchestrator
didericis:orchestrator-slice5
didericis:orchestrator-slice4
didericis:orchestrator-slice3
didericis:orchestrator-slice2
didericis:firecracker-backend
didericis:forge-native-integration
didericis:prd-smolmachines-linux
didericis:prd-egress-control-plane
didericis:manifest-break-import-cycle
didericis:dlp-supervise-quality-fixes
didericis:table-drive-dlp-tests
didericis:fix-integration-test-failures
didericis:fix/macos-container-relative-dockerfile
didericis:prd-0054-install-script
didericis:commit-bottle-state
didericis:pr-211
didericis:move-codex-auth-to-contrib
didericis:feat/pipelock-skip-scan-extensions
didericis:prd-0049-named-labelled-agents
didericis:harden-git-gate-shell-rendering
Labels
Clear labels
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Status/Needs Triage
Breaking change that won't be backward compatible
Something is not working
Documentation changes
Improve existing functionality
New functionality
This is security issue
Issue or pull request related to testing
Priority
Critical
1
The priority is critical
Priority
High
2
The priority is high
Priority
Low
4
The priority is low
Priority
Medium
3
The priority is medium
Reviewed
Confirmed
1
Issue has been confirmed
Reviewed
Duplicate
2
This issue or pull request already exists
Reviewed
Invalid
3
Invalid issue
Reviewed
Won't Fix
3
This issue won't be fixed
Status
Abandoned
3
Somebody has started to work on this but abandoned work
Status
Blocked
1
Something is blocking this issue or pull request
Status
Need More Info
2
Feedback is required to reproduce issue or to continue work
Awaiting initial classification
No Label
Milestone
No items
No Milestone
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: didericis/bot-bottle#449
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "spike/rootless-docker-macos"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Spikes guest-local rootless Docker for macOS Apple-container bottles behind an opt-in
docker_access: truemanifest flag.Unlike Docker socket sharing, this keeps the daemon, Compose services, images, volumes, and networks inside the existing per-bottle VM. It does not mount Docker Desktop's socket, expose Apple's
containerAPI, or addNET_ADMIN,NET_RAW,SYS_ADMIN, or--privilegedto the outer bottle.Shape
dockerd-rootless.shas the unprivilegednodeuser after source-IP/token registration;docker_accessthrough manifest validation and extends merging.Validation
git diff --check: clean./var/run/docker.sock, and denial of direct unproxied child egress.Known spike boundary
This workspace is Linux, so the Apple Container kernel/user-namespace feasibility test could not run here. On a live Mac:
The PR should not be treated as production-supported until that test passes. If Apple Container requires any forbidden outer capability, the spike is designed to fail rather than weaken the bottle boundary.
Closes neither the broader capability issue nor the remote-worker fallback.
Refs #392.
@@ -0,0 +54,4 @@log=/tmp/bot-bottle-rootless-docker.lognohup dockerd-rootless.sh \--storage-driver=fuse-overlayfs \>"$log" 2>&1 </dev/null &@didericis-claude This was only needed for the rootless docker test, correct? podman doesn't require this? Further clarify whether podman required anything in this PR and whether this issue is worded correctly/podman would enable docker compose (minus the token issue)
Spike conclusion: rootless Docker is impossible here; we're going with podman
Pushed as
6a22b9f(research notes) and55def37(implementation). Full write-ups indocs/research/rootless-docker-in-apple-container-spike.mdanddocs/research/egress-proxy-oom-on-large-downloads.md.Why rootless Docker cannot work
Not a packaging gap. Apple Container's capability bounding set omits
CAP_SYS_ADMIN:The kernel gates writing a multi-range
uid_maponfile_ns_capable(file, ns, CAP_SYS_ADMIN).newuidmapis setuid-root, but its capabilities are clamped by that bounding set, and the"creator euid == ns owner" shortcut in
cap_capable()doesn't apply (the namespace is createdby uid 1000 while
newuidmapruns as euid 0). Result:EPERM.Everything else was already correct — setuid bit intact through the OCI export,
/notnosuid,NoNewPrivs: 0, no seccomp, subuid/subgid populated, initial userns,unshare -U -rworking.Adding
--cap-add CAP_SYS_ADMINclears it instantly, which is precisely what we don't want to do:it's roughly "root" in practical terms and would undercut the containment the backend is built on
(
--cap-drop CAP_NET_RAW, noNET_ADMIN, host-only agent network).This is the same constraint that makes upstream's
dind-rootlessrequire--privileged.Why podman works
With no subordinate UID range configured, podman falls back to a single-UID self-mapping,
which an unprivileged process may write itself —
newuidmapis never invoked. So the image buildstrips
/etc/subuidand/etc/subgidrather than populating them.Verified live on macOS 26.5.1 / Apple Container 1.0.0, zero added capabilities: service starts,
docker compose up -d --waitpulls and serves the workspace on a published port,podman inforeports rootless, no host Docker socket, and a nested container cannot reach the network outside
the egress path. Agent-facing surface is unchanged —
dockeranddocker composetalk to podman'sDocker-compatible API socket.
The tradeoff to be explicit about
The single-UID mapping means nested containers give no isolation from the agent itself —
rootinside a nested container is the agent user outside it. It cannot escalate to bottle-root (namespaced
capabilities only apply to UIDs mapped into the namespace, and only uid 1000 is), but it can do
anything the agent can: read the workspace, read
~/.claude, etc. Nested containers are a build/testconvenience, not a security boundary. The bottle remains the boundary.
Three defects found on the way, none of them about containers
mitmdumpgrow to ~1GB RSS and get OOM-killed(
Memory cgroup out of memory: Killed process 118 (mitmdump) ... anon-rss:997768kB). The gatewayis a per-host singleton with no restart-on-death, so one bottle's pull removes egress for every
bottle until the container is recreated. Documented, not fixed — the root-cause fix (a body-size
threshold on scanning) is a security decision, since "skip above N" trades a DoS for a covert channel.
Authorizationunconditionally, and Docker Hub/GHCR need a client-fetched, per-scope bearer token. Measured in a
bottle: token fetch
200, manifest request with that valid token401, manifest request withno auth
401— identical, so the header never arrives. Registries that skip the token dance(quay
200, MCR200, registry.k8s.io307) work today with a plain route; the test uses quay.docker run ... 'exit 7'returns0. podman 5.4.2 returns 7. This silently defeated the acceptance test's containment assertion,
which now checks an in-band marker instead. Base bump tracked separately (#451).
Replying to your review comment on
rootless-docker-init.sh."This was only needed for the rootless docker test, correct? podman doesn't require this?"
Correct, and podman requires the exact opposite. That script asserted a subordinate range existed:
The podman init asserts one does not:
A subuid range is precisely what pushes podman onto
newuidmap, which is the call that cannotsucceed in this guest. The file itself is deleted in this PR and replaced by
rootless-podman-init.sh."Clarify whether podman required anything in this PR"
Yes — six things, none of them an outer privilege:
/etc/subuidand/etc/subgidfor the agent user (the single-UID fallback).chmod 0666 /dev/fuse /dev/net/tun, as root inside the bottle at launch. Apple Containercreates both nodes
0600 root:root./dev/fuseblocks the fuse-overlayfs storage driver(otherwise you're on
vfs, which copies whole layers per container);/dev/net/tunblocksslirp4netns, which rootless podman uses for the default bridge network that stock compose files
expect. Both need no capability the bottle doesn't already hold — categorically different from
CAP_SYS_ADMIN.storage.conf— overlay +mount_program=fuse-overlayfs+ignore_chown_errors=true. Thelast one is required, not cosmetic: with a single-UID mapping there is no second UID for image
layers to be chowned to.
containers.conf—cgroups="disabled",cgroup_manager="cgroupfs",events_logger="file".No cgroup delegation or journald socket reaches this guest.
podman system servicefor the Docker-compatible API socket, sodockeranddocker composekeep working unchanged.
podman,fuse-overlayfs,slirp4netns,uidmap, plus thedockerCLI and thecompose plugin copied from
docker:28-cli.Note
netns="host"incontainers.confdoes not propagate through the compat API — stockdocker runand compose files request bridge explicitly — so slirp4netns (and the tun chmod) isrequired for ordinary compose files to work at all.
"Would podman enable docker compose (minus the token issue)?"
Yes, verified live —
docker compose up -d --waitpulls an image, starts a service, and serves theworkspace on a published port from inside a registered bottle. Three caveats worth knowing:
strip (the "token issue"). quay/MCR/registry.k8s.io and unauthenticated internal registries work
with a plain route today.
nested container failed entirely (
wget: bad address 'quay.io',nslookup→ connection refused).So compose services that need egress won't work yet — they'd need proxy env, the CA, and DNS
wired into the nested container. This is fail-closed, which is the right default, but it does
bound what "enables docker compose" means today: running images and local services, yes; nested
containers calling out, not yet.
uses a tiny busybox image specifically to avoid tripping that.
Is #451 worded correctly?
Mostly, but three things I'd tighten:
(Debian 13) is current stable. Suggest "on Debian 12 (bookworm); should be Debian 13 (trixie)".
node:22-trixie-slimexists, so it's a one-line change in each ofcontrib/{claude,codex,pi}/Dockerfile— those three are the only images on bookworm; the gatewayand orchestrator are already on trixie via
python:3.12-slim.upstream GA is 6.0.1 (6.0.0 GA 2026-06-24), but no distro packages it, upstream is still actively
patching the 5.8 line (5.8.5 shipped the same day as 6.0.1), and
quay.io/podman/stable:latestisstill 5.8.2. Chasing 6 means a third-party repo or building from source — added supply-chain
surface on every agent image for zero benefit here. Trixie's 5.4.2 already fixes the only
version-sensitive defect we found (exit-code propagation). Suggest "podman ≥5.4 from the distro".
won't run. It also needs
passt(podman 5's default network tool) andnftables(netavark shellsout to
nft; without it every run fails withunable to upgrade to tcp, received 500), plusfuse-overlayfs/slirp4netns, the subuid/subgid entries removed, and the launch-time devicechmods. Suggest "agent images can start rootless podman" with a pointer to
docs/research/rootless-docker-in-apple-container-spike.md.Measured, for the record:
docker run … 'exit 7'(compat API)podman run … 'exit 7'(native)feat(macos): spike rootless Docker inside bottlesto spike(macos): rootless containers in bottles — Docker impossible, podman worksPull request closed