A single bash script that runs a coding harness (pi by default) inside a rootless Ubuntu 24.04 container with only the current directory mounted at /project, an explicit egress allowlist and an explicit environment: nothing is passed in that was not named on the command line or in a config file. Network enforcement here is empirical rather than assumed (tests/spike_network.sh, docs/networking.md). The original design -- a sidecar proxy on an internal network -- does not work rootless on this host, so allow mode runs pasta with --map-host-loopback, installs an inet aisb ruleset inside the container that drops every egress path except the gate's, and relays through a squid gate bound to the host's loopback that resolves names itself and refuses anything unlisted. The entrypoint checks that the default-drop rule really exists before setpriv drops to uid 1000 with an empty capability set, and exits 77/78 rather than run unenforced. The Containerfile, the entrypoint, the squid config generator and the ruleset renderer all live in bin/ai-sandbox, and the image is tagged by content hash, so a policy change can never silently reuse a stale image. Tests are pure bash: 161 assertions start no container and touch no network, 256 run real ones, including proof that a raw socket, a stripped proxy env and DNS all stay blocked while an allowlisted host works over both http and CONNECT. LLM-Generated: true
72 lines
4.3 KiB
Bash
Executable File
72 lines
4.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# tests/unit/06_entrypoint.sh -- the container-side enforcement script.
|
|
source "$TESTS/lib.sh"
|
|
|
|
PROJ="$WORK/proj"; mkdir -p "$PROJ"; cd "$PROJ" || exit 1
|
|
export AISB_CONFIG_DIR="$WORK/cfg-empty"; mkdir -p "$AISB_CONFIG_DIR"
|
|
|
|
# Extract the entrypoint exactly as it is baked into the image.
|
|
EP="$WORK/entrypoint.sh"
|
|
"$AISB" --print-entrypoint > "$EP" 2>"$WORK/ep.err"; RC=$?
|
|
assert_eq 0 "$RC" "the embedded entrypoint can be printed"
|
|
bash -n "$EP" && _ok "entrypoint is syntactically valid bash" || _bad "entrypoint is syntactically valid bash"
|
|
grep -q 'set -Eeuo pipefail' "$EP" && _ok "entrypoint is strict" || _bad "entrypoint is strict"
|
|
|
|
t_name "the image is described by the script itself"
|
|
sb --print-containerfile
|
|
assert_contains "FROM docker.io/library/ubuntu:24.04" "ubuntu 24.04 base"
|
|
assert_contains "nftables" "image ships nftables"
|
|
assert_contains "util-linux" "image ships setpriv (util-linux)"
|
|
assert_contains "sha256sum -c" "node tarball is checksum verified"
|
|
assert_contains "useradd" "image creates the sandbox user"
|
|
assert_contains 'ENTRYPOINT ["/usr/local/lib/aisb/entrypoint.sh"]' "entrypoint is baked in"
|
|
assert_contains "WORKDIR /project" "project is the default workdir"
|
|
|
|
t_name "generated ruleset shape"
|
|
# Run the rule generator outside a container (nft is not needed to render it).
|
|
POL="$WORK/mnt"; mkdir -p "$POL"
|
|
cat > "$POL/policy.env" <<EOF
|
|
AISB_NET=allow
|
|
AISB_UID=1000
|
|
AISB_GID=1000
|
|
AISB_UNENFORCED=0
|
|
AISB_KEEP_ROOT=0
|
|
AISB_ALLOW_ICMP=1
|
|
AISB_ALLOW_DNS=0
|
|
AISB_PROXY_IPS=100.64.0.1
|
|
AISB_PROXY_PORTS=18081
|
|
EOF
|
|
printf '10.0.0.5 5432\n10.1.0.0/16 80,443\n' > "$POL/direct4.list"
|
|
printf '::1 8080\n' > "$POL/direct6.list"
|
|
|
|
RULES="$AISB_STATE_DIR/rules.f"
|
|
# Render the ruleset by extracting the generator and feeding it the policy above.
|
|
GEN="$WORK/gen.sh"
|
|
awk '/^gen_rules\(\) \{/{f=1} f{print} f&&/^\}$/{exit}' "$EP" > "$GEN"
|
|
{ printf 'POLICY_DIR=%q\nAISB_PROXY_IPS="100.64.0.1"\nAISB_PROXY_PORTS="18081"\nAISB_ALLOW_ICMP=1\nAISB_ALLOW_DNS=0\n' "$POL"
|
|
printf 'gen_rules\n'; } >> "$GEN"
|
|
bash "$GEN" > "$RULES" 2>"$WORK/gen.err" && _ok "rules render" || _bad "rules render" "$(tail -3 "$WORK/gen.err")"
|
|
|
|
grep -q 'table inet aisb' "$RULES" && _ok "inet table (covers v4 and v6)" || _bad "inet table"
|
|
grep -q 'policy drop' "$RULES" && _ok "default policy is drop" || _bad "default policy drop"
|
|
grep -q 'aisb-default-drop' "$RULES" && _ok "explicit default-drop rule exists" || _bad "default-drop rule"
|
|
grep -q 'oifname "lo" accept' "$RULES" && _ok "loopback stays usable" || _bad "loopback accept"
|
|
grep -q 'ip daddr 10.0.0.5 tcp dport { 5432 }' "$RULES" && _ok "per-entry host:port rule" || _bad "host:port rule: $(grep -c . "$RULES") lines"
|
|
grep -q 'ip daddr 10.1.0.0/16 tcp dport { 80,443 }' "$RULES" && _ok "CIDR rule with default ports" || _bad "CIDR rule"
|
|
grep -q 'ip6 daddr ::1 tcp dport { 8080 }' "$RULES" && _ok "ipv6 rule" || _bad "ipv6 rule"
|
|
grep -q 'ip daddr 100.64.0.1 tcp dport 18081' "$RULES" && _ok "gate endpoint is allowed" || _bad "gate rule"
|
|
grep -q 'aisb-est' "$RULES" && _ok "established traffic accepted" || _bad "established rule"
|
|
grep -c 'accept comment "aisb-direct"' "$RULES" | grep -qx 3 && _ok "exactly 3 direct rules" || _bad "direct rule count: $(grep -c 'aisb-direct' "$RULES")"
|
|
|
|
t_name "no DNS hole when the gate is in front"
|
|
grep -E 'th dport 53' "$RULES" >/dev/null && _bad "DNS is closed in gate mode" || _ok "DNS is closed in gate mode"
|
|
|
|
t_name "privilege drop is the last thing that happens"
|
|
tail -5 "$EP" | grep -q 'setpriv' && _ok "exec setpriv at the end" || _bad "setpriv last: $(tail -3 "$EP" | tr '\n' ' ')"
|
|
grep -q -- '--bounding-set=-all' "$EP" && _ok "bounding set cleared" || _bad "bounding set cleared"
|
|
grep -q -- '--inh-caps=-all' "$EP" && _ok "inheritable caps cleared" || _bad "inheritable caps cleared"
|
|
grep -q -- '--no-new-privs' "$EP" && _ok "no new privileges" || _bad "no-new-privs"
|
|
grep -q -- '--reuid=' "$EP" && _ok "uid switched" || _bad "reuid"
|
|
grep -q 'fail "policy file' "$EP" && _ok "missing policy = refuse to start" || _bad "fail closed on missing policy"
|
|
grep -q 'refusing to start\|Refusing to run unenforced' "$EP" && _ok "missing NET_ADMIN = refuse to start" || _bad "fail closed without NET_ADMIN"
|