fix(docker): prevent privilege regain after bootstrap

Co-authored-by: yu-xin-c <175149126+yu-xin-c@users.noreply.github.com>
This commit is contained in:
Xubin Ren
2026-08-13 02:13:51 +09:00
co-authored by yu-xin-c
parent 3741ecda0b
commit b571d3b9ff
3 changed files with 56 additions and 8 deletions
+39 -2
View File
@@ -189,10 +189,47 @@ jobs:
- name: Build image with default channel dependencies - name: Build image with default channel dependencies
run: docker build -t nanobot:test . run: docker build -t nanobot:test .
- name: Verify Docker Compose startup - name: Verify Docker Compose startup and privilege boundary
env: env:
HOME: ${{ runner.temp }} 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 - name: Verify default WhatsApp dependencies
run: docker run --rm --entrypoint python nanobot:test -c "import neonize, segno" run: docker run --rm --entrypoint python nanobot:test -c "import neonize, segno"
+4
View File
@@ -13,6 +13,10 @@ x-common-config: &common-config
- CHOWN - CHOWN
- SETGID - SETGID
- SETUID - 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: services:
nanobot-gateway: nanobot-gateway:
+13 -6
View File
@@ -160,8 +160,11 @@ docker compose logs -f nanobot-gateway # view logs
docker compose down # stop docker compose down # stop
``` ```
The default Compose file drops all Linux capabilities and keeps Docker's default The default Compose file drops all Linux capabilities except `CHOWN`, `SETUID`, and
AppArmor/seccomp profiles enabled. If you explicitly set `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 `"tools.exec.sandbox": "bwrap"` in `~/.nanobot/config.json`, add the bwrap
override file when starting containers: 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!" 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 override adds `CAP_SYS_ADMIN` and disables AppArmor/seccomp confinement for the
the container so bubblewrap can create its nested namespaces. Use it only when the container so bubblewrap can create its nested namespaces. It preserves
bwrap sandbox is enabled. `no-new-privileges`. Use it only when the bwrap sandbox is enabled.
### Docker ### Docker
@@ -197,6 +200,8 @@ vim ~/.nanobot/config.json
# health endpoint on 18790. # health endpoint on 18790.
docker run \ docker run \
--cap-drop ALL \ --cap-drop ALL \
--cap-add CHOWN --cap-add SETGID --cap-add SETUID \
--security-opt no-new-privileges:true \
-v ~/.nanobot:/home/nanobot/.nanobot \ -v ~/.nanobot:/home/nanobot/.nanobot \
-p 18790:18790 -p 8765:8765 \ -p 18790:18790 -p 8765:8765 \
nanobot gateway nanobot gateway
@@ -205,7 +210,9 @@ docker run \
# bubblewrap needs for nested namespaces. Without them, `bwrap` may exit with # bubblewrap needs for nested namespaces. Without them, `bwrap` may exit with
# `clone3: Operation not permitted`. # `clone3: Operation not permitted`.
docker run \ 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 apparmor=unconfined \
--security-opt seccomp=unconfined \ --security-opt seccomp=unconfined \
-v ~/.nanobot:/home/nanobot/.nanobot \ -v ~/.nanobot:/home/nanobot/.nanobot \