diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 28cf3ccfc..b02efa9d8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -189,10 +189,47 @@ jobs: - name: Build image with default channel dependencies run: docker build -t nanobot:test . - - name: Verify Docker Compose startup + - name: Verify Docker Compose startup and privilege boundary env: HOME: ${{ runner.temp }} - run: docker compose run --rm --no-deps --build -T nanobot-cli status + run: | + docker compose run --rm --no-deps --build -T nanobot-cli status + docker compose run --rm --no-deps -T --entrypoint sh nanobot-cli -s <<'OUTER' + set -eu + field() { + awk -v key="$1:" '$1 == key { print $2 }' /proc/self/status + } + test "$(id -u)" = "0" + test "$(field NoNewPrivs)" = "1" + setpriv --reuid=nanobot --regid=nanobot --init-groups sh -s <<'INNER' + set -eu + field() { + awk -v key="$1:" '$1 == key { print $2 }' /proc/self/status + } + test "$(id -u)" = "1000" + test "$(field NoNewPrivs)" = "1" + for capability_set in CapInh CapPrm CapEff CapAmb; do + test "$(field "$capability_set")" = "0000000000000000" + done + INNER + OUTER + docker compose -f docker-compose.yml -f docker-compose.bwrap.yml \ + run --rm --no-deps -T --entrypoint sh nanobot-cli -s <<'BWRAP' + set -eu + mkdir -p /home/nanobot/.nanobot/workspace + chown -R nanobot:nanobot /home/nanobot/.nanobot + setpriv --reuid=nanobot --regid=nanobot --init-groups \ + bwrap --new-session --die-with-parent \ + --ro-bind /usr /usr \ + --ro-bind-try /bin /bin \ + --ro-bind-try /lib /lib \ + --ro-bind-try /lib64 /lib64 \ + --proc /proc --dev /dev --tmpfs /tmp \ + --bind /home/nanobot/.nanobot/workspace /home/nanobot/.nanobot/workspace \ + --chdir /home/nanobot/.nanobot/workspace \ + -- sh -c 'test "$(id -u)" = 1000; touch sandbox-ok' + test -f /home/nanobot/.nanobot/workspace/sandbox-ok + BWRAP - name: Verify default WhatsApp dependencies run: docker run --rm --entrypoint python nanobot:test -c "import neonize, segno" diff --git a/docker-compose.yml b/docker-compose.yml index 86706a8e0..65a283f87 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,6 +13,10 @@ x-common-config: &common-config - CHOWN - SETGID - SETUID + # Prevent the non-root process from regaining capabilities through setuid + # binaries or file capabilities left inside the container image. + security_opt: + - no-new-privileges:true services: nanobot-gateway: diff --git a/docs/deployment.md b/docs/deployment.md index 506a7e6f9..a5a1b56ad 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -160,8 +160,11 @@ docker compose logs -f nanobot-gateway # view logs docker compose down # stop ``` -The default Compose file drops all Linux capabilities and keeps Docker's default -AppArmor/seccomp profiles enabled. If you explicitly set +The default Compose file drops all Linux capabilities except `CHOWN`, `SETUID`, and +`SETGID`, which the root entrypoint needs to fix bind-mount ownership and become UID +1000. It also enables `no-new-privileges`, so the non-root process cannot regain those +bootstrap capabilities through setuid binaries or file capabilities. Docker's default +AppArmor/seccomp profiles remain enabled. If you explicitly set `"tools.exec.sandbox": "bwrap"` in `~/.nanobot/config.json`, add the bwrap override file when starting containers: @@ -170,9 +173,9 @@ docker compose -f docker-compose.yml -f docker-compose.bwrap.yml up -d nanobot-g docker compose -f docker-compose.yml -f docker-compose.bwrap.yml run --rm nanobot-cli agent -m "Hello!" ``` -The override grants `CAP_SYS_ADMIN` and disables AppArmor/seccomp confinement for -the container so bubblewrap can create its nested namespaces. Use it only when the -bwrap sandbox is enabled. +The override adds `CAP_SYS_ADMIN` and disables AppArmor/seccomp confinement for the +container so bubblewrap can create its nested namespaces. It preserves +`no-new-privileges`. Use it only when the bwrap sandbox is enabled. ### Docker @@ -197,6 +200,8 @@ vim ~/.nanobot/config.json # health endpoint on 18790. docker run \ --cap-drop ALL \ + --cap-add CHOWN --cap-add SETGID --cap-add SETUID \ + --security-opt no-new-privileges:true \ -v ~/.nanobot:/home/nanobot/.nanobot \ -p 18790:18790 -p 8765:8765 \ nanobot gateway @@ -205,7 +210,9 @@ docker run \ # bubblewrap needs for nested namespaces. Without them, `bwrap` may exit with # `clone3: Operation not permitted`. docker run \ - --cap-drop ALL --cap-add SYS_ADMIN \ + --cap-drop ALL \ + --cap-add CHOWN --cap-add SETGID --cap-add SETUID --cap-add SYS_ADMIN \ + --security-opt no-new-privileges:true \ --security-opt apparmor=unconfined \ --security-opt seccomp=unconfined \ -v ~/.nanobot:/home/nanobot/.nanobot \