diff --git a/README.md b/README.md index e0be5e6..64b02ce 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ | **age** | A simple, modern, and secure file-encryption tool. | `ghcr.io/hansehart/devcontainer-features/age` | | **Claude Code CLI** | Anthropic's official command-line tool for agentic coding with Claude. | `ghcr.io/hansehart/devcontainer-features/claude-code` | | **Codex CLI** | OpenAI's official command-line tool for agentic coding with Codex. | `ghcr.io/hansehart/devcontainer-features/codex` | -| **Docker in Docker** | A Docker engine for building and running containers inside the dev container. | `ghcr.io/hansehart/devcontainer-features/docker-in-docker` | +| **Docker in Docker** | A rootless Docker engine for building and running containers inside the dev container. | `ghcr.io/hansehart/devcontainer-features/docker-in-docker` | | **Headless Chrome** | A headless Chrome build for browser automation and rendering. | `ghcr.io/hansehart/devcontainer-features/headless-chrome` | | **LaTeX (TeX Live)** | The TeX Live distribution for typesetting LaTeX documents and bibliographies. | `ghcr.io/hansehart/devcontainer-features/latex` | | **Node.js** | The Node.js JavaScript runtime, with npm and npx for installing and running packages. | `ghcr.io/hansehart/devcontainer-features/node` | diff --git a/src/docker-in-docker/devcontainer-feature.json b/src/docker-in-docker/devcontainer-feature.json index 32618d8..bbfbbad 100644 --- a/src/docker-in-docker/devcontainer-feature.json +++ b/src/docker-in-docker/devcontainer-feature.json @@ -1,8 +1,8 @@ { "id": "docker-in-docker", - "version": "1.2.1", + "version": "2.0.0", "name": "Docker in Docker", - "description": "Installs a Docker engine for building and running containers inside the dev container.", + "description": "Installs a rootless Docker engine for building and running containers inside the dev container.", "documentationURL": "https://github.com/hansehart/devcontainer-features/tree/main/src/docker-in-docker", "options": { "version": { @@ -17,9 +17,10 @@ } }, "entrypoint": "/usr/local/share/docker-in-docker/docker-init.sh", - "privileged": true, + "privileged": false, "containerEnv": { - "DOCKER_BUILDKIT": "1" + "DOCKER_BUILDKIT": "1", + "DOCKER_HOST": "unix:///run/docker-rootless.sock" }, "customizations": { "vscode": { @@ -27,17 +28,5 @@ "ms-azuretools.vscode-containers" ] } - }, - "mounts": [ - { - "source": "dind-var-lib-docker", - "target": "/var/lib/docker", - "type": "volume" - }, - { - "source": "dind-var-lib-containerd", - "target": "/var/lib/containerd", - "type": "volume" - } - ] + } } diff --git a/src/docker-in-docker/docker-init.sh b/src/docker-in-docker/docker-init.sh index 052ae26..51e6a2e 100644 --- a/src/docker-in-docker/docker-init.sh +++ b/src/docker-in-docker/docker-init.sh @@ -1,6 +1,25 @@ #!/bin/sh set -e +# The unprivileged user that owns the daemon (persisted by install.sh). +RUSER="$(cat /usr/local/share/docker-in-docker/rootless-user 2>/dev/null || echo root)" + +# The daemon runs as the remote user, and cannot start as root. +# Resolving the account here lets a name with no passwd entry take this branch too. +if [ "$RUSER" = "root" ] || ! RUSER_ENT="$(getent passwd "$RUSER")"; then + echo "docker-in-docker: needs a non-root remoteUser, the daemon stays down" >&2 + # Hand off to the container command, or stop when there is none to hand off to. + exec "$@" + exit 0 +fi + +RUID="$(echo "$RUSER_ENT" | cut -d: -f3)" +RHOME="$(echo "$RUSER_ENT" | cut -d: -f6)" +RUNTIME_DIR="/run/user/${RUID}" + +# Trust a mounted CA, so registries behind a TLS proxy resolve. +update-ca-certificates >/dev/null 2>&1 || true + # Match the iptables backend to the host kernel we share. if type iptables-legacy > /dev/null 2>&1 \ && { grep -qE '^ip_tables\b' /proc/modules || [ -d /sys/module/ip_tables ]; } \ @@ -23,44 +42,95 @@ if [ -d /sys/kernel/security ] && ! mountpoint -q /sys/kernel/security; then fi mountpoint -q /tmp || mount -t tmpfs none /tmp || true -# Delegate cgroup v2 controllers to a leaf so nested cgroups work. -if [ -f /sys/fs/cgroup/cgroup.controllers ]; then - mkdir -p /sys/fs/cgroup/init - # Retry the process move, which races with EBUSY on a cold boot. +# Nest the cgroup controllers where the host delegates them, and pass over where it does not. +if [ -f /sys/fs/cgroup/cgroup.controllers ] && mkdir -p /sys/fs/cgroup/init 2>/dev/null; then + # Move the existing processes into a leaf, retrying while they settle. cg_tries=0 until xargs -rn1 < /sys/fs/cgroup/cgroup.procs > /sys/fs/cgroup/init/cgroup.procs 2>/dev/null \ || [ "$cg_tries" -ge 5 ]; do sleep 1 cg_tries=$((cg_tries + 1)) done + # Hand the controllers down to that leaf. sed -e 's/ / +/g' -e 's/^/+/' < /sys/fs/cgroup/cgroup.controllers \ > /sys/fs/cgroup/cgroup.subtree_control 2>/dev/null || true fi -# Start the daemon and wait for it to accept commands. -dockerd_pid="" +# Provide the device the userspace network stack builds its tap on. +if [ ! -c /dev/net/tun ]; then + mkdir -p /dev/net + mknod /dev/net/tun c 10 200 2>/dev/null || true + # Readable by the remote user, which runs the network stack. + chmod 0666 /dev/net/tun 2>/dev/null || true +fi + +# Prepare the runtime and data directories owned by the remote user. +# The store expects a volume here, since it cannot sit on the container's own overlayfs. +mkdir -p "$RUNTIME_DIR" "${RHOME}/.local/share/docker" +# The runtime directory carries the daemon socket, so it stays shut to the other accounts. +chmod 0700 "$RUNTIME_DIR" +chown "$RUSER":"$RUSER" "$RUNTIME_DIR" "${RHOME}/.local/share/docker" 2>/dev/null || true + +# Restore the daemon settings when a volume mounted over the home hides the image's copy. +if [ -f /usr/local/share/docker-in-docker/daemon.json ] \ + && [ ! -f "${RHOME}/.config/docker/daemon.json" ]; then + mkdir -p "${RHOME}/.config/docker" + cp /usr/local/share/docker-in-docker/daemon.json "${RHOME}/.config/docker/daemon.json" + chown -R "$RUSER":"$RUSER" "${RHOME}/.config/docker" 2>/dev/null || true +fi + +# Re-privilege the id-mapping helpers, since the image unpack drops their capabilities. +# no-new-privileges has to stay unset, or the kernel ignores what is set below. +for b in /usr/bin/newuidmap /usr/bin/newgidmap; do + # Keep the caller's identity, which is what the kernel grants the mapping to. + chmod u-s "$b" 2>/dev/null || true + # Carry the mapping rights as file capabilities instead. + setcap cap_setuid,cap_setgid+ep "$b" 2>/dev/null || true +done + +# Publish the socket under a name that holds no user id. +# The daemon's own path holds one, and it is only known once the container runs. +ln -sfn "${RUNTIME_DIR}/docker.sock" /run/docker-rootless.sock + +# Point login shells at the same name, for the ones that inherit no container environment. +printf 'export DOCKER_HOST=unix:///run/docker-rootless.sock\n' \ + > /etc/profile.d/99-rootless-docker.sh + start_dockerd() { - # Clear stale pid files left by an unclean stop. + # Clear pid files left by an unclean stop, which otherwise block the next start. find /run /var/run -iname 'docker*.pid' -delete 2>/dev/null || true find /run /var/run -iname 'container*.pid' -delete 2>/dev/null || true - dockerd > /tmp/dockerd.log 2>&1 & - dockerd_pid=$! - tries=0 - until docker info > /dev/null 2>&1 || [ "$tries" -ge 30 ]; do - sleep 1 - tries=$((tries + 1)) - done - docker info > /dev/null 2>&1 + + # Run as the remote user, with userspace networking and a private pid namespace. + # Exits unless the container's apparmor and seccomp profiles admit those namespaces. + # overlay2 needs the store on a real filesystem, which the volume above provides. + runuser -u "$RUSER" -- env \ + HOME="$RHOME" \ + XDG_RUNTIME_DIR="$RUNTIME_DIR" \ + DOCKER_HOST="unix://${RUNTIME_DIR}/docker.sock" \ + PATH="/usr/bin:/usr/local/bin:/sbin:/usr/sbin:/bin" \ + DOCKERD_ROOTLESS_ROOTLESSKIT_NET=slirp4netns \ + DOCKERD_ROOTLESS_ROOTLESSKIT_MTU=65520 \ + DOCKERD_ROOTLESS_ROOTLESSKIT_DETACH_NETNS=false \ + DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS="--pidns" \ + DOCKERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SANDBOX=auto \ + DOCKERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SECCOMP=auto \ + dockerd-rootless.sh --storage-driver=overlay2 > /tmp/dockerd.log 2>&1 } -# Retry once when a stale lock in the persisted data volume kills the first start. -if ! start_dockerd; then - echo "docker-in-docker: dockerd did not come up, restarting once" >&2 - cat /tmp/dockerd.log >&2 || true - kill "$dockerd_pid" 2>/dev/null || true - sleep 1 - start_dockerd || { echo "docker-in-docker: dockerd failed to start" >&2; cat /tmp/dockerd.log >&2 || true; } -fi +# Supervise in the background, so the container command starts without waiting. +{ + if ! start_dockerd; then + echo "docker-in-docker: rootless dockerd exited, retrying once" >&2 + # Carry the reason into the container log, which is all a CI run gets to read. + cat /tmp/dockerd.log >&2 || true + sleep 2 + if ! start_dockerd; then + echo "docker-in-docker: rootless dockerd failed to start" >&2 + cat /tmp/dockerd.log >&2 || true + fi + fi +} & # Hand off to the container's original entrypoint/command. exec "$@" diff --git a/src/docker-in-docker/install.sh b/src/docker-in-docker/install.sh index 1047e64..cd84bee 100644 --- a/src/docker-in-docker/install.sh +++ b/src/docker-in-docker/install.sh @@ -25,37 +25,70 @@ apt-get update # Resolve: take the current CE packages, or pin them to an apt version matching VERSION. if [ "$VERSION" = "latest" ]; then - ce="docker-ce docker-ce-cli" + ce="docker-ce docker-ce-cli docker-ce-rootless-extras" else pin="$(apt-cache madison docker-ce | awk -v v="$VERSION" '$3 ~ v {print $3; exit}')" [ -n "$pin" ] || { echo "docker-in-docker: Docker CE version '$VERSION' not found in apt" >&2; exit 1; } - ce="docker-ce=$pin docker-ce-cli=$pin" + # The rootless extras depend on the exact engine version, so they carry the same pin. + ce="docker-ce=$pin docker-ce-cli=$pin docker-ce-rootless-extras=$pin" fi # Install: engine, CLI, containerd, and the buildx/compose plugins (Docker's official set). # shellcheck disable=SC2086 apt-get install -y --no-install-recommends $ce containerd.io docker-buildx-plugin docker-compose-plugin -# Pin the engine so a later apt upgrade keeps it in sync with the persisted data volume. -apt-mark hold docker-ce docker-ce-cli containerd.io +# Install: the helpers and userspace networking an unprivileged daemon needs. +# iproute2 carries `ip`, which the userspace network stack calls to build its tap device. +# libcap2-bin carries `setcap`, which the entrypoint puts on the id-mapping helpers. +# uidmap carries those helpers, which map the subordinate ids granted below. +apt-get install -y --no-install-recommends \ + fuse-overlayfs \ + iproute2 \ + libcap2-bin \ + slirp4netns \ + uidmap + +# Configure: pin the engine so a later apt upgrade keeps it in sync with the persisted data volume. +apt-mark hold docker-ce docker-ce-cli docker-ce-rootless-extras containerd.io rm -rf /var/lib/apt/lists/* -# Configure: add the non-root remote user to the docker group so it can run docker directly. +# Configure: add the non-root remote user to the docker group so the client resolves the daemon. groupadd -f docker +# A root remote user has nothing to configure here, and leaves the daemon down at start. if [ -n "$_REMOTE_USER" ] && [ "$_REMOTE_USER" != "root" ]; then usermod -aG docker "$_REMOTE_USER" || true -fi -# Configure: write the requested daemon settings to daemon.json (empty leaves the file untouched). -if [ -n "$DAEMON_JSON" ]; then - mkdir -p /etc/docker - printf '%s\n' "$DAEMON_JSON" > /etc/docker/daemon.json + # Grant the subordinate ids the daemon maps its containers into. + if ! grep -q "^${_REMOTE_USER}:" /etc/subuid; then + echo "${_REMOTE_USER}:100000:65536" >> /etc/subuid + fi + if ! grep -q "^${_REMOTE_USER}:" /etc/subgid; then + echo "${_REMOTE_USER}:100000:65536" >> /etc/subgid + fi fi # Hook: install the entrypoint that starts dockerd at container start, then execs the container command. install -d /usr/local/share/docker-in-docker install -m 0755 "$(dirname "$0")/docker-init.sh" /usr/local/share/docker-in-docker/docker-init.sh +# Hook: persist which user runs the rootless daemon (the entrypoint has no _REMOTE_USER at runtime). +printf '%s\n' "${_REMOTE_USER:-root}" > /usr/local/share/docker-in-docker/rootless-user + +# Configure: write the requested daemon settings into the image (empty writes nothing). +if [ -n "$DAEMON_JSON" ]; then + # The copy the entrypoint restores from when a volume hides the home directory. + printf '%s\n' "$DAEMON_JSON" > /usr/local/share/docker-in-docker/daemon.json + + # The copy the daemon reads, in place before anything can look for it. + remote_home="${_REMOTE_USER_HOME:-}" + if [ -n "$remote_home" ] && [ "${_REMOTE_USER:-root}" != "root" ]; then + install -d -m 0755 "${remote_home}/.config/docker" + printf '%s\n' "$DAEMON_JSON" > "${remote_home}/.config/docker/daemon.json" + # By name, since the numeric id can still change. + chown -R "$_REMOTE_USER" "${remote_home}/.config" || true + fi +fi + # Verify: the CLI resolves on PATH. docker --version diff --git a/test/docker-in-docker/daemon_json.sh b/test/docker-in-docker/daemon_json.sh index f2db7f3..3636756 100644 --- a/test/docker-in-docker/daemon_json.sh +++ b/test/docker-in-docker/daemon_json.sh @@ -4,7 +4,8 @@ set -e # Import the test library source dev-container-features-test-lib -check "daemon.json no-new-privileges written" grep -qF '"no-new-privileges":true' /etc/docker/daemon.json +# The rootless daemon reads its settings from the remote user's config directory. +check "daemon.json no-new-privileges written" grep -qF '"no-new-privileges":true' "$HOME/.config/docker/daemon.json" # Report result reportResults diff --git a/test/docker-in-docker/rootless_daemon.sh b/test/docker-in-docker/rootless_daemon.sh new file mode 100644 index 0000000..3d1419f --- /dev/null +++ b/test/docker-in-docker/rootless_daemon.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -e + +# Import the test library +source dev-container-features-test-lib + +# The entrypoint starts the daemon in the background, so give it time to accept commands. +wait_for_daemon() { + for _ in $(seq 1 30); do + docker info > /dev/null 2>&1 && return 0 + sleep 1 + done + return 1 +} + +check "daemon reachable" bash -c "$(declare -f wait_for_daemon); wait_for_daemon" +check "daemon runs rootless" bash -c "docker info --format '{{.SecurityOptions}}' | grep -q rootless" +# The point of 2.0.0: the daemon comes up in a container that was never granted +# CAP_SYS_ADMIN, which privileged would have put in pid 1's bounding set. Read pid 1 +# rather than this shell, whose capabilities are empty simply because it is not root. +check "container is unprivileged" bash -c 'bnd=$(awk "/^CapBnd:/ {print \$2}" /proc/1/status); [ $((0x$bnd >> 21 & 1)) -eq 0 ]' +check "overlay2 storage driver" bash -c "docker info --format '{{.Driver}}' | grep -qx overlay2" +# The published name has to reach a live socket. +check "socket is live at the fixed path" bash -c 'test -S /run/docker-rootless.sock && [ "$DOCKER_HOST" = "unix:///run/docker-rootless.sock" ]' +# And to lead to this shell's own runtime directory, which a path fixed at build time misses. +check "socket link resolves to the remote user" bash -c '[ "$(readlink /run/docker-rootless.sock)" = "/run/user/$(id -u)/docker.sock" ]' +check "nested container runs" docker run --rm hello-world + +# Report result +reportResults diff --git a/test/docker-in-docker/scenarios.json b/test/docker-in-docker/scenarios.json index ac9d90b..96bbc0c 100644 --- a/test/docker-in-docker/scenarios.json +++ b/test/docker-in-docker/scenarios.json @@ -1,6 +1,7 @@ { "daemon_json": { "image": "ubuntu:24.04", + "remoteUser": "ubuntu", "features": { "docker-in-docker": { "daemonJson": "{\\\"no-new-privileges\\\":true}" @@ -9,10 +10,31 @@ }, "pinned_version": { "image": "ubuntu:24.04", + "remoteUser": "ubuntu", "features": { "docker-in-docker": { "version": "28" } } + }, + "rootless_daemon": { + "image": "ubuntu:24.04", + "remoteUser": "ubuntu", + "runArgs": [ + "--security-opt", + "apparmor=unconfined", + "--security-opt", + "seccomp=${localEnv:PWD}/test/docker-in-docker/seccomp.json" + ], + "mounts": [ + { + "source": "dind-rootless-data", + "target": "/home/ubuntu/.local/share/docker", + "type": "volume" + } + ], + "features": { + "docker-in-docker": {} + } } } diff --git a/test/docker-in-docker/seccomp.json b/test/docker-in-docker/seccomp.json new file mode 100644 index 0000000..337ad48 --- /dev/null +++ b/test/docker-in-docker/seccomp.json @@ -0,0 +1,1149 @@ +{ + "defaultAction": "SCMP_ACT_ERRNO", + "defaultErrnoRet": 38, + "defaultErrno": "ENOSYS", + "archMap": [ + { + "architecture": "SCMP_ARCH_X86_64", + "subArchitectures": [ + "SCMP_ARCH_X86", + "SCMP_ARCH_X32" + ] + }, + { + "architecture": "SCMP_ARCH_AARCH64", + "subArchitectures": [ + "SCMP_ARCH_ARM" + ] + }, + { + "architecture": "SCMP_ARCH_MIPS64", + "subArchitectures": [ + "SCMP_ARCH_MIPS", + "SCMP_ARCH_MIPS64N32" + ] + }, + { + "architecture": "SCMP_ARCH_MIPS64N32", + "subArchitectures": [ + "SCMP_ARCH_MIPS", + "SCMP_ARCH_MIPS64" + ] + }, + { + "architecture": "SCMP_ARCH_MIPSEL64", + "subArchitectures": [ + "SCMP_ARCH_MIPSEL", + "SCMP_ARCH_MIPSEL64N32" + ] + }, + { + "architecture": "SCMP_ARCH_MIPSEL64N32", + "subArchitectures": [ + "SCMP_ARCH_MIPSEL", + "SCMP_ARCH_MIPSEL64" + ] + }, + { + "architecture": "SCMP_ARCH_S390X", + "subArchitectures": [ + "SCMP_ARCH_S390" + ] + } + ], + "syscalls": [ + { + "names": [ + "bdflush", + "cachestat", + "io_pgetevents", + "io_pgetevents_time64", + "kexec_file_load", + "kexec_load", + "map_shadow_stack", + "migrate_pages", + "move_pages", + "nfsservctl", + "nice", + "oldfstat", + "oldlstat", + "oldolduname", + "oldstat", + "olduname", + "pciconfig_iobase", + "pciconfig_read", + "pciconfig_write", + "sgetmask", + "ssetmask", + "swapoff", + "swapon", + "syscall", + "sysfs", + "uselib", + "userfaultfd", + "ustat", + "vm86", + "vm86old", + "vmsplice" + ], + "action": "SCMP_ACT_ERRNO", + "args": [], + "comment": "", + "includes": {}, + "excludes": {}, + "errnoRet": 1, + "errno": "EPERM" + }, + { + "names": [ + "_llseek", + "_newselect", + "accept", + "accept4", + "access", + "adjtimex", + "alarm", + "bind", + "brk", + "capget", + "capset", + "chdir", + "chmod", + "chown", + "chown32", + "clock_adjtime", + "clock_adjtime64", + "clock_getres", + "clock_getres_time64", + "clock_gettime", + "clock_gettime64", + "clock_nanosleep", + "clock_nanosleep_time64", + "clone", + "clone3", + "close", + "close_range", + "connect", + "copy_file_range", + "creat", + "dup", + "dup2", + "dup3", + "epoll_create", + "epoll_create1", + "epoll_ctl", + "epoll_ctl_old", + "epoll_pwait", + "epoll_pwait2", + "epoll_wait", + "epoll_wait_old", + "eventfd", + "eventfd2", + "execve", + "execveat", + "exit", + "exit_group", + "faccessat", + "faccessat2", + "fadvise64", + "fadvise64_64", + "fallocate", + "fanotify_init", + "fanotify_mark", + "fchdir", + "fchmod", + "fchmodat", + "fchmodat2", + "fchown", + "fchown32", + "fchownat", + "fcntl", + "fcntl64", + "fdatasync", + "fgetxattr", + "flistxattr", + "flock", + "fork", + "fremovexattr", + "fsconfig", + "fsetxattr", + "fsmount", + "fsopen", + "fspick", + "fstat", + "fstat64", + "fstatat64", + "fstatfs", + "fstatfs64", + "fsync", + "ftruncate", + "ftruncate64", + "futex", + "futex_requeue", + "futex_time64", + "futex_wait", + "futex_waitv", + "futex_wake", + "futimesat", + "get_mempolicy", + "get_robust_list", + "get_thread_area", + "getcpu", + "getcwd", + "getdents", + "getdents64", + "getegid", + "getegid32", + "geteuid", + "geteuid32", + "getgid", + "getgid32", + "getgroups", + "getgroups32", + "getitimer", + "getpeername", + "getpgid", + "getpgrp", + "getpid", + "getppid", + "getpriority", + "getrandom", + "getresgid", + "getresgid32", + "getresuid", + "getresuid32", + "getrlimit", + "getrusage", + "getsid", + "getsockname", + "getsockopt", + "gettid", + "gettimeofday", + "getuid", + "getuid32", + "getxattr", + "inotify_add_watch", + "inotify_init", + "inotify_init1", + "inotify_rm_watch", + "io_cancel", + "io_destroy", + "io_getevents", + "io_setup", + "io_submit", + "ioctl", + "ioprio_get", + "ioprio_set", + "ipc", + "keyctl", + "kill", + "landlock_add_rule", + "landlock_create_ruleset", + "landlock_restrict_self", + "lchown", + "lchown32", + "lgetxattr", + "link", + "linkat", + "listen", + "listxattr", + "llistxattr", + "lremovexattr", + "lseek", + "lsetxattr", + "lstat", + "lstat64", + "madvise", + "mbind", + "membarrier", + "memfd_create", + "memfd_secret", + "mincore", + "mkdir", + "mkdirat", + "mknod", + "mknodat", + "mlock", + "mlock2", + "mlockall", + "mmap", + "mmap2", + "mount", + "mount_setattr", + "move_mount", + "mprotect", + "mq_getsetattr", + "mq_notify", + "mq_open", + "mq_timedreceive", + "mq_timedreceive_time64", + "mq_timedsend", + "mq_timedsend_time64", + "mq_unlink", + "mremap", + "msgctl", + "msgget", + "msgrcv", + "msgsnd", + "msync", + "munlock", + "munlockall", + "munmap", + "name_to_handle_at", + "nanosleep", + "newfstatat", + "open", + "open_tree", + "openat", + "openat2", + "pause", + "pidfd_getfd", + "pidfd_open", + "pidfd_send_signal", + "pipe", + "pipe2", + "pivot_root", + "pkey_alloc", + "pkey_free", + "pkey_mprotect", + "poll", + "ppoll", + "ppoll_time64", + "prctl", + "pread64", + "preadv", + "preadv2", + "prlimit64", + "process_mrelease", + "process_vm_readv", + "process_vm_writev", + "pselect6", + "pselect6_time64", + "ptrace", + "pwrite64", + "pwritev", + "pwritev2", + "read", + "readahead", + "readlink", + "readlinkat", + "readv", + "reboot", + "recv", + "recvfrom", + "recvmmsg", + "recvmmsg_time64", + "recvmsg", + "remap_file_pages", + "removexattr", + "rename", + "renameat", + "renameat2", + "restart_syscall", + "rmdir", + "rseq", + "rt_sigaction", + "rt_sigpending", + "rt_sigprocmask", + "rt_sigqueueinfo", + "rt_sigreturn", + "rt_sigsuspend", + "rt_sigtimedwait", + "rt_sigtimedwait_time64", + "rt_tgsigqueueinfo", + "sched_get_priority_max", + "sched_get_priority_min", + "sched_getaffinity", + "sched_getattr", + "sched_getparam", + "sched_getscheduler", + "sched_rr_get_interval", + "sched_rr_get_interval_time64", + "sched_setaffinity", + "sched_setattr", + "sched_setparam", + "sched_setscheduler", + "sched_yield", + "seccomp", + "select", + "semctl", + "semget", + "semop", + "semtimedop", + "semtimedop_time64", + "send", + "sendfile", + "sendfile64", + "sendmmsg", + "sendmsg", + "sendto", + "set_mempolicy", + "set_robust_list", + "set_thread_area", + "set_tid_address", + "setfsgid", + "setfsgid32", + "setfsuid", + "setfsuid32", + "setgid", + "setgid32", + "setgroups", + "setgroups32", + "setitimer", + "setns", + "setpgid", + "setpriority", + "setregid", + "setregid32", + "setresgid", + "setresgid32", + "setresuid", + "setresuid32", + "setreuid", + "setreuid32", + "setrlimit", + "setsid", + "setsockopt", + "setuid", + "setuid32", + "setxattr", + "shmat", + "shmctl", + "shmdt", + "shmget", + "shutdown", + "sigaltstack", + "signal", + "signalfd", + "signalfd4", + "sigprocmask", + "sigreturn", + "socketcall", + "socketpair", + "splice", + "stat", + "stat64", + "statfs", + "statfs64", + "statx", + "symlink", + "symlinkat", + "sync", + "sync_file_range", + "syncfs", + "sysinfo", + "syslog", + "tee", + "tgkill", + "time", + "timer_create", + "timer_delete", + "timer_getoverrun", + "timer_gettime", + "timer_gettime64", + "timer_settime", + "timer_settime64", + "timerfd_create", + "timerfd_gettime", + "timerfd_gettime64", + "timerfd_settime", + "timerfd_settime64", + "times", + "tkill", + "truncate", + "truncate64", + "ugetrlimit", + "umask", + "umount", + "umount2", + "uname", + "unlink", + "unlinkat", + "unshare", + "utime", + "utimensat", + "utimensat_time64", + "utimes", + "vfork", + "wait4", + "waitid", + "waitpid", + "write", + "writev" + ], + "action": "SCMP_ACT_ALLOW", + "args": [], + "comment": "", + "includes": {}, + "excludes": {} + }, + { + "names": [ + "personality" + ], + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 0, + "valueTwo": 0, + "op": "SCMP_CMP_EQ" + } + ], + "comment": "", + "includes": {}, + "excludes": {} + }, + { + "names": [ + "personality" + ], + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 8, + "valueTwo": 0, + "op": "SCMP_CMP_EQ" + } + ], + "comment": "", + "includes": {}, + "excludes": {} + }, + { + "names": [ + "personality" + ], + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 131072, + "valueTwo": 0, + "op": "SCMP_CMP_EQ" + } + ], + "comment": "", + "includes": {}, + "excludes": {} + }, + { + "names": [ + "personality" + ], + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 131080, + "valueTwo": 0, + "op": "SCMP_CMP_EQ" + } + ], + "comment": "", + "includes": {}, + "excludes": {} + }, + { + "names": [ + "personality" + ], + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 4294967295, + "valueTwo": 0, + "op": "SCMP_CMP_EQ" + } + ], + "comment": "", + "includes": {}, + "excludes": {} + }, + { + "names": [ + "sync_file_range2", + "swapcontext" + ], + "action": "SCMP_ACT_ALLOW", + "args": [], + "comment": "", + "includes": { + "arches": [ + "ppc64le" + ] + }, + "excludes": {} + }, + { + "names": [ + "arm_fadvise64_64", + "arm_sync_file_range", + "breakpoint", + "cacheflush", + "set_tls", + "sync_file_range2" + ], + "action": "SCMP_ACT_ALLOW", + "args": [], + "comment": "", + "includes": { + "arches": [ + "arm", + "arm64" + ] + }, + "excludes": {} + }, + { + "names": [ + "arch_prctl" + ], + "action": "SCMP_ACT_ALLOW", + "args": [], + "comment": "", + "includes": { + "arches": [ + "amd64", + "x32" + ] + }, + "excludes": {} + }, + { + "names": [ + "modify_ldt" + ], + "action": "SCMP_ACT_ALLOW", + "args": [], + "comment": "", + "includes": { + "arches": [ + "amd64", + "x32", + "x86" + ] + }, + "excludes": {} + }, + { + "names": [ + "s390_pci_mmio_read", + "s390_pci_mmio_write", + "s390_runtime_instr" + ], + "action": "SCMP_ACT_ALLOW", + "args": [], + "comment": "", + "includes": { + "arches": [ + "s390", + "s390x" + ] + }, + "excludes": {} + }, + { + "names": [ + "riscv_flush_icache" + ], + "action": "SCMP_ACT_ALLOW", + "args": [], + "comment": "", + "includes": { + "arches": [ + "riscv64" + ] + }, + "excludes": {} + }, + { + "names": [ + "open_by_handle_at" + ], + "action": "SCMP_ACT_ALLOW", + "args": [], + "comment": "", + "includes": { + "caps": [ + "CAP_DAC_READ_SEARCH" + ] + }, + "excludes": {} + }, + { + "names": [ + "open_by_handle_at" + ], + "action": "SCMP_ACT_ERRNO", + "args": [], + "comment": "", + "includes": {}, + "excludes": { + "caps": [ + "CAP_DAC_READ_SEARCH" + ] + }, + "errnoRet": 1, + "errno": "EPERM" + }, + { + "names": [ + "bpf", + "lookup_dcookie", + "perf_event_open", + "quotactl", + "quotactl_fd", + "setdomainname", + "sethostname", + "setns" + ], + "action": "SCMP_ACT_ALLOW", + "args": [], + "comment": "", + "includes": { + "caps": [ + "CAP_SYS_ADMIN" + ] + }, + "excludes": {} + }, + { + "names": [ + "lookup_dcookie", + "quotactl", + "quotactl_fd", + "setdomainname" + ], + "action": "SCMP_ACT_ERRNO", + "args": [], + "comment": "", + "includes": {}, + "excludes": { + "caps": [ + "CAP_SYS_ADMIN" + ] + }, + "errnoRet": 1, + "errno": "EPERM" + }, + { + "names": [ + "sethostname", + "setns" + ], + "action": "SCMP_ACT_ALLOW", + "args": [], + "comment": "o3s: nested container init calls these, and the cage holds no CAP_SYS_ADMIN, so the upstream rule would deny them", + "includes": {}, + "excludes": {} + }, + { + "names": [ + "chroot" + ], + "action": "SCMP_ACT_ALLOW", + "args": [], + "comment": "", + "includes": { + "caps": [ + "CAP_SYS_CHROOT" + ] + }, + "excludes": {} + }, + { + "names": [ + "chroot" + ], + "action": "SCMP_ACT_ERRNO", + "args": [], + "comment": "", + "includes": {}, + "excludes": { + "caps": [ + "CAP_SYS_CHROOT" + ] + }, + "errnoRet": 1, + "errno": "EPERM" + }, + { + "names": [ + "delete_module", + "finit_module", + "init_module", + "query_module" + ], + "action": "SCMP_ACT_ALLOW", + "args": [], + "comment": "", + "includes": { + "caps": [ + "CAP_SYS_MODULE" + ] + }, + "excludes": {} + }, + { + "names": [ + "delete_module", + "finit_module", + "init_module", + "query_module" + ], + "action": "SCMP_ACT_ERRNO", + "args": [], + "comment": "", + "includes": {}, + "excludes": { + "caps": [ + "CAP_SYS_MODULE" + ] + }, + "errnoRet": 1, + "errno": "EPERM" + }, + { + "names": [ + "acct" + ], + "action": "SCMP_ACT_ALLOW", + "args": [], + "comment": "", + "includes": { + "caps": [ + "CAP_SYS_PACCT" + ] + }, + "excludes": {} + }, + { + "names": [ + "acct" + ], + "action": "SCMP_ACT_ERRNO", + "args": [], + "comment": "", + "includes": {}, + "excludes": { + "caps": [ + "CAP_SYS_PACCT" + ] + }, + "errnoRet": 1, + "errno": "EPERM" + }, + { + "names": [ + "kcmp", + "process_madvise" + ], + "action": "SCMP_ACT_ALLOW", + "args": [], + "comment": "", + "includes": { + "caps": [ + "CAP_SYS_PTRACE" + ] + }, + "excludes": {} + }, + { + "names": [ + "kcmp", + "process_madvise" + ], + "action": "SCMP_ACT_ERRNO", + "args": [], + "comment": "", + "includes": {}, + "excludes": { + "caps": [ + "CAP_SYS_PTRACE" + ] + }, + "errnoRet": 1, + "errno": "EPERM" + }, + { + "names": [ + "ioperm", + "iopl" + ], + "action": "SCMP_ACT_ALLOW", + "args": [], + "comment": "", + "includes": { + "caps": [ + "CAP_SYS_RAWIO" + ] + }, + "excludes": {} + }, + { + "names": [ + "ioperm", + "iopl" + ], + "action": "SCMP_ACT_ERRNO", + "args": [], + "comment": "", + "includes": {}, + "excludes": { + "caps": [ + "CAP_SYS_RAWIO" + ] + }, + "errnoRet": 1, + "errno": "EPERM" + }, + { + "names": [ + "clock_settime", + "clock_settime64", + "settimeofday", + "stime" + ], + "action": "SCMP_ACT_ALLOW", + "args": [], + "comment": "", + "includes": { + "caps": [ + "CAP_SYS_TIME" + ] + }, + "excludes": {} + }, + { + "names": [ + "clock_settime", + "clock_settime64", + "settimeofday", + "stime" + ], + "action": "SCMP_ACT_ERRNO", + "args": [], + "comment": "", + "includes": {}, + "excludes": { + "caps": [ + "CAP_SYS_TIME" + ] + }, + "errnoRet": 1, + "errno": "EPERM" + }, + { + "names": [ + "vhangup" + ], + "action": "SCMP_ACT_ALLOW", + "args": [], + "comment": "", + "includes": { + "caps": [ + "CAP_SYS_TTY_CONFIG" + ] + }, + "excludes": {} + }, + { + "names": [ + "vhangup" + ], + "action": "SCMP_ACT_ERRNO", + "args": [], + "comment": "", + "includes": {}, + "excludes": { + "caps": [ + "CAP_SYS_TTY_CONFIG" + ] + }, + "errnoRet": 1, + "errno": "EPERM" + }, + { + "names": [ + "socket" + ], + "action": "SCMP_ACT_ERRNO", + "args": [ + { + "index": 0, + "value": 40, + "valueTwo": 0, + "op": "SCMP_CMP_EQ" + } + ], + "comment": "", + "includes": {}, + "excludes": {}, + "errnoRet": 1, + "errno": "EPERM" + }, + { + "names": [ + "socket" + ], + "action": "SCMP_ACT_ERRNO", + "args": [ + { + "index": 0, + "value": 16, + "valueTwo": 0, + "op": "SCMP_CMP_EQ" + }, + { + "index": 2, + "value": 9, + "valueTwo": 0, + "op": "SCMP_CMP_EQ" + } + ], + "comment": "", + "includes": {}, + "excludes": { + "caps": [ + "CAP_AUDIT_WRITE" + ] + }, + "errnoRet": 22, + "errno": "EINVAL" + }, + { + "names": [ + "socket" + ], + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 16, + "valueTwo": 0, + "op": "SCMP_CMP_EQ" + }, + { + "index": 2, + "value": 9, + "valueTwo": 0, + "op": "SCMP_CMP_NE" + } + ], + "comment": "", + "includes": {}, + "excludes": { + "caps": [ + "CAP_AUDIT_WRITE" + ] + } + }, + { + "names": [ + "socket" + ], + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 16, + "valueTwo": 0, + "op": "SCMP_CMP_NE" + } + ], + "comment": "", + "includes": {}, + "excludes": { + "caps": [ + "CAP_AUDIT_WRITE" + ] + } + }, + { + "names": [ + "socket" + ], + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 40, + "valueTwo": 0, + "op": "SCMP_CMP_NE" + } + ], + "comment": "", + "includes": { + "caps": [ + "CAP_AUDIT_WRITE" + ] + }, + "excludes": {} + }, + { + "names": [ + "bpf" + ], + "action": "SCMP_ACT_ERRNO", + "args": [], + "comment": "", + "includes": {}, + "excludes": { + "caps": [ + "CAP_SYS_ADMIN", + "CAP_BPF" + ] + }, + "errnoRet": 1, + "errno": "EPERM" + }, + { + "names": [ + "bpf" + ], + "action": "SCMP_ACT_ALLOW", + "args": [], + "comment": "", + "includes": { + "caps": [ + "CAP_BPF" + ] + }, + "excludes": {} + }, + { + "names": [ + "perf_event_open" + ], + "action": "SCMP_ACT_ERRNO", + "args": [], + "comment": "", + "includes": {}, + "excludes": { + "caps": [ + "CAP_SYS_ADMIN", + "CAP_PERFMON" + ] + }, + "errnoRet": 1, + "errno": "EPERM" + }, + { + "names": [ + "perf_event_open" + ], + "action": "SCMP_ACT_ALLOW", + "args": [], + "comment": "", + "includes": { + "caps": [ + "CAP_PERFMON" + ] + }, + "excludes": {} + } + ] +} \ No newline at end of file diff --git a/test/docker-in-docker/test.sh b/test/docker-in-docker/test.sh index b74adfa..7d9515d 100644 --- a/test/docker-in-docker/test.sh +++ b/test/docker-in-docker/test.sh @@ -4,12 +4,18 @@ set -e # Import the test library source dev-container-features-test-lib +# Default apparmor and seccomp apply here, so no daemon is expected and only the client is +# checked. The rootless_daemon scenario relaxes both and covers the daemon itself. + check "docker on PATH" bash -c "command -v docker" check "docker version" docker --version check "compose plugin" docker compose version check "buildx plugin" docker buildx version -check "daemon reachable" docker info -check "nested container runs" docker run --rm hello-world +# Written just before the daemon is backgrounded, so its presence shows the setup finished. +check "entrypoint handed off" bash -c "grep -qF 'DOCKER_HOST=unix:///run/docker-rootless.sock' /etc/profile.d/99-rootless-docker.sh" +check "socket path is fixed" bash -c '[ "$DOCKER_HOST" = "unix:///run/docker-rootless.sock" ]' +# Read against the id this shell runs as, which a path fixed at build time would not match. +check "socket link resolves to the remote user" bash -c '[ "$(readlink /run/docker-rootless.sock)" = "/run/user/$(id -u)/docker.sock" ]' # Report result reportResults