add Phase 2B contained runtime artifacts
This commit is contained in:
parent
a9fec0d83d
commit
82e64cec43
35 changed files with 5177 additions and 40 deletions
|
|
@ -0,0 +1,28 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
test "$(id -u)" -eq 0
|
||||
test "$(docker version --format '{{.Server.Version}}')" = 29.6.1
|
||||
test "$(sha256sum payload/usr/local/libexec/dockerd-rootless-29.6.1.sh | cut -d' ' -f1)" = 904c9b9e35f6927c0a5e65afb4d35b6bc9eb1278c878044501281fc728c9be46
|
||||
test ! -S /run/user/1200/docker.sock
|
||||
test "$(systemctl show user@1200.service -p ActiveState --value)" = inactive
|
||||
test ! -e /var/lib/systemd/linger/le_app_codex
|
||||
test -z "$(pgrep -u 1200 || true)"
|
||||
test ! -e /run/netns/le-app-codex
|
||||
test ! -e /etc/systemd/system/user@1200.service.d/50-le-app-codex-containment.conf
|
||||
test ! -e /etc/systemd/system/user-1200.slice.d/50-le-app-codex-resources.conf
|
||||
docker network inspect $(docker network ls -q) --format '{{.Name}} {{range .IPAM.Config}}{{.Subnet}} {{end}}'
|
||||
ip -brief address
|
||||
ip -4 route show table all
|
||||
ip -6 route show table all
|
||||
sed -n '1,120p' /etc/resolv.conf
|
||||
nft --numeric list ruleset
|
||||
systemctl cat user@1200.service
|
||||
loginctl show-user 1200 || true
|
||||
namei -om /srv/le-app-codex
|
||||
find /srv/le-app-codex -xdev -printf '%M %u:%g %p\n'
|
||||
getfacl -R -p /srv/le-app-codex
|
||||
findmnt -R /srv/le-app-codex
|
||||
grep -q 'elements = { 9\.9\.9\.9, 149\.112\.112\.112 }' payload/etc/nftables.d/50-le-app-codex.nft
|
||||
grep -q 'elements = { 74\.67\.173\.56 }' payload/etc/nftables.d/50-le-app-codex.nft
|
||||
grep -q '^nameserver 9\.9\.9\.9$' payload/etc/le-app-codex-runtime/resolv.conf
|
||||
grep -q '^nameserver 149\.112\.112\.112$' payload/etc/le-app-codex-runtime/resolv.conf
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
#!/bin/sh
|
||||
|
||||
failures=0
|
||||
artifact_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." 2>/dev/null && pwd)
|
||||
|
||||
pass() { printf 'PASS: %s\n' "$1"; }
|
||||
fail() { printf 'FAIL: %s\n' "$1" >&2; failures=$((failures + 1)); }
|
||||
run_check() {
|
||||
label=$1
|
||||
shift
|
||||
if "$@"; then pass "$label"; else fail "$label"; fi
|
||||
}
|
||||
expect_stat() {
|
||||
path=$1 expected=$2
|
||||
actual=$(/usr/bin/stat -c '%u:%g:%a:%F' "$path" 2>/dev/null)
|
||||
if [ "$actual" = "$expected" ]; then pass "$path ownership/mode"; else fail "$path expected $expected, got ${actual:-missing}"; fi
|
||||
}
|
||||
expect_absent() {
|
||||
path=$1
|
||||
if [ ! -e "$path" ] && [ ! -L "$path" ]; then pass "$path absent"; else fail "$path must not already exist"; fi
|
||||
}
|
||||
|
||||
if [ "$(id -u)" = 0 ]; then pass 'running as root'; else fail 'must be run as root'; fi
|
||||
if [ -n "$artifact_root" ] && [ -d "$artifact_root/payload" ]; then pass 'artifact root resolved'; else fail 'artifact root not found'; fi
|
||||
|
||||
if (cd "$artifact_root" && /usr/bin/sha256sum -c MANIFEST.sha256); then pass 'complete artifact manifest'; else fail 'complete artifact manifest'; fi
|
||||
if (cd "$artifact_root/inventory" && /usr/bin/sha256sum -c SHA256SUMS); then pass 'original root inventory'; else fail 'original root inventory'; fi
|
||||
|
||||
run_check 'staged nftables syntax' /usr/bin/nft --check -f "$artifact_root/payload/etc/nftables.d/50-le-app-codex.nft"
|
||||
|
||||
system_unit_path="$artifact_root/payload/etc/systemd/system:/etc/systemd/system:/run/systemd/system:/usr/local/lib/systemd/system:/usr/lib/systemd/system"
|
||||
systemd_output=$(SYSTEMD_UNIT_PATH="$system_unit_path" /usr/bin/systemd-analyze verify --recursive-errors=no user@1200.service user-1200.slice le-app-codex-netns.service 2>&1)
|
||||
systemd_rc=$?
|
||||
printf '%s\n' "$systemd_output"
|
||||
systemd_unexpected=$(printf '%s\n' "$systemd_output" | /usr/bin/sed -e '\#le-app-codex-netns.service: Command /usr/local/libexec/le-app-codex-netns is not executable: No such file or directory#d')
|
||||
if [ "$systemd_rc" -eq 0 ] || [ -z "$systemd_unexpected" ]; then pass 'staged system units/drop-ins via host search path'; else fail 'staged system units/drop-ins via host search path'; fi
|
||||
|
||||
user_unit_path="$artifact_root/payload/srv/le-app-codex/home/.config/systemd/user:/etc/systemd/user:/run/systemd/user:/usr/local/lib/systemd/user:/usr/lib/systemd/user"
|
||||
user_systemd_output=$(SYSTEMD_UNIT_PATH="$user_unit_path" /usr/bin/systemd-analyze verify --recursive-errors=no docker.service 2>&1)
|
||||
user_systemd_rc=$?
|
||||
printf '%s\n' "$user_systemd_output"
|
||||
user_systemd_unexpected=$(printf '%s\n' "$user_systemd_output" | /usr/bin/sed -e '\#docker.service: Command /usr/local/libexec/dockerd-rootless-29.6.1.sh is not executable: No such file or directory#d')
|
||||
if [ "$user_systemd_rc" -eq 0 ] || [ -z "$user_systemd_unexpected" ]; then pass 'staged Docker user unit via host search path'; else fail 'staged Docker user unit via host search path'; fi
|
||||
|
||||
shell_failures=0
|
||||
for file in "$artifact_root"/tests/*.sh "$artifact_root"/payload/usr/local/libexec/*; do
|
||||
/bin/sh -n "$file" || shell_failures=$((shell_failures + 1))
|
||||
done
|
||||
if [ "$shell_failures" -eq 0 ]; then pass 'shell syntax'; else fail 'shell syntax'; fi
|
||||
|
||||
json_failures=0
|
||||
for file in "$artifact_root"/payload/srv/le-app-codex/home/.config/docker/daemon.json \
|
||||
"$artifact_root"/payload/srv/le-app-codex/home/.docker/config.json \
|
||||
"$artifact_root"/payload/srv/le-app-codex/home/.docker/contexts/meta/1b645e55bd6af77d420732b143423e7a27b12fbb5a71497e5d89c52481b6a535/meta.json \
|
||||
"$artifact_root"/inventory/docker-networks.json; do
|
||||
/usr/bin/jq empty "$file" || json_failures=$((json_failures + 1))
|
||||
done
|
||||
if [ "$json_failures" -eq 0 ]; then pass 'JSON syntax'; else fail 'JSON syntax'; fi
|
||||
|
||||
expect_stat /srv/le-app-codex '0:1200:750:directory'
|
||||
expect_stat /srv/le-app-codex/home '1200:1200:700:directory'
|
||||
expect_stat /srv/le-app-codex/lasereverything.net.db '1200:1200:700:directory'
|
||||
expect_stat /srv/le-app-codex/database-source-readonly '0:1200:550:directory'
|
||||
expect_stat /srv/le-app-codex/nonproduction-secrets '0:1200:750:directory'
|
||||
expect_stat /srv/le-app-codex/container-runtime '1200:1200:700:directory'
|
||||
expect_stat /srv/le-app-codex/testing-data '1200:1200:700:directory'
|
||||
expect_stat /srv/le-app-codex/build-artifacts '1200:1200:700:directory'
|
||||
|
||||
project_mounts=$(/usr/bin/findmnt -rn -o TARGET | /usr/bin/awk '$0 == "/srv/le-app-codex" || index($0, "/srv/le-app-codex/") == 1 { print }')
|
||||
if [ -z "$project_mounts" ]; then pass 'no project-rooted mounts'; else fail "unexpected project-rooted mounts: $project_mounts"; fi
|
||||
|
||||
expect_absent /etc/le-app-codex-runtime/OPERATOR-INPUTS-APPROVED
|
||||
expect_absent /etc/le-app-codex-runtime/resolv.conf
|
||||
expect_absent /etc/nftables.d/50-le-app-codex.nft
|
||||
expect_absent /etc/systemd/system/le-app-codex-netns.service
|
||||
expect_absent /etc/systemd/system/user-1200.slice.d/50-le-app-codex-resources.conf
|
||||
expect_absent /etc/systemd/system/user@1200.service.d/50-le-app-codex-containment.conf
|
||||
expect_absent /usr/local/libexec/dockerd-rootless-29.6.1.sh
|
||||
expect_absent /usr/local/libexec/le-app-codex-netns
|
||||
expect_absent /srv/le-app-codex/home/.config/systemd/user/docker.service
|
||||
expect_absent /srv/le-app-codex/home/.config/docker/daemon.json
|
||||
expect_absent /srv/le-app-codex/home/.docker/config.json
|
||||
expect_absent /srv/le-app-codex/home/.docker/contexts/meta/1b645e55bd6af77d420732b143423e7a27b12fbb5a71497e5d89c52481b6a535/meta.json
|
||||
expect_absent /srv/le-app-codex/phase2b-tests/00-root-preinstall-validate.sh
|
||||
expect_absent /srv/le-app-codex/phase2b-tests/00-read-only-preflight.sh
|
||||
expect_absent /srv/le-app-codex/phase2b-tests/10-inert-containment.sh
|
||||
expect_absent /srv/le-app-codex/phase2b-tests/20-rootless-docker.sh
|
||||
expect_absent /srv/le-app-codex/phase2b-tests/run-inert-without-user-manager.sh
|
||||
|
||||
active_state=$(/usr/bin/systemctl show user@1200.service -p ActiveState --value 2>/dev/null)
|
||||
if [ "$active_state" = inactive ]; then pass 'user@1200.service inactive'; else fail "user@1200.service state is ${active_state:-unknown}"; fi
|
||||
if [ ! -e /var/lib/systemd/linger/le_app_codex ] && [ ! -e /var/lib/systemd/linger/1200 ]; then pass 'linger absent'; else fail 'linger present'; fi
|
||||
uid_processes=$(/usr/bin/pgrep -u 1200 2>/dev/null)
|
||||
if [ -z "$uid_processes" ]; then pass 'UID 1200 has no processes'; else fail "UID 1200 processes found: $uid_processes"; fi
|
||||
if [ ! -S /run/user/1200/docker.sock ]; then pass 'rootless Docker socket absent'; else fail 'rootless Docker socket exists'; fi
|
||||
|
||||
public_ipv4=$(/usr/bin/curl -4fsS --max-time 10 https://api.ipify.org 2>/dev/null)
|
||||
if [ "$public_ipv4" = 74.67.173.56 ]; then pass 'public/NAT IPv4 unchanged'; else fail "public/NAT IPv4 drift or lookup failure: ${public_ipv4:-unavailable}"; fi
|
||||
|
||||
printf 'root pre-install validation failures=%s\n' "$failures"
|
||||
[ "$failures" -eq 0 ]
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
fail=0
|
||||
expect_denied() { if "$@" >/dev/null 2>&1; then echo "UNEXPECTED SUCCESS: $*" >&2; fail=1; fi; }
|
||||
|
||||
test "$(id -u)" -eq 1200
|
||||
test "$DOCKER_HOST" = unix:///run/user/1200/docker.sock
|
||||
touch /srv/le-app-codex/tmp/phase2b-write-test
|
||||
rm /srv/le-app-codex/tmp/phase2b-write-test
|
||||
expect_denied ls /var/www
|
||||
expect_denied ls /root
|
||||
expect_denied ls /home
|
||||
expect_denied ls /srv/directus-archive
|
||||
expect_denied ls /srv/codex-work
|
||||
expect_denied stat /run/docker.sock
|
||||
expect_denied stat /var/run/docker.sock
|
||||
expect_denied stat /run/containerd/containerd.sock
|
||||
expect_denied stat /dev/sda
|
||||
expect_denied stat /proc/1/root
|
||||
expect_denied sh -c 'echo x > /etc/phase2b-write-test'
|
||||
expect_denied sh -c 'echo x > /srv/phase2b-write-test'
|
||||
expect_denied curl -4fsS --connect-timeout 2 http://127.0.0.1
|
||||
expect_denied curl -4fsS --connect-timeout 2 http://10.200.120.1
|
||||
expect_denied curl -4fsS --connect-timeout 2 http://192.168.10.151
|
||||
expect_denied nc -w2 -z 192.168.10.1 53
|
||||
expect_denied curl -4fsS --connect-timeout 2 http://10.98.0.2
|
||||
expect_denied curl -4fsS --connect-timeout 2 http://74.67.173.56
|
||||
expect_denied curl -4fsS --connect-timeout 2 http://169.254.169.254/latest/meta-data/
|
||||
expect_denied curl -6fsS --connect-timeout 2 https://[::1]/
|
||||
expect_denied nc -w2 -z 1.1.1.1 25
|
||||
expect_denied nc -w2 -z 1.1.1.1 465
|
||||
expect_denied nc -w2 -z 1.1.1.1 587
|
||||
test "$(ps -e --no-headers | wc -l)" -lt 32
|
||||
test -w /dev/fuse
|
||||
test -w /dev/net/tun
|
||||
grep -q '^nameserver 9\.9\.9\.9$' /etc/resolv.conf
|
||||
grep -q '^nameserver 149\.112\.112\.112$' /etc/resolv.conf
|
||||
getent ahostsv4 example.com >/dev/null
|
||||
test -r /sys/fs/cgroup/cpu.max
|
||||
test -r /sys/fs/cgroup/memory.max
|
||||
exit "$fail"
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
fail=0
|
||||
export HOME=/srv/le-app-codex/home
|
||||
export DOCKER_HOST=unix:///run/user/1200/docker.sock
|
||||
ALPINE='docker.io/library/alpine:3.23.3@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659'
|
||||
NGINX='docker.io/library/nginx:1.29.4-alpine@sha256:4870c12cd2ca986de501a804b4f506ad3875a0b1874940ba0a2c7f763f1855b2'
|
||||
test "$(id -u)" -eq 1200
|
||||
test -S /run/user/1200/docker.sock
|
||||
test "$(docker context show)" = le-app-codex
|
||||
test "$(docker context inspect le-app-codex --format '{{.Endpoints.docker.Host}}')" = unix:///run/user/1200/docker.sock
|
||||
docker info --format '{{json .SecurityOptions}}' | grep -q rootless
|
||||
test "$(docker info --format '{{.DockerRootDir}}')" = /srv/le-app-codex/container-runtime/docker
|
||||
docker run --rm --network none "$ALPINE" true
|
||||
docker run --rm "$ALPINE" wget -qO- https://example.com >/dev/null
|
||||
for target in 127.0.0.1 10.200.120.1 192.168.10.1 192.168.10.151 10.98.0.2 74.67.173.56 169.254.169.254; do
|
||||
if docker run --rm "$ALPINE" wget -T2 -qO- "http://$target" >/dev/null 2>&1; then
|
||||
echo "UNEXPECTED CONTAINER ACCESS: $target" >&2; fail=1
|
||||
fi
|
||||
done
|
||||
if docker run --rm "$ALPINE" sh -c 'nc -w2 -z 1.1.1.1 25 || nc -w2 -z 1.1.1.1 465 || nc -w2 -z 1.1.1.1 587'; then
|
||||
echo 'UNEXPECTED CONTAINER SMTP ACCESS' >&2; fail=1
|
||||
fi
|
||||
cid=$(docker run -d --rm -p 18080:80 "$NGINX")
|
||||
trap 'docker rm -f "$cid" >/dev/null 2>&1 || true' EXIT
|
||||
sleep 2
|
||||
for host in 127.0.0.1 192.168.10.151 10.98.0.2; do
|
||||
if curl -fsS --connect-timeout 2 "http://$host:18080" >/dev/null 2>&1; then
|
||||
echo "UNEXPECTED PUBLISHED PORT: $host" >&2; fail=1
|
||||
fi
|
||||
done
|
||||
docker rm -f "$cid" >/dev/null
|
||||
trap - EXIT
|
||||
test ! -S /run/docker.sock || ! docker -H unix:///run/docker.sock info >/dev/null 2>&1
|
||||
exit "$fail"
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
test "$(id -u)" -eq 0
|
||||
test "$(systemctl is-active user@1200.service || true)" = inactive
|
||||
systemd-run --wait --pipe --collect --unit=le-app-codex-inert-test \
|
||||
--uid=1200 --gid=1200 \
|
||||
--setenv=DOCKER_HOST=unix:///run/user/1200/docker.sock \
|
||||
--property=NetworkNamespacePath=/run/netns/le-app-codex \
|
||||
--property=ProtectSystem=strict --property=PrivateTmp=yes \
|
||||
--property=ProtectHome=yes --property=ProtectProc=invisible \
|
||||
--property=TemporaryFileSystem=/srv:ro \
|
||||
--property=BindPaths=/srv/le-app-codex:/srv/le-app-codex \
|
||||
--property=TemporaryFileSystem=/var:ro \
|
||||
--property=TemporaryFileSystem=/mnt:ro \
|
||||
--property=TemporaryFileSystem=/media:ro \
|
||||
--property=TemporaryFileSystem=/opt:ro \
|
||||
--property=DevicePolicy=closed --property='DeviceAllow=/dev/fuse rw' \
|
||||
--property='DeviceAllow=/dev/net/tun rw' \
|
||||
/srv/le-app-codex/phase2b-tests/10-inert-containment.sh
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue