disinto/tools/edge-control/verify-chat-sandbox.sh

106 lines
3.2 KiB
Bash
Raw Normal View History

#!/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
# 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