disinto/tools/edge-control/verify-chat-sandbox.sh
Claude 0c5bb09e16
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
ci/woodpecker/pr/smoke-init Pipeline was successful
fix: address review — move LOGFILE to tmpfs, add CapDrop check (#706)
LOGFILE=/var/chat/chat.log is unwritable on read-only rootfs; move to
/tmp/chat.log (tmpfs-backed). Add CapDrop=ALL assertion to verify script
so removing cap_drop from compose is caught.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 01:19:42 +00:00

113 lines
3.5 KiB
Bash
Executable file

#!/usr/bin/env bash
set -euo pipefail
# verify-chat-sandbox.sh — One-shot sandbox verification for disinto-chat (#706)
#
# Runs against a live compose project and asserts hardening constraints.
# Exit 0 if all pass, non-zero otherwise.
CONTAINER="disinto-chat"
PASS=0
FAIL=0
pass() { printf ' ✓ %s\n' "$1"; PASS=$((PASS + 1)); }
fail() { printf ' ✗ %s\n' "$1"; FAIL=$((FAIL + 1)); }
echo "=== disinto-chat sandbox verification ==="
echo
# --- docker inspect checks ---
inspect_json=$(docker inspect "$CONTAINER" 2>/dev/null) || {
echo "ERROR: container '$CONTAINER' not found or not running"
exit 1
}
# ReadonlyRootfs
readonly_rootfs=$(echo "$inspect_json" | python3 -c "import sys,json; print(json.load(sys.stdin)[0]['HostConfig']['ReadonlyRootfs'])")
if [ "$readonly_rootfs" = "True" ]; then
pass "ReadonlyRootfs=true"
else
fail "ReadonlyRootfs expected true, got $readonly_rootfs"
fi
# CapAdd — should be null or empty
cap_add=$(echo "$inspect_json" | python3 -c "import sys,json; print(json.load(sys.stdin)[0]['HostConfig']['CapAdd'])")
if [ "$cap_add" = "None" ] || [ "$cap_add" = "[]" ]; then
pass "CapAdd=null (no extra capabilities)"
else
fail "CapAdd expected null, got $cap_add"
fi
# CapDrop — should contain ALL
cap_drop=$(echo "$inspect_json" | python3 -c "import sys,json; caps=json.load(sys.stdin)[0]['HostConfig']['CapDrop'] or []; print(' '.join(caps))")
if echo "$cap_drop" | grep -q "ALL"; then
pass "CapDrop contains ALL"
else
fail "CapDrop expected ALL, got: $cap_drop"
fi
# PidsLimit
pids_limit=$(echo "$inspect_json" | python3 -c "import sys,json; print(json.load(sys.stdin)[0]['HostConfig']['PidsLimit'])")
if [ "$pids_limit" = "128" ]; then
pass "PidsLimit=128"
else
fail "PidsLimit expected 128, got $pids_limit"
fi
# Memory limit (512MB = 536870912 bytes)
mem_limit=$(echo "$inspect_json" | python3 -c "import sys,json; print(json.load(sys.stdin)[0]['HostConfig']['Memory'])")
if [ "$mem_limit" = "536870912" ]; then
pass "Memory=512m"
else
fail "Memory expected 536870912, got $mem_limit"
fi
# SecurityOpt — must contain no-new-privileges
sec_opt=$(echo "$inspect_json" | python3 -c "import sys,json; opts=json.load(sys.stdin)[0]['HostConfig']['SecurityOpt'] or []; print(' '.join(opts))")
if echo "$sec_opt" | grep -q "no-new-privileges"; then
pass "SecurityOpt contains no-new-privileges"
else
fail "SecurityOpt missing no-new-privileges (got: $sec_opt)"
fi
# No docker.sock bind mount
binds=$(echo "$inspect_json" | python3 -c "import sys,json; binds=json.load(sys.stdin)[0]['HostConfig']['Binds'] or []; print(' '.join(binds))")
if echo "$binds" | grep -q "docker.sock"; then
fail "docker.sock is bind-mounted"
else
pass "No docker.sock mount"
fi
echo
# --- runtime exec checks ---
# touch /root/x should fail (read-only rootfs + unprivileged user)
if docker exec "$CONTAINER" touch /root/x 2>/dev/null; then
fail "touch /root/x succeeded (should fail)"
else
pass "touch /root/x correctly denied"
fi
# /var/run/docker.sock must not exist
if docker exec "$CONTAINER" ls /var/run/docker.sock 2>/dev/null; then
fail "/var/run/docker.sock is accessible"
else
pass "/var/run/docker.sock not accessible"
fi
# /etc/shadow should not be readable
if docker exec "$CONTAINER" cat /etc/shadow 2>/dev/null; then
fail "cat /etc/shadow succeeded (should fail)"
else
pass "cat /etc/shadow correctly denied"
fi
echo
echo "=== Results: $PASS passed, $FAIL failed ==="
if [ "$FAIL" -gt 0 ]; then
exit 1
fi
exit 0