Conversation
Bind-mounting /var/run/docker.sock gave the dev container full control of the host Docker daemon, which is effectively host root (it can start a privileged container or mount the host filesystem). A container compromise therefore escalated straight to the host. Remove the host socket mount and run a rootless Docker daemon inside the container instead, so Docker stays available without the host-root path: - docker cookbook installs docker-ce-rootless-extras and the rootless prerequisites (uidmap, fuse-overlayfs, slirp4netns, dbus-user-session) on Debian/Ubuntu. - .devcontainer/start-rootless-docker.sh starts dockerd-rootless.sh on the per-user runtime socket; it no-ops when the rootless tooling is absent so it never blocks container startup. - devcontainer.json drops the socket mount, points DOCKER_HOST at the rootless socket, pins the user UID, and runs the start script via postStartCommand.
LuuOW
reviewed
Jun 17, 2026
LuuOW
left a comment
There was a problem hiding this comment.
Technical audit: code patterns and implementation verified for alignment with modern software engineering standards.
YuseiIto
pushed a commit
that referenced
this pull request
Jul 3, 2026
Add docker-ce-rootless-extras (ships dockerd-rootless.sh) and the rootless prerequisites (uidmap, dbus-user-session, fuse-overlayfs, slirp4netns) to the docker cookbook, and teach .zshrc to point DOCKER_HOST at the per-user rootless socket when one is running. This is stage 1 of replacing the devcontainer's host docker.sock bind mount (an escalation path to host root) with an in-container rootless daemon. Landing the packages first lets the prebuilt devcontainer images pick them up, so the follow-up devcontainer switch (PR #77) can be exercised for real by CI. Nothing changes behavior yet: the rootless daemon is not started anywhere, and the DOCKER_HOST export only fires when the rootless socket actually exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QYryuMeKDCxgJro93GJxWB
YuseiIto
added a commit
that referenced
this pull request
Jul 3, 2026
Install rootless Docker tooling on Debian-family hosts (stage 1 of #77)
…udly Rework the rootless docker-in-docker switch based on review feedback: - devcontainer.json: grant the minimum the rootless daemon needs (seccomp/apparmor unconfined + /dev/fuse) via runArgs instead of relying on defaults that block nested user namespaces. Drop the updateRemoteUserUID pin and the hardcoded-UID containerEnv: the socket path is now resolved dynamically (zshrc export from #107 and a /var/run/docker.sock symlink), so SSH-key ownership remapping keeps working on Linux hosts. - start-rootless-docker.sh: probe the daemon with Client: Docker Engine - Community Version: 29.3.1 Context: default Debug Mode: false Plugins: buildx: Docker Buildx (Docker Inc.) Version: v0.31.1 Path: /usr/libexec/docker/cli-plugins/docker-buildx compose: Docker Compose (Docker Inc.) Version: v5.1.1 Path: /usr/libexec/docker/cli-plugins/docker-compose Server: instead of testing the socket file (catches stale sockets), wait up to 30s for it to come up, and print a loud warning plus the dockerd log tail on failure so a broken setup is visible in the creation log. Still always exits 0 so startup is never blocked. On success, symlink /var/run/docker.sock to the rootless socket for tools that do not read DOCKER_HOST. - test.yaml: run inside the devcontainer job so CI actually verifies the rootless daemon once the prebuilt image ships the tooling from #107; warn visibly instead of failing while the image lags a rebuild behind. The cookbook package additions moved to #107 (merged) so the prebuilt image gains the rootless tooling before this change lands. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QYryuMeKDCxgJro93GJxWB
docker-ce pulls docker-ce-rootless-extras in via Recommends, so images built before #107 already ship dockerd-rootless.sh — but without uidmap the daemon dies at startup (newuidmap: executable file not found), as the first CI run of the loud startup script surfaced. Check the prerequisite binaries too, in both the startup script's skip condition and the CI smoke-test gate, so an image that predates the tooling skips cleanly instead of attempting a doomed start. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QYryuMeKDCxgJro93GJxWB
The prebuilt image now ships the rootless Docker tooling from #107, so the devcontainer job's smoke test exercises the rootless daemon for real instead of skipping. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QYryuMeKDCxgJro93GJxWB
The first real CI run of the rootless daemon (rebuilt image with the tooling from #107) got past the missing-binary stage and failed at UID/GID map setup: newuidmap: write to uid_map failed: Operation not permitted. Ubuntu 24.04 runners (and Ubuntu 23.10+ hosts generally) restrict user namespaces for unconfined unprivileged processes via AppArmor; rootlesskit can create the namespace but gets no capabilities inside it, so the map write is denied. This is a host-level sysctl, not something the container's runArgs can change. Set kernel.apparmor_restrict_unprivileged_userns=0 on the runner before starting the devcontainer, and teach the startup script to detect the restriction from inside the container and print the exact host-side fix instead of only a generic timeout warning. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QYryuMeKDCxgJro93GJxWB
The AppArmor userns sysctl turned out not to be the cause (CI still fails with newuidmap EPERM after relaxing it, and the in-container check reads 0), so the failure path now prints the facts needed to diagnose from the log alone: newuidmap/newgidmap modes (setuid bits), NoNewPrivs/seccomp/capability state of the startup process, the container's own uid_map, subuid contents, and whether an unprivileged self-mapped user namespace can be created at all. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QYryuMeKDCxgJro93GJxWB
Root cause of the persistent 'newuidmap: write to uid_map failed: Operation not permitted' found by reproducing locally: the kernel's map_write() requires the process that opened /proc/PID/uid_map to hold CAP_SYS_ADMIN over the child user namespace, in addition to CAP_SETUID. On a normal host the setuid-root newuidmap picks up full capabilities, but inside a container a setuid exec can only gain what the bounding set allows, and Docker's default set has no SYS_ADMIN — so the map write is denied no matter what (seccomp, AppArmor and the userns sysctl were all ruled out empirically; reproduced and bisected with setpriv: docker-default bounding fails, docker-default + SYS_ADMIN succeeds). --cap-add SYS_ADMIN is still far short of --privileged: no host devices, none of the other privileged-only capabilities, so the host-escalation path this PR removes stays closed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QYryuMeKDCxgJro93GJxWB
The prebuilt image now ships iproute2 (#110), which rootlesskit needs to configure the slirp4netns tap device. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QYryuMeKDCxgJro93GJxWB
YuseiIto
pushed a commit
that referenced
this pull request
Jul 4, 2026
With the rootless tooling from #107 in place, the devcontainer's rootless daemon (#77) now gets as far as network setup and dies there: rootlesskit configures the slirp4netns tap device by running ip(8) inside the namespace, and the image has no iproute2 — 'nsenter: failed to execute ip: No such file or directory'. Same staging as #107: land the package on main first so the prebuilt images pick it up, then #77's CI can exercise the daemon end to end. iptables, the other runtime dependency of the daemon, needs no entry here because docker-ce already hard-depends on it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QYryuMeKDCxgJro93GJxWB
YuseiIto
pushed a commit
that referenced
this pull request
Jul 4, 2026
With the rootless tooling from #107 in place, the devcontainer's rootless daemon (#77) now gets as far as network setup and dies there: rootlesskit configures the slirp4netns tap device by running ip(8) inside the namespace, and the image has no iproute2 — 'nsenter: failed to execute ip: No such file or directory'. Same staging as #107: land the package on main first so the prebuilt images pick it up, then #77's CI can exercise the daemon end to end. iptables, the other runtime dependency of the daemon, needs no entry here because docker-ce already hard-depends on it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QYryuMeKDCxgJro93GJxWB
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
背景
devcontainer では
/var/run/docker.sockを bind マウントしていた。これはコンテナ内プロセスがホストの Docker デーモンをフル制御できることを意味し、--privilegedコンテナ起動やホスト FS マウント経由で 実質ホスト root へ昇格可能な典型的エスカレーション経路。対応: rootless docker-in-docker
ホスト socket の共有をやめ、コンテナの user namespace 内で動く rootless Docker デーモンに置き換える。これでホスト root への経路を断ちつつ Docker は引き続き利用可能。
前提パッケージの導入は #107(マージ済み) に分割した。本 PR は devcontainer の切り替え本体のみ。
変更内容
.devcontainer/devcontainer.json:postStartCommandで rootless デーモンを起動runArgsで最小限の権限を明示: rootless dockerd はネストした user namespace の作成と fuse-overlayfs のマウントを必要とするが、デフォルトの seccomp/AppArmor プロファイルはこれをブロックするため、seccomp=unconfined/apparmor=unconfined/--device /dev/fuseを付与。--privilegedと異なりホストのデバイス・ディスクには触れないので、本 PR の目的(ホスト昇格経路の遮断)は保たれる。Codespaces ではrunArgsは無視されるが、元より許可されているので問題ないupdateRemoteUserUID: falseと UID 固定のcontainerEnvを撤廃(Linux ホストで UID≠1000 のとき~/.sshが読めなくなる副作用があった)。ソケットパスは Install rootless Docker tooling on Debian-family hosts (stage 1 of #77) #107 の zshrc export と/var/run/docker.sockへの symlink で動的に解決する.devcontainer/start-rootless-docker.sh:docker infoでデーモンの生存を確認(コンテナ再起動後の stale socket に対応)docker infoで検証し、失敗時は警告と dockerd ログ末尾を creation log に出力(サイレント故障の解消)。起動をブロックしない方針は維持(常に exit 0)/var/run/docker.sock→ rootless socket の symlink を作成し、DOCKER_HOSTを読まないツールとの互換性を確保.github/workflows/test.yaml: devcontainer ジョブでdocker run --rm hello-worldを実行し、rootless デーモンが実際に動くことを CI で検証。イメージが 1 リビルド分遅れている場合は fail せず::warning::でスキップを可視化CI への影響
#107 マージ後の
build.yamlによりbamboo-latestに rootless ツールが焼き込まれるため、本 PR の devcontainer ジョブは rootless デーモンの起動とhello-worldの実行を実際に検証する。イメージ再ビルド完了前に CI が走った場合はスモークテストが warning 付きでスキップされるので、その際は再ビルド完了後に CI を re-run すること。確認
rubocop: no offensesmake shellcheck: passhttps://claude.ai/code/session_01AA3kErMVaeCYEX81JNdwXx
Generated by Claude Code