2026-04-12 01:08:23 +00:00
|
|
|
#!/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
|
|
|
|
|
|
2026-04-12 01:19:42 +00:00
|
|
|
# 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
|
|
|
|
|
|
2026-04-12 01:08:23 +00:00
|
|
|
# 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
|