#!/usr/bin/env bash set -euo pipefail [[ "$#" -eq 1 && "$1" = /* ]] || { echo "usage: test-ingress-runtime.sh NEW_OS_TEMP_DIRECTORY" >&2 exit 2 } repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" work="$(realpath -m "$1")" case "${work}" in /tmp/* | /var/tmp/*) ;; *) echo "runtime test requires an OS temporary directory" >&2 exit 2 ;; esac # Refuse existing data; retain this run's isolated storage/evidence for inspection. mkdir -m 0700 "${work}" mkdir "${work}/tmp" "${work}/socket" export TMPDIR="${work}/tmp" p() { podman --root "${work}/store" --runroot "${work}/run" --storage-driver vfs "$@"; } cleanup() { p rm -f ingress-test pufferpanel >/dev/null 2>&1 || true p network rm ingress-test >/dev/null 2>&1 || true } trap cleanup EXIT cp "${repo_root}/services/pufferpanel/deploy/gateway.Caddyfile" "${work}/gateway.Caddyfile" cat >"${work}/backend.Caddyfile" <<'CONFIG' { admin off auto_https off persist_config off } http://:8080 { respond "{http.request.header.X-Forwarded-Proto}" 200 } CONFIG # Stock Caddy's file capability needs a matching rootless capability bound. if p run --rm --network none --read-only --image-volume ignore --cap-drop all \ --security-opt no-new-privileges docker.io/library/caddy:2 caddy version \ >"${work}/no-capability.log" 2>&1; then echo "stock image no longer needs NET_BIND_SERVICE; review gateway capability" >&2 exit 1 fi grep -F 'Operation not permitted' "${work}/no-capability.log" >/dev/null p run --rm --network none --read-only --image-volume ignore --cap-drop all \ --cap-add NET_BIND_SERVICE --security-opt no-new-privileges \ docker.io/library/caddy:2 caddy version >"${work}/version.log" p network create --internal ingress-test >/dev/null p run -d --name pufferpanel --network ingress-test --read-only --image-volume ignore \ --cap-drop all --cap-add NET_BIND_SERVICE --security-opt no-new-privileges \ -v "${work}/backend.Caddyfile:/etc/caddy/Caddyfile:ro" docker.io/library/caddy:2 >/dev/null p run -d --name ingress-test --network ingress-test --read-only --image-volume ignore \ --cap-drop all --cap-add NET_BIND_SERVICE --security-opt no-new-privileges \ -v "${work}/gateway.Caddyfile:/etc/caddy/Caddyfile:ro" \ -v "${work}/socket:/run/pufferpanel-ingress" docker.io/library/caddy:2 >/dev/null for _ in $(seq 1 100); do [[ -S "${work}/socket/http.sock" ]] && break sleep 0.1 done [[ -S "${work}/socket/http.sock" ]] response="$(curl --fail --silent --show-error --unix-socket "${work}/socket/http.sock" \ -H 'Host: games.bugabinga.net' http://localhost/api/proof)" [[ "${response}" == https ]] status="$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' \ --unix-socket "${work}/socket/http.sock" -H 'Host: invalid.example' http://localhost/api/proof)" [[ "${status}" == 404 ]] p inspect ingress-test | jq -e '.[0] | .HostConfig.ReadonlyRootfs and (.Mounts | length == 2)' >/dev/null printf '%s\n' 'Stock Caddy capability control, private-network DNS, Unix HTTP, HTTPS forwarding, host filter, and mount boundaries passed' printf 'Retained isolated test evidence: %s\n' "${work}"