Files
Ekklesia/scripts/check-image.sh
ekklesia 4be046c8d0
CI / verify (pull_request) Successful in 1m10s
CI / infrastructure-smoke (pull_request) Successful in 37s
CI / migration-smoke (pull_request) Successful in 1m54s
CI / package-release (pull_request) Successful in 36s
CI / image-build (pull_request) Successful in 52s
CI / deploy-smoke (same-origin) (pull_request) Successful in 51s
CI / deploy-smoke (split) (pull_request) Successful in 1m31s
CI / package-publication (pull_request) Successful in 51s
fix: support native package verifier scratch
2026-10-09 18:05:04 -05:00

213 lines
10 KiB
Bash
Executable File

#!/usr/bin/env bash
# Proves that Ekklesia's image is the one generic platform artifact ADR 0048 requires:
#
# - it contains zero feature modules: no /opt/ekklesia/modules, no module template, and no
# ekklesia.module.json anywhere outside installed dependencies;
# - its module storage mount point, /var/lib/ekklesia/modules, exists, is root-owned, and is empty;
# - EKKLESIA_MODULES_DIR names that directory, in the image configuration and in a running process;
# - a checkout with a populated modules/ directory builds the same image: the runtime file system
# of both builds is identical, and no module from that directory enters it;
# - the hardened verifier keeps /tmp noexec while a dedicated bounded execution scratch loads and
# runs the real sharp 0.35.5/libvips 1.3.4 native proof package.
#
# bash scripts/check-image.sh builds both images from this checkout
# EKKLESIA_CHECK_IMAGE=<image> bash scripts/check-image.sh checks one existing image's contents
#
# Both builds use a copy of the files Git would track (committed, staged, or untracked but not
# ignored), so they differ in exactly one thing: the second context also has modules/example. Images
# are built and inspected locally only; nothing is pushed, published, or logged in to.
set -Eeuo pipefail
repository_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$repository_root"
# shellcheck source=scripts/lib/ci-project.sh
source "$repository_root/scripts/lib/ci-project.sh"
command -v docker >/dev/null || {
echo "check-image: docker is required." >&2
exit 1
}
project="$(ci_project_name image)"
modules_directory="/var/lib/ekklesia/modules"
work="$(mktemp -d)"
built_images=()
cleanup() {
local status=$?
trap - EXIT INT TERM
for built in "${built_images[@]}"; do
docker image rm --force "$built" >/dev/null 2>&1 || true
done
rm -rf "$work" || true
exit "$status"
}
trap cleanup EXIT INT TERM
step() {
echo "==> $*"
}
fail() {
echo "check-image: $*" >&2
exit 1
}
# check_contents <image>: every content guarantee, inside the image and in its configuration.
check_contents() {
local image="$1" configured
configured="$(docker image inspect --format '{{range .Config.Env}}{{println .}}{{end}}' "$image")" ||
fail "could not inspect ${image}."
grep -qx "EKKLESIA_MODULES_DIR=${modules_directory}" <<<"$configured" ||
fail "the image does not set EKKLESIA_MODULES_DIR=${modules_directory}."
# As root, so a directory the runtime user cannot read cannot hide a manifest. No network.
docker run --rm --network none --user 0:0 --entrypoint sh "$image" -ec '
fail() { echo "check-image: $*" >&2; exit 1; }
[ ! -e /opt/ekklesia/modules ] || fail "/opt/ekklesia/modules exists in the image."
[ ! -e /opt/ekklesia/templates ] || fail "the module template is in the image."
[ -d /var/lib/ekklesia/modules ] || fail "/var/lib/ekklesia/modules is missing."
[ "$(stat -c %U:%G:%a /var/lib/ekklesia/modules)" = "root:root:755" ] ||
fail "/var/lib/ekklesia/modules is not root-owned with mode 755."
[ -z "$(ls -A /var/lib/ekklesia/modules)" ] || fail "/var/lib/ekklesia/modules is not empty."
manifests="$(find / -xdev -name ekklesia.module.json -not -path "*/node_modules/*" 2>/dev/null)"
[ -z "$manifests" ] || fail "a module manifest is in the image: ${manifests}"
[ "$EKKLESIA_MODULES_DIR" = /var/lib/ekklesia/modules ] ||
fail "a process in the image does not see EKKLESIA_MODULES_DIR."
' || fail "${image} is not the generic platform image."
echo "ok ${image}: no modules, no template, empty root-owned module storage, EKKLESIA_MODULES_DIR set"
}
# runtime_listing <image>: every path the platform and its module storage consist of, with its
# type, mode, and size, in a stable order. Two builds that differ here differ in content.
runtime_listing() {
docker run --rm --network none --user 0:0 --entrypoint sh "$1" -ec \
'find /opt/ekklesia /var/lib/ekklesia -xdev -printf "%y %m %s %p\n" | LC_ALL=C sort'
}
if [[ -n "${EKKLESIA_CHECK_IMAGE:-}" ]]; then
check_contents "$EKKLESIA_CHECK_IMAGE"
exit 0
fi
command -v git >/dev/null || fail "git is required to copy the build context."
# context_copy <directory>: the files Git would track, and nothing it ignores.
context_copy() {
mkdir -p "$1"
git ls-files -z --cached --others --exclude-standard |
tar --null --files-from=- --create --file=- |
tar -C "$1" -xf -
}
tag_base="ekklesia-ci:${GITHUB_SHA:-local}-${GITHUB_RUN_ID:-0}-${GITHUB_RUN_ATTEMPT:-1}-"
generic_image="${tag_base}generic-$$"
populated_image="${tag_base}populated-$$"
builder_image="${tag_base}native-proof-builder-$$"
# build_image <context> <tag>
build_image() {
built_images+=("$2")
DOCKER_BUILDKIT=1 docker build --quiet --file "$1/deploy/Dockerfile" \
--build-arg REVISION="${GITHUB_SHA:-}" \
--label "${EKKLESIA_CI_LABEL}=${project}" --label "${EKKLESIA_CI_CREATED_LABEL}=$(date +%s)" \
--tag "$2" "$1" >/dev/null || fail "the image did not build from ${1##*/}."
}
step "Building the platform image from this checkout"
context_copy "$work/generic"
[[ ! -e "$work/generic/modules" ]] || fail "this checkout itself has a modules/ directory."
build_image "$work/generic" "$generic_image"
check_contents "$generic_image"
step "Building again from the same checkout with a populated modules/ directory"
context_copy "$work/populated"
mkdir -p "$work/populated/modules"
cp -R "$work/populated/templates/module" "$work/populated/modules/example"
printf 'must never reach an image\n' >"$work/populated/modules/example/canary.txt"
build_image "$work/populated" "$populated_image"
check_contents "$populated_image"
step "Comparing the two images' runtime file systems"
runtime_listing "$generic_image" >"$work/generic.listing"
runtime_listing "$populated_image" >"$work/populated.listing"
[[ -s "$work/generic.listing" ]] || fail "could not list the image's file system."
if ! diff -u "$work/generic.listing" "$work/populated.listing" >"$work/listing.diff"; then
head -40 "$work/listing.diff" >&2
fail "a populated modules/ directory changed the image."
fi
echo "ok both builds have the identical runtime file system ($(wc -l <"$work/generic.listing") paths)"
if [[ "$(docker image inspect --format '{{json .RootFS.Layers}}' "$generic_image")" == \
"$(docker image inspect --format '{{json .RootFS.Layers}}' "$populated_image")" ]]; then
echo "ok both builds share every layer"
else
echo "note the two builds' layers differ (a cold build cache); their file systems do not"
fi
step "Proving native package verification with /tmp noexec and a dedicated execution scratch"
built_images+=("$builder_image")
DOCKER_BUILDKIT=1 docker build --quiet --file "$work/generic/deploy/Dockerfile" \
--target build --label "${EKKLESIA_CI_LABEL}=${project}" \
--label "${EKKLESIA_CI_CREATED_LABEL}=$(date +%s)" \
--tag "$builder_image" "$work/generic" >/dev/null || fail "the native proof builder did not build."
mkdir "$work/native-package"
package_archive="$({
docker run --rm --network none --user "$(id -u):$(id -g)" \
-v "$work/native-package:/proof" --entrypoint node "$builder_image" \
packages/module-pack/tests/support/pack-native-verifier-proof.mjs /proof
} | tail -1)" || fail "the real native proof package did not build."
[[ "$package_archive" == native-proof-0.1.0-linux-x64-glibc.tar.gz ]] ||
fail "the native proof builder returned an unexpected archive."
docker run --rm --network none --read-only \
--tmpfs /tmp:rw,nosuid,nodev,noexec,size=64m \
--tmpfs /run/ekklesia-verifier:rw,nosuid,nodev,exec,size=1100m,mode=0700,uid=1000,gid=1000 \
--cap-drop ALL --security-opt no-new-privileges \
-e "PROOF_ARCHIVE=$package_archive" -v "$work/native-package:/package:ro" \
--entrypoint sh "$generic_image" -ec '
fail() { echo "check-image: $*" >&2; exit 1; }
verifier() {
EKKLESIA_MODULE_VERIFIER_SCRATCH_DIR=/run/ekklesia-verifier \
node core/dist/cli/verify-module-package.js "/package/$PROOF_ARCHIVE" \
--sha256-file "/package/$PROOF_ARCHIVE.sha256" "$@"
}
scratch_failure() {
if output="$(EKKLESIA_MODULE_VERIFIER_SCRATCH_DIR="$1" \
node core/dist/cli/verify-module-package.js "/package/$PROOF_ARCHIVE" \
--sha256-file "/package/$PROOF_ARCHIVE.sha256" 2>&1)"; then
fail "the verifier accepted unusable execution scratch $1."
fi
printf "%s\n" "$output" | grep -Fq "[MODULE_PACKAGE_VERIFIER_SCRATCH_UNAVAILABLE]" ||
fail "the verifier did not identify unusable execution scratch $1."
}
grep -Eq " /tmp .*noexec" /proc/mounts || fail "/tmp is not mounted noexec."
verifier --extract-to /tmp/noexec-installed >/tmp/noexec-verify.log
if env -i /usr/local/bin/node --input-type=module --eval \
"await import(\"file:///tmp/noexec-installed/server/index.js\");" \
>/tmp/noexec-load.log 2>&1; then
fail "the native addon unexpectedly loaded from noexec /tmp."
fi
rm -rf /tmp/noexec-installed
scratch_failure /tmp
scratch_failure /missing-verifier-scratch
scratch_failure /package
verifier --extract-to /run/ekklesia-verifier/installed >/tmp/native-verify.log
env -i /usr/local/bin/node --input-type=module --eval \
"const m=await import(\"file:///run/ekklesia-verifier/installed/server/index.js\"); const actual=await m.proof(); const expected={pixels:[255,0,0,255,0,0,255,0,0,255,0,0],png:{format:\"png\",height:3,width:5},resized:{channels:3,height:2,width:2}}; if(JSON.stringify(actual)!==JSON.stringify(expected)) throw new Error(\"sharp proof mismatch\");"
rm -rf /run/ekklesia-verifier/installed
verifier >/tmp/native-verify-a.log 2>&1 & first=$!
verifier >/tmp/native-verify-b.log 2>&1 & second=$!
wait "$first" || { cat /tmp/native-verify-a.log >&2; fail "first concurrent verification failed."; }
wait "$second" || { cat /tmp/native-verify-b.log >&2; fail "second concurrent verification failed."; }
[ -z "$(find /run/ekklesia-verifier -mindepth 1 -print -quit)" ] ||
fail "the verifier left execution scratch behind."
' || fail "the hardened native verifier proof failed."
echo "ok /tmp stayed noexec; native verification, sharp execution, rejection, concurrency, and cleanup passed"
echo "Generic platform image check passed."