#!/usr/bin/env bash set -euo pipefail readonly REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" readonly UBUNTU_IMAGE="${PULSE_ROOTLESS_UBUNTU_IMAGE:?set PULSE_ROOTLESS_UBUNTU_IMAGE to an immutable ubuntu@sha256:... Ubuntu 24.04 image}" readonly OUTPUT_PARENT="${PULSE_ROOTLESS_QUALIFICATION_OUTPUT_DIR:?set PULSE_ROOTLESS_QUALIFICATION_OUTPUT_DIR to an existing absolute private directory}" readonly CONFIRM="${PULSE_ROOTLESS_QUALIFICATION_CONFIRM:-}" if [[ ! "${UBUNTU_IMAGE}" =~ ^ubuntu@sha256:[0-9a-f]{64}$ ]]; then echo "ERROR: PULSE_ROOTLESS_UBUNTU_IMAGE must be an exact ubuntu@sha256 digest" >&2 exit 2 fi if [[ "${OUTPUT_PARENT}" != /* || ! -d "${OUTPUT_PARENT}" || -L "${OUTPUT_PARENT}" ]]; then echo "ERROR: PULSE_ROOTLESS_QUALIFICATION_OUTPUT_DIR must be an existing absolute non-symlink directory" >&2 exit 2 fi if [[ "$(stat -c '%a' "${OUTPUT_PARENT}" 2>/dev/null || stat -f '%Lp' "${OUTPUT_PARENT}")" != "700" ]]; then echo "ERROR: qualification output directory must have exact mode 0700" >&2 exit 2 fi if ! command -v docker >/dev/null 2>&1 || ! docker info >/dev/null 2>&1; then echo "ERROR: a working Docker CLI/daemon is required to create disposable qualification containers" >&2 exit 2 fi if [[ "$(git -C "${REPO_ROOT}" branch --show-current)" != "main" ]]; then echo "ERROR: qualification builds are allowed only from main" >&2 exit 2 fi if [[ -n "$(git -C "${REPO_ROOT}" status --porcelain)" ]]; then echo "ERROR: qualification requires a clean exact source checkout" >&2 exit 2 fi readonly SOURCE_COMMIT="$(git -C "${REPO_ROOT}" rev-parse HEAD)" readonly EXPECTED_CONFIRM="I_HAVE_VERIFIED_THESE_ARE_DISPOSABLE_ROOTLESS_SYSTEMD_CONTAINERS_COMMIT_${SOURCE_COMMIT}" if [[ "${CONFIRM}" != "${EXPECTED_CONFIRM}" ]]; then echo "ERROR: exact destructive opt-in required:" >&2 echo "export PULSE_ROOTLESS_QUALIFICATION_CONFIRM=${EXPECTED_CONFIRM}" >&2 exit 2 fi readonly RUN_STAMP="$(date -u +%Y%m%dT%H%M%SZ)" readonly OUTPUT_DIR="${OUTPUT_PARENT}/${RUN_STAMP}-${SOURCE_COMMIT:0:12}" readonly IMAGE_TAG="pulse-rootless-qualification:${SOURCE_COMMIT:0:12}" readonly CONTAINER_RUN_LABEL="org.pulse.rootless-qualification.run" readonly CONTAINER_RUN_NONCE="$(openssl rand -hex 16)" PACKET_DIR="$(mktemp -d /tmp/pulse-rootless-packet.XXXXXX)" CONTAINER_IDS=() inspect_qualification_container_nonce() { local container_id="$1" docker inspect --format '{{ index .Config.Labels "org.pulse.rootless-qualification.run" }}' "${container_id}" } remove_qualification_container_strict() { local container_id="$1" local observed_nonce local remaining if ! observed_nonce="$(inspect_qualification_container_nonce "${container_id}")"; then echo "ERROR: unable to verify ownership label for qualification container ${container_id}" >&2 return 1 fi if [[ "${observed_nonce}" != "${CONTAINER_RUN_NONCE}" ]]; then echo "ERROR: qualification container ${container_id} ownership label changed" >&2 return 1 fi if ! docker rm -f "${container_id}" >/dev/null; then echo "ERROR: failed to remove qualification container ${container_id}" >&2 return 1 fi if ! remaining="$(docker ps -aq --no-trunc --filter "id=${container_id}")"; then echo "ERROR: unable to verify removal of qualification container ${container_id}" >&2 return 1 fi if printf '%s\n' "${remaining}" | grep -Fxq "${container_id}"; then echo "ERROR: qualification container ${container_id} remains after removal" >&2 return 1 fi } remove_qualification_container_best_effort() { local container_id="$1" local observed_nonce if ! observed_nonce="$(inspect_qualification_container_nonce "${container_id}" 2>/dev/null)"; then echo "WARNING: unable to inspect qualification container ${container_id}; manual cleanup may be required" >&2 return fi if [[ "${observed_nonce}" != "${CONTAINER_RUN_NONCE}" ]]; then echo "WARNING: refusing to remove qualification container ${container_id} after ownership label mismatch" >&2 return fi if ! docker rm -f "${container_id}" >/dev/null 2>&1; then echo "WARNING: failed to remove qualification container ${container_id}; manual cleanup is required" >&2 fi } cleanup() { local container_id local discovered local discovered_ids="" if ! discovered_ids="$(docker ps -aq --no-trunc --filter "label=${CONTAINER_RUN_LABEL}=${CONTAINER_RUN_NONCE}" 2>/dev/null)"; then echo "WARNING: unable to discover qualification containers for final cleanup" >&2 fi for container_id in "${CONTAINER_IDS[@]}"; do remove_qualification_container_best_effort "${container_id}" done while IFS= read -r discovered; do [[ -n "${discovered}" ]] || continue if [[ " ${CONTAINER_IDS[*]} " != *" ${discovered} "* ]]; then remove_qualification_container_best_effort "${discovered}" fi done <<<"${discovered_ids}" if [[ "${PACKET_DIR}" == /tmp/pulse-rootless-packet.* && -d "${PACKET_DIR}" ]]; then find "${PACKET_DIR}" -type f -exec chmod u+w {} + 2>/dev/null || true rm -rf -- "${PACKET_DIR}" fi } trap cleanup EXIT INT TERM sha256_files() { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@" else shasum -a 256 "$@" fi } mkdir "${OUTPUT_DIR}" chmod 0700 "${OUTPUT_DIR}" openssl genpkey -algorithm ED25519 -out "${PACKET_DIR}/update-private.pem" chmod 0600 "${PACKET_DIR}/update-private.pem" openssl pkey -in "${PACKET_DIR}/update-private.pem" -pubout -outform DER -out "${PACKET_DIR}/update-public.der" chmod 0600 "${PACKET_DIR}/update-public.der" update_public_key="$(python3 -I - "${PACKET_DIR}/update-public.der" <<'PY' import base64 import pathlib import sys # Ed25519 SubjectPublicKeyInfo is an exact 12-byte algorithm/bit-string prefix # followed by the raw 32-byte public key required by the release ldflags. spki = pathlib.Path(sys.argv[1]).read_bytes() prefix = bytes.fromhex("302a300506032b6570032100") if len(spki) != len(prefix) + 32 or not spki.startswith(prefix): raise SystemExit("unexpected Ed25519 SubjectPublicKeyInfo encoding") print(base64.b64encode(spki[len(prefix):]).decode("ascii"), end="") PY )" qualification_version="rootless-v1.${SOURCE_COMMIT:0:12}" agent_ldflags="$(cd "${REPO_ROOT}" && ./scripts/release_ldflags.sh agent --version "${qualification_version}" --update-public-keys "${update_public_key}")" helper_ldflags="$(cd "${REPO_ROOT}" && ./scripts/release_ldflags.sh agent --version "${qualification_version}" --update-public-keys "${update_public_key}")" ( cd "${REPO_ROOT}" CGO_ENABLED=0 GOOS=linux GOARCH="$(go env GOARCH)" GOFLAGS= GOWORK=off go build -trimpath -buildvcs=true -ldflags "${agent_ldflags}" -o "${PACKET_DIR}/pulse-agent" ./cmd/pulse-agent CGO_ENABLED=0 GOOS=linux GOARCH="$(go env GOARCH)" GOFLAGS= GOWORK=off go build -trimpath -buildvcs=true -ldflags "${helper_ldflags}" -o "${PACKET_DIR}/pulse-agent-helper" ./cmd/pulse-agent-helper CGO_ENABLED=0 GOOS=linux GOARCH="$(go env GOARCH)" GOFLAGS= GOWORK=off go test -c -trimpath -buildvcs=true -o "${PACKET_DIR}/dockeragent.test" ./scripts/installtests ) openssl pkeyutl -sign -rawin -inkey "${PACKET_DIR}/update-private.pem" -in "${PACKET_DIR}/pulse-agent" | openssl base64 -A >"${PACKET_DIR}/pulse-agent.sig" printf '\n' >>"${PACKET_DIR}/pulse-agent.sig" rm -f "${PACKET_DIR}/update-private.pem" "${PACKET_DIR}/update-public.der" unset update_public_key agent_ldflags helper_ldflags install -m 0700 "${REPO_ROOT}/scripts/install.sh" "${PACKET_DIR}/install.sh" python3 -I - "${REPO_ROOT}" "${PACKET_DIR}/source-hashes.json" <<'PY' import importlib.util import json import pathlib import sys checkout = pathlib.Path(sys.argv[1]).resolve(strict=True) destination = pathlib.Path(sys.argv[2]) validator_path = checkout / "scripts/release_control/secure_runtime_rootless_attestation_v1.py" spec = importlib.util.spec_from_file_location("rootless_validator", validator_path) if spec is None or spec.loader is None: raise SystemExit("unable to load rootless receipt validator") module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) _, hashes = module.load_source_manifest(checkout, checkout / module.SOURCE_MANIFEST_PATH) destination.write_text(json.dumps(hashes, sort_keys=True, separators=(",", ":")) + "\n") PY chmod 0600 "${PACKET_DIR}/source-hashes.json" cat >"${PACKET_DIR}/Dockerfile" </etc/apt/sources.list.d/docker.list && \ apt-get update && apt-get install -y --no-install-recommends docker-ce docker-ce-cli docker-ce-rootless-extras containerd.io && \ apt-get clean && rm -rf /var/lib/apt/lists/* && \ ln -sf /dev/null /etc/systemd/system/docker.service && \ ln -sf /dev/null /etc/systemd/system/docker.socket && \ ln -sf /dev/null /etc/systemd/system/podman.service && \ ln -sf /dev/null /etc/systemd/system/podman.socket && \ install -d -m 0700 /opt/pulse/packet && \ printf '%s\n' disposable-v1 >/etc/pulse-secure-runtime-rootless-qualification && \ rm -f /etc/machine-id && touch /etc/machine-id && \ systemctl set-default multi-user.target STOPSIGNAL SIGRTMIN+3 CMD ["/sbin/init"] EOF docker build --pull --no-cache --network default -t "${IMAGE_TAG}" -f "${PACKET_DIR}/Dockerfile" "${PACKET_DIR}" | tee "${OUTPUT_DIR}/image-build.log" docker image inspect "${IMAGE_TAG}" >"${OUTPUT_DIR}/qualification-image-inspect.json" capture_qualification_container_diagnostics() { local runtime_name="$1" local container_id="$2" docker logs "${container_id}" >"${OUTPUT_DIR}/${runtime_name}-container.log" 2>&1 || true docker exec "${container_id}" journalctl --no-pager -n 2000 >"${OUTPUT_DIR}/${runtime_name}-journal.log" 2>&1 || true chmod 0600 "${OUTPUT_DIR}/${runtime_name}-container.log" "${OUTPUT_DIR}/${runtime_name}-journal.log" } run_runtime() { local runtime_name="$1" local container_name="pulse-rootless-qual-${runtime_name}-${SOURCE_COMMIT:0:8}-$$" local container_id local local_receipt="${OUTPUT_DIR}/${runtime_name}-receipt.json" container_id="$(docker create --name "${container_name}" --hostname "pulse-rootless-${runtime_name}" \ --label "${CONTAINER_RUN_LABEL}=${CONTAINER_RUN_NONCE}" \ --privileged --network none --cgroupns=private \ --tmpfs /run:rw,nosuid,nodev,mode=755 --tmpfs /run/lock:rw,nosuid,nodev,mode=755 \ "${IMAGE_TAG}")" CONTAINER_IDS+=("${container_id}") docker cp "${PACKET_DIR}/." "${container_id}:/opt/pulse/packet" docker start "${container_id}" >/dev/null local deadline=$((SECONDS + 60)) until docker exec "${container_id}" systemctl is-system-running --wait >/dev/null 2>&1; do if (( SECONDS >= deadline )); then capture_qualification_container_diagnostics "${runtime_name}" "${container_id}" echo "ERROR: ${runtime_name} disposable systemd container did not become ready" >&2 return 1 fi sleep 1 done if docker exec "${container_id}" sh -c 'ip route | grep -q "^default "'; then echo "ERROR: ${runtime_name} qualification container unexpectedly has an external default route" >&2 return 1 fi if docker inspect "${container_id}" --format '{{range .Mounts}}{{println .Source "->" .Destination}}{{end}}' | grep -E '/(var/)?run/(docker|podman)(\.sock)?'; then echo "ERROR: host runtime socket was mounted into ${runtime_name} qualification container" >&2 return 1 fi if ! docker exec \ -e PULSE_SECURE_RUNTIME_ROOTLESS_QUALIFICATION=disposable-v1 \ -e "PULSE_ROOTLESS_RUNTIME=${runtime_name}" \ -e PULSE_ROOTLESS_RECEIPT=/run/rootless-receipt.json \ -e PULSE_ROOTLESS_SOURCE_HASHES=/opt/pulse/packet/source-hashes.json \ -e "PULSE_ROOTLESS_SOURCE_COMMIT=${SOURCE_COMMIT}" \ -e PULSE_SECURE_RUNTIME_COLLECTOR=/opt/pulse/packet/pulse-agent \ -e PULSE_SECURE_RUNTIME_COLLECTOR_SIGNATURE=/opt/pulse/packet/pulse-agent.sig \ -e PULSE_SECURE_RUNTIME_HELPER=/opt/pulse/packet/pulse-agent-helper \ -e PULSE_SECURE_RUNTIME_INSTALLER=/opt/pulse/packet/install.sh \ "${container_id}" /opt/pulse/packet/dockeragent.test \ -test.run '^TestSecureRuntimeRootlessQualification$' -test.count=1 -test.v -test.timeout=45m \ | tee "${OUTPUT_DIR}/${runtime_name}-test.log"; then capture_qualification_container_diagnostics "${runtime_name}" "${container_id}" chmod 0600 "${OUTPUT_DIR}/${runtime_name}-test.log" return 1 fi docker cp "${container_id}:/run/rootless-receipt.json" "${local_receipt}" capture_qualification_container_diagnostics "${runtime_name}" "${container_id}" chmod 0600 "${local_receipt}" "${OUTPUT_DIR}/${runtime_name}-test.log" remove_qualification_container_strict "${container_id}" } run_runtime docker run_runtime podman python3 -I - "${OUTPUT_DIR}/docker-receipt.json" "${OUTPUT_DIR}/podman-receipt.json" "${OUTPUT_DIR}/receipt.json" <<'PY' import json import pathlib import sys docker_path, podman_path, output_path = map(pathlib.Path, sys.argv[1:]) docker = json.loads(docker_path.read_text()) podman = json.loads(podman_path.read_text()) if docker.get("result") != "passed" or podman.get("result") != "passed": raise SystemExit("per-runtime qualification result != \"passed\"") for field in ("schema_version", "kind", "source_commit", "source_hashes", "artifacts"): if docker.get(field) != podman.get(field): raise SystemExit(f"per-runtime qualification field differs: {field}") if [run.get("runtime", {}).get("runtime") for run in docker.get("runs", []) + podman.get("runs", [])] != ["docker", "podman"]: raise SystemExit("per-runtime receipts are not exact Docker then Podman runs") combined = { "schema_version": docker["schema_version"], "kind": docker["kind"], "result": "passed", "source_commit": docker["source_commit"], "started_at": min(docker["started_at"], podman["started_at"]), "completed_at": max(docker["completed_at"], podman["completed_at"]), "source_hashes": docker["source_hashes"], "artifacts": docker["artifacts"], "runs": docker["runs"] + podman["runs"], } output_path.write_text(json.dumps(combined, indent=2, sort_keys=True) + "\n") PY chmod 0600 "${OUTPUT_DIR}/receipt.json" python3 -I "${REPO_ROOT}/scripts/release_control/secure_runtime_rootless_attestation_v1.py" \ "${OUTPUT_DIR}/receipt.json" \ --qualification-test "${PACKET_DIR}/dockeragent.test" \ --collector "${PACKET_DIR}/pulse-agent" \ --helper "${PACKET_DIR}/pulse-agent-helper" \ --installer "${PACKET_DIR}/install.sh" \ >"${OUTPUT_DIR}/attestation.json" chmod 0600 "${OUTPUT_DIR}/attestation.json" sha256_files "${OUTPUT_DIR}/receipt.json" "${OUTPUT_DIR}/attestation.json" >"${OUTPUT_DIR}/sha256.txt" chmod 0600 "${OUTPUT_DIR}/sha256.txt" echo "Rootless qualification passed: ${OUTPUT_DIR}"