add ai-sandbox: rootless podman sandbox for agentic development
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
This commit is contained in:
Executable
+42
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
# tests/unit/01_args.sh -- argument handling.
|
||||
source "$TESTS/lib.sh"
|
||||
|
||||
t_name "help / version"
|
||||
sb --help; assert_rc 0 "--help exits 0"; assert_contains "NETWORK" "--help documents the network section"
|
||||
sb --version; assert_rc 0 "--version exits 0"
|
||||
sb version; assert_contains "ai-sandbox" "version subcommand prints the name"
|
||||
|
||||
t_name "flag -> harness pass-through"
|
||||
A="--allow example.com" # allow mode is the default and fails closed without this
|
||||
sb --dry-run $A --model sonnet; assert_rc 0 "unknown flag is accepted"; assert_contains "pi --model sonnet" "unknown flag and the rest are handed to pi"
|
||||
sb --dry-run $A file.txt --more; assert_contains "pi file.txt --more" "positional starts the command"
|
||||
sb --dry-run $A -- echo hi; assert_contains "echo hi" "after -- the command runs exactly as given"
|
||||
assert_absent "pi echo hi" "after -- the harness is replaced, not prefixed"
|
||||
assert_contains "--network pasta:--map-host-loopback" "and a --net after -- is not mistaken for a sandbox flag"
|
||||
sb --dry-run $A --shell; assert_contains "bash -li" "--shell runs bash"
|
||||
sb --dry-run $A --harness codex; assert_contains "codex" "--harness codex selects the command"
|
||||
sb --dry-run $A --harness none; assert_contains "bash -li" "--harness none drops to a shell"
|
||||
sb --dry-run --net none -- echo hi; assert_contains "echo hi" "--net= form before the command still parses"
|
||||
|
||||
t_name "strict args"
|
||||
sb --strict-args --model sonnet; assert_rc 1 "strict mode rejects unknown flags"
|
||||
assert_contains "unknown option" "strict mode says 'unknown option'"
|
||||
sb --strict-args --al foo; assert_contains "did you mean --allow" "strict mode suggests a near miss"
|
||||
|
||||
t_name "value validation"
|
||||
sb --net bogus --dry-run; assert_rc 1 "bad --net rejected"; assert_contains "--net must be none, allow or full" "names the valid values"
|
||||
sb --pids abc --dry-run; assert_rc 1 "bad --pids rejected"
|
||||
sb --cpus x4 --dry-run; assert_rc 1 "bad --cpus rejected"
|
||||
sb --memory huge --dry-run; assert_rc 1 "bad --memory rejected"
|
||||
sb --allow; assert_rc 1 "--allow with no value"
|
||||
sb --allow-file /nope/missing; assert_rc 1 "--allow-file must exist"
|
||||
sb --mount /nope/missing --dry-run; assert_rc 1 "--mount path must exist"
|
||||
|
||||
t_name "fail-closed network policy"
|
||||
sb --dry-run; assert_rc 1 "allow mode with no allowlist refuses to start"
|
||||
assert_contains "Refusing to start wide open" "explains that it refuses to be wide open"
|
||||
sb --dry-run --net none; assert_rc 0 "--net none needs no allowlist"; assert_contains "--network none" "--net none maps to podman --network none"
|
||||
sb --dry-run --net full; assert_rc 0 "--net full is allowed (explicit opt-in)"
|
||||
assert_contains "unrestricted internet" "--net full warns"
|
||||
sb --dry-run --allow example.com; assert_rc 0 "one allow entry is enough"
|
||||
Executable
+78
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env bash
|
||||
# tests/unit/02_config.sh -- config files, allowlists and precedence.
|
||||
source "$TESTS/lib.sh"
|
||||
|
||||
CFG="$WORK/cfg"
|
||||
REPO="$WORK/repo"
|
||||
mkdir -p "$CFG" "$REPO/.ai-sandbox"
|
||||
export AISB_CONFIG_DIR="$CFG"
|
||||
|
||||
cd "$REPO" || exit 1
|
||||
|
||||
t_name "allow files are merged"
|
||||
cat > "$CFG/allow" <<'EOF'
|
||||
# user-wide allowlist
|
||||
api.openai.com
|
||||
|
||||
registry.npmjs.org # trailing comment
|
||||
EOF
|
||||
cat > "$REPO/.ai-sandbox/allow" <<'EOF'
|
||||
pypi.org
|
||||
EOF
|
||||
sb --print-policy --net allow
|
||||
assert_rc 0 "print-policy runs"
|
||||
assert_contains "api.openai.com" "user allowlist entry present"
|
||||
assert_contains "registry.npmjs.org" "user entry with trailing comment present"
|
||||
assert_contains "pypi.org" "repo allowlist entry present"
|
||||
|
||||
t_name "dedup"
|
||||
sb --print-policy --net allow --allow pypi.org --allow pypi.org
|
||||
ALLOW_LINE="$(printf '%s' "$OUT" | grep '^allow=')"
|
||||
N="$(printf '%s' "$ALLOW_LINE" | grep -o 'pypi.org' | wc -l | tr -d ' ')"
|
||||
assert_eq "1" "$N" "duplicate allow entries collapse into one"
|
||||
|
||||
t_name "key = value config"
|
||||
cat > "$CFG/config" <<'EOF'
|
||||
net = none
|
||||
harness = none
|
||||
cpus = 7
|
||||
memory = 1536m
|
||||
pids = 77
|
||||
selinux = :Z
|
||||
EOF
|
||||
sb --print-policy
|
||||
assert_contains "net=none" "config file sets net"
|
||||
assert_contains "harness=none" "config file sets harness"
|
||||
assert_contains "cpus:7" "config file sets cpus"
|
||||
assert_contains "memory:1536m" "config file sets memory"
|
||||
assert_contains "pids:77" "config file sets pids"
|
||||
assert_contains "selinux=:Z" "config file sets selinux"
|
||||
|
||||
t_name "precedence: flag > env > config file"
|
||||
sb --print-policy --net allow
|
||||
assert_contains "net=allow" "flag beats config file"
|
||||
sb_env AISB_NET=full --print-policy
|
||||
assert_contains "net=full" "AISB_* env beats config file"
|
||||
sb_env AISB_NET=full --print-policy --net none
|
||||
assert_contains "net=none" "flag beats AISB_* env"
|
||||
|
||||
t_name "AISB_ALLOW / AISB_ENV lists"
|
||||
sb_env AISB_ALLOW=a.example.com,b.example.com:8443 --print-policy --net none
|
||||
assert_contains "a.example.com" "AISB_ALLOW entry 1"
|
||||
assert_contains "b.example.com:8443" "AISB_ALLOW entry 2 with port"
|
||||
sb_env AISB_ENV=HOME_X,TMPDIR --print-policy --net none
|
||||
assert_contains "env_names=HOME_X TMPDIR" "AISB_ENV populates env names"
|
||||
assert_contains "env_missing=HOME_X" "unset names are reported by --print-policy"
|
||||
|
||||
t_name "repo config wins per-directory"
|
||||
cat > "$REPO/.ai-sandbox/config" <<'EOF'
|
||||
pids = 42
|
||||
EOF
|
||||
sb --print-policy --net none
|
||||
assert_contains "pids:42" "repo config overrides user config"
|
||||
|
||||
t_name "unknown keys warn but do not break"
|
||||
echo 'bogus_key = 1' >> "$CFG/config"
|
||||
sb --print-policy --net none
|
||||
assert_contains "unknown key: bogus_key" "unknown key warns"
|
||||
assert_rc 0 "unknown key is not fatal"
|
||||
Executable
+86
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env bash
|
||||
# tests/unit/03_argv.sh -- the policy actually reaches the podman invocation.
|
||||
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"
|
||||
A="--allow example.com"
|
||||
|
||||
t_name "isolation flags are always on"
|
||||
sb --dry-run $A
|
||||
assert_contains "--cap-drop=ALL" "all capabilities dropped"
|
||||
assert_contains "no-new-privileges" "no new privileges"
|
||||
assert_contains "--userns keep-id" "uid mapped back to the host user"
|
||||
assert_contains "--pids-limit 512" "pid limit"
|
||||
assert_contains "--memory 4g" "memory limit"
|
||||
assert_contains "--cpus 4" "cpu limit"
|
||||
assert_contains "/tmp:rw,nosuid,nodev" "/tmp is a nosuid tmpfs"
|
||||
assert_contains "--hostname aisb" "hostname"
|
||||
assert_contains "--rm" "container is removed on exit"
|
||||
assert_contains "--stop-signal SIGTERM" "clean shutdown signal"
|
||||
|
||||
t_name "network modes"
|
||||
sb --dry-run --net none
|
||||
assert_contains "--network none" "none"
|
||||
sb --dry-run --net full
|
||||
assert_contains "--network pasta" "full uses pasta with no filter"
|
||||
assert_absent "--cap-add=NET_ADMIN" "full mode needs no NET_ADMIN"
|
||||
sb --dry-run $A
|
||||
assert_contains "--network pasta:--map-host-loopback,100.64.0.1" "allow mode maps a host-loopback door for the gate"
|
||||
assert_contains "--cap-add=NET_ADMIN" "allow mode grants NET_ADMIN to the entrypoint only"
|
||||
assert_contains "HTTP_PROXY=http://100.64.0.1:18081" "proxy env points at the gate"
|
||||
assert_contains "NO_PROXY=localhost" "localhost is never proxied"
|
||||
sb --dry-run $A --proxy 10.9.8.7:3128
|
||||
assert_contains "HTTP_PROXY=http://10.9.8.7:3128" "--proxy overrides the gate"
|
||||
sb --dry-run $A --no-gate
|
||||
assert_absent "HTTP_PROXY=" "--no-gate sets no proxy"
|
||||
|
||||
t_name "filesystem"
|
||||
sb --dry-run $A
|
||||
assert_contains "$PROJ:/project" "cwd is mounted at /project"
|
||||
assert_contains "--workdir /project" "cwd is the working directory"
|
||||
assert_contains "/run/aisb:ro" "policy dir is mounted read-only"
|
||||
sb --dry-run $A --no-cwd
|
||||
assert_absent "$PROJ:/project" "--no-cwd mounts nothing"
|
||||
sb --dry-run $A --workdir /src
|
||||
assert_contains "$PROJ:/src" "--workdir changes the mount point"
|
||||
mkdir -p "$WORK/extra"
|
||||
sb --dry-run $A --mount "$WORK/extra"
|
||||
assert_contains "$WORK/extra:/mnt/extra" "--mount defaults to /mnt/<name>"
|
||||
sb --dry-run $A --mount "$WORK/extra:/ex:ro"
|
||||
assert_contains "$WORK/extra:/ex" "--mount with explicit destination"
|
||||
assert_contains "ro" " ... and ro option present"
|
||||
sb --dry-run $A --read-only
|
||||
assert_contains "--read-only" "--read-only"
|
||||
sb --dry-run $A
|
||||
assert_contains "type=volume,source=aisb-home,target=/home/sandbox" "home cache volume by default"
|
||||
sb --dry-run $A --ephemeral-home
|
||||
assert_contains "/home/sandbox:rw,nosuid,nodev" "--ephemeral-home uses a tmpfs home"
|
||||
assert_absent "type=volume,source=aisb-home" "--ephemeral-home drops the volume"
|
||||
|
||||
t_name "labels, limits, extra args"
|
||||
sb --dry-run $A --label team=x
|
||||
assert_contains "--label team=x" "--label"
|
||||
assert_contains "aisb.net=allow" "session label"
|
||||
sb --dry-run $A --cpus 12 --memory 8g --pids 99
|
||||
assert_contains "--cpus 12" "custom cpus"
|
||||
assert_contains "--memory 8g" "custom memory"
|
||||
assert_contains "--pids-limit 99" "custom pids"
|
||||
sb_env "AISB_EXTRA_ARGS=--label from=extra" --dry-run $A
|
||||
assert_contains "--label from=extra" "AISB_EXTRA_ARGS is appended"
|
||||
|
||||
t_name "everything lands before the image; the command comes last"
|
||||
sb --dry-run $A --label zzz=1 -- echo hello
|
||||
line="$(printf '%s' "$OUT" | tail -1)"
|
||||
pos_img="$(printf '%s' "$line" | tr ' ' '\n' | grep -n -m1 'localhost/ai-sandbox' | cut -d: -f1)"
|
||||
pos_lbl="$(printf '%s' "$line" | tr ' ' '\n' | grep -n -m1 'zzz=1' | cut -d: -f1)"
|
||||
pos_cmd="$(printf '%s' "$line" | tr ' ' '\n' | grep -n -m1 '^hello$' | cut -d: -f1)"
|
||||
if [[ -n "$pos_img" && -n "$pos_lbl" && $pos_lbl -lt $pos_img ]]; then _ok "extra flags precede the image"; else _bad "extra flags precede the image" "img=$pos_img label=$pos_lbl"; fi
|
||||
if [[ -n "$pos_cmd" && -n "$pos_img" && $pos_img -lt $pos_cmd ]]; then _ok "the command follows the image"; else _bad "the command follows the image" "img=$pos_img cmd=$pos_cmd"; fi
|
||||
|
||||
t_name "dry run is side-effect free"
|
||||
sb --dry-run $A
|
||||
RUNNING="$(podman ps --format '{{.Names}}' | grep -c 'aisb' || true)"
|
||||
DIRS="$(ls -A "$AISB_STATE_DIR/sessions" 2>/dev/null | wc -l | tr -d ' ')"
|
||||
assert_eq "$RUNNING" 0 "dry run started no container"
|
||||
assert_eq "$DIRS" 0 "dry run left no session directory behind"
|
||||
Executable
+65
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
# tests/unit/04_env.sh -- environment passing and secret hygiene.
|
||||
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"
|
||||
export AISB_CANARY=hunter2-secret-value
|
||||
export AISB_EMPTY_VAR=""
|
||||
unset AISB_NOT_SET
|
||||
|
||||
t_name "deny by default"
|
||||
sb --print-policy --net none
|
||||
assert_eq "env_names=" "$(printf '%s' "$OUT" | grep '^env_names=')" "nothing is passed by default"
|
||||
assert_absent "CANARY" "a host variable is not passed unless asked for"
|
||||
|
||||
t_name "explicit passing"
|
||||
sb --print-policy --net none --env AISB_CANARY
|
||||
assert_contains "env_names=AISB_CANARY" "named variable is queued"
|
||||
assert_absent "hunter2" "the value never appears in --print-policy output"
|
||||
sb --dry-run --net none --env AISB_CANARY
|
||||
assert_absent "hunter2" "the value never appears in the printed podman invocation"
|
||||
assert_contains "<env-file:0600>" "values travel through a 0600 env file, not the command line"
|
||||
|
||||
t_name "set-but-empty passes, unset fails closed"
|
||||
sb --print-policy --net none --env AISB_EMPTY_VAR
|
||||
assert_contains "env_names=AISB_EMPTY_VAR" "empty value still passes"
|
||||
assert_absent "env_missing=AISB_EMPTY_VAR" "empty value is not 'missing'"
|
||||
sb --print-policy --net none --env AISB_NOT_SET
|
||||
assert_rc 0 "--print-policy warns instead of dying"
|
||||
assert_contains "not set in the host environment" "the warning names the problem"
|
||||
assert_contains "env_missing=AISB_NOT_SET" "env_missing reports it"
|
||||
|
||||
t_name "literal values"
|
||||
sb --dry-run --net none --env FOO=bar
|
||||
assert_contains "<env-file:0600>" "literal values also go through the env file"
|
||||
assert_absent "bar" "literal values are not echoed into the invocation"
|
||||
|
||||
t_name "env files"
|
||||
cat > "$WORK/envfile" <<'EOF'
|
||||
# comment
|
||||
AISENV_ONE=1
|
||||
AISENV_TWO=with spaces
|
||||
AISENV_THREE=has=equals
|
||||
EOF
|
||||
chmod 600 "$WORK/envfile"
|
||||
sb --print-policy --net none --env-file "$WORK/envfile"
|
||||
assert_contains "env_names=AISENV_ONE AISENV_TWO AISENV_THREE" "all three names queued"
|
||||
sb_env AISENV_ONE=1 --print-policy --net none --env AISB_CANARY --env-file "$WORK/envfile"
|
||||
assert_contains "AISENV_THREE" "env file entries pass"
|
||||
assert_contains "AISB_CANARY" "and combine with --env"
|
||||
|
||||
t_name "loose env-file permissions warn"
|
||||
chmod 644 "$WORK/envfile"
|
||||
sb --print-policy --net none --env-file "$WORK/envfile"
|
||||
assert_contains "chmod 600 recommended" "world-readable env file warns"
|
||||
chmod 600 "$WORK/envfile"
|
||||
|
||||
t_name "invalid names and values"
|
||||
sb --print-policy --net none --env 9BAD
|
||||
assert_rc 1 "invalid variable name rejected"
|
||||
sb --print-policy --net none --env-file "$WORK/does-not-exist"
|
||||
assert_rc 1 "missing env file rejected"
|
||||
export AISB_MULTILINE=$'one\ntwo'
|
||||
sb --print-policy --net none --env AISB_MULTILINE
|
||||
assert_rc 1 "multi-line values are rejected rather than silently truncated"
|
||||
Executable
+48
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
# tests/unit/05_policy.sh -- how allow entries are classified.
|
||||
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"
|
||||
|
||||
direct_line() { printf '%s' "$1" | grep '^direct=' | cut -d= -f2-; }
|
||||
|
||||
t_name "domains go to the gate, not to netfilter"
|
||||
sb --print-policy --net allow --allow api.openai.com
|
||||
assert_contains "gate_domains=api.openai.com" "domain listed for the gate"
|
||||
assert_eq "" "$(direct_line "$OUT")" "no netfilter entry for a domain in gate mode"
|
||||
assert_contains "proxy=100.64.0.1:18081" "gate address is the proxy"
|
||||
|
||||
t_name "IP and CIDR entries become direct rules"
|
||||
sb --print-policy --net allow --allow 10.0.0.5:5432
|
||||
assert_eq "direct=10.0.0.5 5432" "$(printf '%s' "$OUT" | grep '^direct=')" "explicit host:port"
|
||||
sb --print-policy --net allow --allow 10.1.0.0/16
|
||||
assert_eq "direct=10.1.0.0/16 80,443" "$(printf '%s' "$OUT" | grep '^direct=')" "CIDR gets the default ports"
|
||||
sb --print-policy --net allow --allow 10.0.0.5:5432 --allow 10.9.9.9
|
||||
assert_eq "direct=10.0.0.5 5432;10.9.9.9 80,443" "$(printf '%s' "$OUT" | grep '^direct=')" "several direct entries"
|
||||
|
||||
t_name "ipv6"
|
||||
sb --print-policy --net allow --allow "[::1]:8080"
|
||||
assert_eq "direct6=::1 8080" "$(printf '%s' "$OUT" | grep '^direct6=')" "bracketed [ipv6]:port parses"
|
||||
assert_eq "direct=" "$(printf '%s' "$OUT" | grep '^direct=')" "and stays out of the ipv4 rules"
|
||||
assert_contains "gate_domains=" "policy printed for an ipv6 entry"
|
||||
|
||||
t_name "--no-gate resolves names up front"
|
||||
sb --print-policy --net allow --no-gate --allow localhost
|
||||
assert_contains "direct=127.0.0.1 80,443" "localhost resolves to a direct rule"
|
||||
assert_contains "proxy=none" "--no-gate has no proxy"
|
||||
sb --print-policy --net allow --no-gate --allow nope.invalid
|
||||
assert_rc 1 "an unresolvable name is an error, not a silent no-op"
|
||||
assert_contains "could not resolve" "and it says so"
|
||||
|
||||
t_name "ports"
|
||||
sb --allow api.openai.com:not-a-port --print-policy
|
||||
assert_rc 1 "non-numeric port rejected"
|
||||
sb --print-policy --net allow --allow api.openai.com:8443
|
||||
assert_contains "gate_domains=api.openai.com" "domain+port still routed through the gate"
|
||||
|
||||
t_name "mixed list"
|
||||
sb --print-policy --net allow --allow api.openai.com --allow 10.0.0.5:5432 --allow files.pythonhosted.org
|
||||
assert_contains "api.openai.com" "domain 1"
|
||||
assert_contains "files.pythonhosted.org" "domain 2"
|
||||
assert_contains "10.0.0.5 5432" "ip entry"
|
||||
Executable
+71
@@ -0,0 +1,71 @@
|
||||
#!/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"
|
||||
Reference in New Issue
Block a user