From 670fe2c87e8e5815c178266f561c373a5a7f68e1 Mon Sep 17 00:00:00 2001 From: Brett Kinny Date: Thu, 1 Oct 2026 12:15:10 +1000 Subject: [PATCH] Harden Dev Container capabilities and PID bound to match installers Apply the installers' --cap-drop=ALL capability set and --pids-limit=4096 to .devcontainer/devcontainer.json via runArgs. Add SYS_CHROOT, which the SSH server Feature's OpenSSH privilege separation needs (chroot to /run/sshd); without it every SSH connection is reset during key exchange. A static consistency check now requires the Dev Container to drop all capabilities, keep the 4096 PID bound, avoid widening flags, and use exactly the installers' capability set plus the documented SYS_CHROOT. Co-Authored-By: Claude Opus 5.5 (1M context) --- .devcontainer/devcontainer.json | 11 ++++++++++ SECURITY.md | 9 +++++++++ tests/test-repository-consistency.sh | 30 ++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 2ea7a67..4ba39d5 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -22,5 +22,16 @@ "containerEnv": { "DEVCONTAINER": "1" }, + "runArgs": [ + "--cap-drop=ALL", + "--cap-add=CHOWN", + "--cap-add=DAC_OVERRIDE", + "--cap-add=FOWNER", + "--cap-add=SETUID", + "--cap-add=SETGID", + "--cap-add=KILL", + "--cap-add=SYS_CHROOT", + "--pids-limit=4096" + ], "postCreateCommand": ["bash", "${containerWorkspaceFolder}/scripts/devcontainer-postcreate.sh"] } diff --git a/SECURITY.md b/SECURITY.md index 910b23e..54ff957 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -226,6 +226,15 @@ does not contain that server. The upstream Feature enables root login in its SSHD configuration, but Squarebox publishes no SSH host port or password, and restricts remote-forwarded listeners to loopback. +The Dev Container applies the same `runArgs` hardening as the installers and +Compose: `--cap-drop=ALL`, re-adding only `CHOWN`, `DAC_OVERRIDE`, `FOWNER`, +`SETUID`, `SETGID`, and `KILL`, plus `--pids-limit=4096`. It adds exactly one +extra capability, `SYS_CHROOT`, because the SSH server Feature's OpenSSH uses +privilege separation: the pre-authentication child `chroot`s into `/run/sshd`, +and without `SYS_CHROOT` every connection is reset during key exchange. The +Feature listens on port 2222, so `NET_BIND_SERVICE` is not needed, and logins +were verified to succeed without `AUDIT_WRITE`. + Linux capabilities are reduced, and the Box is bounded to 4096 PIDs so a runaway process cannot exhaust the host's process table. The Box has network access and the host resources explicitly mounted by its Install identity. diff --git a/tests/test-repository-consistency.sh b/tests/test-repository-consistency.sh index c91dea5..55fd5f8 100755 --- a/tests/test-repository-consistency.sh +++ b/tests/test-repository-consistency.sh @@ -55,6 +55,36 @@ test "$(jq '[.mounts[]? | select( )] | length' .devcontainer/devcontainer.json)" = 1 \ || fail "Dev Container Managed home is not a rebuild-stable, per-Box volume" +# The Dev Container must keep the installers' capability reduction and PID +# bound. Its only documented extra is SYS_CHROOT, which the SSH server Feature +# needs for OpenSSH privilege separation (see SECURITY.md). +DEVCONTAINER_SSHD_EXTRA_CAPS=SYS_CHROOT +installer_caps=$(grep -oE -- '--cap-add=[A-Z_]+' install.sh | sed 's/^--cap-add=//' | sort -u) +test -n "$installer_caps" || fail "install.sh capability set could not be read" +ps_caps=$(grep -oE -- "--cap-add=[A-Z_]+" install.ps1 | sed 's/^--cap-add=//' | sort -u) +test "$ps_caps" = "$installer_caps" \ + || fail "install.ps1 and install.sh capability sets differ" +grep -Fq -- '--cap-drop=ALL' install.sh || fail "install.sh no longer drops all capabilities" +test "$(jq -r '[.runArgs[]? | select(. == "--cap-drop=ALL")] | length' \ + .devcontainer/devcontainer.json)" = 1 \ + || fail "Dev Container does not drop all capabilities" +test "$(jq -r '[.runArgs[]? | select(. == "--pids-limit=4096")] | length' \ + .devcontainer/devcontainer.json)" = 1 \ + || fail "Dev Container is not bounded to 4096 PIDs" +test "$(jq -r '[.runArgs[]? | select(test("^--(privileged|cap-add=ALL|security-opt|pids-limit=(-1|0)$)"))] | length' \ + .devcontainer/devcontainer.json)" = 0 \ + || fail "Dev Container runArgs weaken the hardened Box" +test "$(jq -r '[.capAdd[]?, .privileged // empty, .securityOpt[]?] | length' \ + .devcontainer/devcontainer.json)" = 0 \ + || fail "Dev Container must not widen authority through capAdd/privileged/securityOpt" +devcontainer_caps=$(jq -r '.runArgs[]? | select(startswith("--cap-add=")) | ltrimstr("--cap-add=")' \ + .devcontainer/devcontainer.json | sort -u) +expected_devcontainer_caps=$(printf '%s\n' $installer_caps $DEVCONTAINER_SSHD_EXTRA_CAPS | sort -u) +test "$devcontainer_caps" = "$expected_devcontainer_caps" \ + || fail "Dev Container capability set must equal the installers' set plus SYS_CHROOT" +grep -Fq 'SYS_CHROOT' SECURITY.md \ + || fail "SECURITY.md does not justify the Dev Container SYS_CHROOT capability" + if grep -E '(USER_HOME|\$HOME|USERPROFILE).*[.]config/git' \ install.sh install.ps1 docker-compose.yml >/dev/null; then fail "host real Git config is still referenced"