mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 10:35:51 +00:00
e93ee38084
Rootful evidence must reject non-running systemd states and prove no unrelated container runtime survives. Require exact manager state, mask distro containerd, recheck readiness after scenarios, and remove the complete Podman socket boundary.
469 lines
22 KiB
Bash
Executable File
469 lines
22 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
readonly REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
# shellcheck source=scripts/secure-runtime-rootful-runtime.sh
|
|
source "${REPO_ROOT}/scripts/secure-runtime-rootful-runtime.sh"
|
|
readonly UBUNTU_IMAGE="${PULSE_ROOTFUL_UBUNTU_IMAGE:?set PULSE_ROOTFUL_UBUNTU_IMAGE to an immutable ubuntu@sha256:... Ubuntu 24.04 image}"
|
|
readonly OUTPUT_PARENT="${PULSE_ROOTFUL_QUALIFICATION_OUTPUT_DIR:?set PULSE_ROOTFUL_QUALIFICATION_OUTPUT_DIR to an existing absolute private directory}"
|
|
readonly CONFIRM="${PULSE_ROOTFUL_QUALIFICATION_CONFIRM:-}"
|
|
readonly CANONICAL_ORIGIN_URL="https://github.com/rcourtman/Pulse.git"
|
|
readonly BOUND_PROBE_PATH="/usr/local/libexec/pulse-rootful-qualification/dockeragent.test"
|
|
|
|
portable_mode() {
|
|
stat -c '%a' "$1" 2>/dev/null || stat -f '%Lp' "$1"
|
|
}
|
|
|
|
portable_uid() {
|
|
stat -c '%u' "$1" 2>/dev/null || stat -f '%u' "$1"
|
|
}
|
|
|
|
if [[ ! "${UBUNTU_IMAGE}" =~ ^ubuntu@sha256:[0-9a-f]{64}$ ]]; then
|
|
echo "ERROR: PULSE_ROOTFUL_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_ROOTFUL_QUALIFICATION_OUTPUT_DIR must be an existing absolute non-symlink directory" >&2
|
|
exit 2
|
|
fi
|
|
if [[ "$(portable_mode "${OUTPUT_PARENT}")" != "700" ]]; then
|
|
echo "ERROR: qualification output directory must have exact mode 0700" >&2
|
|
exit 2
|
|
fi
|
|
if [[ "$(portable_uid "${OUTPUT_PARENT}")" != "$(id -u)" ]]; then
|
|
echo "ERROR: qualification output directory must be owned by the invoking user" >&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)"
|
|
origin_url="$(git -C "${REPO_ROOT}" remote get-url origin)"
|
|
if [[ "${origin_url}" != "${CANONICAL_ORIGIN_URL}" ]]; then
|
|
echo "ERROR: qualification requires the canonical Pulse origin URL" >&2
|
|
exit 2
|
|
fi
|
|
origin_main="$(git -C "${REPO_ROOT}" rev-parse refs/remotes/origin/main)"
|
|
remote_main_record="$(git -C "${REPO_ROOT}" ls-remote --exit-code origin refs/heads/main)"
|
|
if [[ ! "${remote_main_record}" =~ ^([0-9a-f]{40})$'\t'refs/heads/main$ ]]; then
|
|
echo "ERROR: canonical remote main lookup returned an unexpected result" >&2
|
|
exit 2
|
|
fi
|
|
remote_main="${BASH_REMATCH[1]}"
|
|
if [[ "${SOURCE_COMMIT}" != "${origin_main}" || "${SOURCE_COMMIT}" != "${remote_main}" ]]; then
|
|
echo "ERROR: qualification requires HEAD, origin/main, and canonical remote main to be identical" >&2
|
|
exit 2
|
|
fi
|
|
readonly EXPECTED_CONFIRM="I_HAVE_VERIFIED_THESE_ARE_DISPOSABLE_ROOTFUL_SYSTEMD_CONTAINERS_COMMIT_${SOURCE_COMMIT}"
|
|
if [[ "${CONFIRM}" != "${EXPECTED_CONFIRM}" ]]; then
|
|
echo "ERROR: exact destructive opt-in required:" >&2
|
|
echo "export PULSE_ROOTFUL_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-rootful-qualification:${SOURCE_COMMIT:0:12}"
|
|
readonly CONTAINER_RUN_LABEL="org.pulse.rootful-qualification.run"
|
|
readonly CONTAINER_RUN_NONCE="$(openssl rand -hex 16)"
|
|
if docker image inspect "${IMAGE_TAG}" >/dev/null 2>&1; then
|
|
echo "ERROR: qualification image tag already exists; refusing to overwrite ${IMAGE_TAG}" >&2
|
|
exit 2
|
|
fi
|
|
PACKET_DIR="$(mktemp -d /tmp/pulse-rootful-packet.XXXXXX)"
|
|
CONTAINER_IDS=()
|
|
IMAGE_CREATED=false
|
|
|
|
inspect_container_nonce() {
|
|
docker inspect --format '{{ index .Config.Labels "org.pulse.rootful-qualification.run" }}' "$1"
|
|
}
|
|
|
|
remove_container_strict() {
|
|
local container_id="$1"
|
|
local observed_nonce remaining
|
|
observed_nonce="$(inspect_container_nonce "${container_id}")" || {
|
|
echo "ERROR: unable to verify ownership label for qualification container ${container_id}" >&2
|
|
return 1
|
|
}
|
|
if [[ "${observed_nonce}" != "${CONTAINER_RUN_NONCE}" ]]; then
|
|
echo "ERROR: qualification container ${container_id} ownership label changed" >&2
|
|
return 1
|
|
fi
|
|
docker rm -f "${container_id}" >/dev/null || return 1
|
|
remaining="$(docker ps -aq --no-trunc --filter "id=${container_id}")" || return 1
|
|
if printf '%s\n' "${remaining}" | grep -Fxq "${container_id}"; then
|
|
echo "ERROR: qualification container ${container_id} remains after removal" >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
forget_container() {
|
|
local removed_id="$1" candidate
|
|
local retained=()
|
|
for candidate in "${CONTAINER_IDS[@]}"; do
|
|
[[ "${candidate}" == "${removed_id}" ]] || retained+=("${candidate}")
|
|
done
|
|
CONTAINER_IDS=("${retained[@]}")
|
|
}
|
|
|
|
remove_container_best_effort() {
|
|
local container_id="$1" observed_nonce
|
|
observed_nonce="$(inspect_container_nonce "${container_id}" 2>/dev/null)" || {
|
|
echo "WARNING: unable to inspect qualification container ${container_id}; manual cleanup may be required" >&2
|
|
return
|
|
}
|
|
if [[ "${observed_nonce}" != "${CONTAINER_RUN_NONCE}" ]]; then
|
|
echo "WARNING: refusing to remove qualification container after ownership-label mismatch: ${container_id}" >&2
|
|
return
|
|
fi
|
|
docker rm -f "${container_id}" >/dev/null 2>&1 || \
|
|
echo "WARNING: failed to remove qualification container ${container_id}" >&2
|
|
}
|
|
|
|
remove_image_strict() {
|
|
local observed_nonce
|
|
[[ "${IMAGE_CREATED}" == true ]] || return 0
|
|
observed_nonce="$(docker image inspect --format '{{ index .Config.Labels "org.pulse.rootful-qualification.run" }}' "${IMAGE_TAG}")" || return 1
|
|
if [[ "${observed_nonce}" != "${CONTAINER_RUN_NONCE}" ]]; then
|
|
echo "ERROR: qualification image ownership label changed" >&2
|
|
return 1
|
|
fi
|
|
docker image rm "${IMAGE_TAG}" >/dev/null || return 1
|
|
IMAGE_CREATED=false
|
|
}
|
|
|
|
cleanup() {
|
|
local container_id discovered discovered_ids=""
|
|
discovered_ids="$(docker ps -aq --no-trunc --filter "label=${CONTAINER_RUN_LABEL}=${CONTAINER_RUN_NONCE}" 2>/dev/null)" || true
|
|
for container_id in "${CONTAINER_IDS[@]}"; do
|
|
remove_container_best_effort "${container_id}"
|
|
done
|
|
while IFS= read -r discovered; do
|
|
[[ -n "${discovered}" ]] || continue
|
|
if [[ " ${CONTAINER_IDS[*]} " != *" ${discovered} "* ]]; then
|
|
remove_container_best_effort "${discovered}"
|
|
fi
|
|
done <<<"${discovered_ids}"
|
|
if [[ "${IMAGE_CREATED}" == true ]]; then
|
|
remove_image_strict >/dev/null 2>&1 || echo "WARNING: failed to remove qualification image ${IMAGE_TAG}" >&2
|
|
fi
|
|
if [[ "${PACKET_DIR}" == /tmp/pulse-rootful-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
|
|
|
|
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="rootful-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"
|
|
chmod 0700 "${PACKET_DIR}/pulse-agent" "${PACKET_DIR}/pulse-agent-helper" "${PACKET_DIR}/dockeragent.test"
|
|
|
|
verify_vcs_artifact() {
|
|
local artifact="$1" expected_package="$2" metadata
|
|
metadata="$(go version -m "${artifact}")" || {
|
|
echo "ERROR: unable to inspect Go build metadata for ${artifact}" >&2
|
|
return 1
|
|
}
|
|
grep -Fq $'path\t'"${expected_package}" <<<"${metadata}" || {
|
|
echo "ERROR: artifact package identity mismatch for ${artifact}" >&2
|
|
return 1
|
|
}
|
|
grep -Fq "vcs.revision=${SOURCE_COMMIT}" <<<"${metadata}" || {
|
|
echo "ERROR: artifact vcs.revision does not match source commit: ${artifact}" >&2
|
|
return 1
|
|
}
|
|
grep -Fq 'vcs.modified=false' <<<"${metadata}" || {
|
|
echo "ERROR: artifact vcs.modified identity is not clean: ${artifact}" >&2
|
|
return 1
|
|
}
|
|
}
|
|
|
|
verify_vcs_artifact "${PACKET_DIR}/pulse-agent" "github.com/rcourtman/pulse-go-rewrite/cmd/pulse-agent"
|
|
verify_vcs_artifact "${PACKET_DIR}/pulse-agent-helper" "github.com/rcourtman/pulse-go-rewrite/cmd/pulse-agent-helper"
|
|
verify_vcs_artifact "${PACKET_DIR}/dockeragent.test" "github.com/rcourtman/pulse-go-rewrite/scripts/installtests.test"
|
|
|
|
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_rootful_attestation_v1.py"
|
|
spec = importlib.util.spec_from_file_location("rootful_validator", validator_path)
|
|
if spec is None or spec.loader is None:
|
|
raise SystemExit("unable to load rootful 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" "${PACKET_DIR}/pulse-agent.sig"
|
|
|
|
cat >"${PACKET_DIR}/Dockerfile" <<EOF
|
|
FROM ${UBUNTU_IMAGE}
|
|
ENV container=docker DEBIAN_FRONTEND=noninteractive
|
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
bash busybox-static ca-certificates curl dbus gnupg iproute2 jq kmod openssl passwd procps python3 \
|
|
software-properties-common systemd systemd-sysv podman && \
|
|
install -d -m 0755 /etc/apt/keyrings && \
|
|
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc && \
|
|
chmod 0644 /etc/apt/keyrings/docker.asc && \
|
|
printf 'deb [arch=%s signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu noble stable\n' "\$(dpkg --print-architecture)" >/etc/apt/sources.list.d/docker.list && \
|
|
apt-get update && apt-get install -y --no-install-recommends docker-ce docker-ce-cli containerd.io && \
|
|
apt-get clean && rm -rf /var/lib/apt/lists/* && \
|
|
for unit in \
|
|
docker.service docker.socket \
|
|
containerd.service \
|
|
podman.service podman.socket \
|
|
podman-auto-update.service podman-auto-update.timer \
|
|
podman-clean-transient.service podman-restart.service; do \
|
|
ln -sf /dev/null "/etc/systemd/system/\${unit}"; \
|
|
done && \
|
|
install -d -m 0700 /opt/pulse/packet /opt/pulse/result && \
|
|
printf '%s\n' disposable-v1 >/etc/pulse-secure-runtime-rootful-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 \
|
|
--label "${CONTAINER_RUN_LABEL}=${CONTAINER_RUN_NONCE}" \
|
|
-t "${IMAGE_TAG}" -f "${PACKET_DIR}/Dockerfile" "${PACKET_DIR}" | tee "${OUTPUT_DIR}/image-build.log"
|
|
IMAGE_CREATED=true
|
|
docker image inspect "${IMAGE_TAG}" >"${OUTPUT_DIR}/qualification-image-inspect.json"
|
|
chmod 0600 "${OUTPUT_DIR}/image-build.log" "${OUTPUT_DIR}/qualification-image-inspect.json"
|
|
|
|
capture_qualification_container_diagnostics() {
|
|
local runtime_name="$1" 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
|
|
docker exec "${container_id}" systemctl is-system-running >"${OUTPUT_DIR}/${runtime_name}-systemd-state.log" 2>&1 || true
|
|
docker exec "${container_id}" systemctl list-units --state=failed --no-legend --no-pager --plain >"${OUTPUT_DIR}/${runtime_name}-failed-units.log" 2>&1 || true
|
|
chmod 0600 \
|
|
"${OUTPUT_DIR}/${runtime_name}-container.log" \
|
|
"${OUTPUT_DIR}/${runtime_name}-journal.log" \
|
|
"${OUTPUT_DIR}/${runtime_name}-systemd-state.log" \
|
|
"${OUTPUT_DIR}/${runtime_name}-failed-units.log"
|
|
}
|
|
|
|
run_runtime() {
|
|
local runtime_name="$1"
|
|
local container_name="pulse-rootful-qual-${runtime_name}-${SOURCE_COMMIT:0:8}-$$"
|
|
local container_id local_receipt machine_id_file machine_id deadline mounts packet_probe_hash installed_probe_hash readiness_status
|
|
local_receipt="${OUTPUT_DIR}/${runtime_name}-receipt.json"
|
|
machine_id_file="${PACKET_DIR}/.machine-id-${runtime_name}"
|
|
machine_id="$(openssl rand -hex 16)"
|
|
if [[ ! "${machine_id}" =~ ^[0-9a-f]{32}$ || "${machine_id}" == "00000000000000000000000000000000" ]]; then
|
|
echo "ERROR: unable to generate a valid machine ID for ${runtime_name}" >&2
|
|
return 1
|
|
fi
|
|
printf '%s\n' "${machine_id}" >"${machine_id_file}"
|
|
chmod 0444 "${machine_id_file}"
|
|
|
|
container_id="$(docker create --name "${container_name}" --hostname "pulse-rootful-${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 "${machine_id_file}" "${container_id}:/etc/machine-id"
|
|
rm -f -- "${machine_id_file}"
|
|
docker cp "${PACKET_DIR}/." "${container_id}:/opt/pulse/packet"
|
|
docker start "${container_id}" >/dev/null
|
|
|
|
deadline=$((SECONDS + 60))
|
|
while true; do
|
|
readiness_status=0
|
|
rootful_qualification_systemd_readiness "${container_id}" || readiness_status=$?
|
|
if (( readiness_status == 0 )); then
|
|
break
|
|
fi
|
|
if (( readiness_status == 2 )); then
|
|
capture_qualification_container_diagnostics "${runtime_name}" "${container_id}"
|
|
echo "ERROR: ${runtime_name} disposable systemd container entered a terminal non-running state or has failed units" >&2
|
|
return 1
|
|
fi
|
|
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
|
|
docker exec "${container_id}" install -d -o root -g root -m 0755 "$(dirname "${BOUND_PROBE_PATH}")"
|
|
docker exec "${container_id}" install -o root -g root -m 0755 /opt/pulse/packet/dockeragent.test "${BOUND_PROBE_PATH}"
|
|
packet_probe_hash="$(sha256_files "${PACKET_DIR}/dockeragent.test" | awk '{print $1}')"
|
|
installed_probe_hash="$(docker exec "${container_id}" sha256sum "${BOUND_PROBE_PATH}" | awk '{print $1}')"
|
|
if [[ ! "${packet_probe_hash}" =~ ^[0-9a-f]{64}$ || "${installed_probe_hash}" != "${packet_probe_hash}" ]]; then
|
|
echo "ERROR: ${runtime_name} collector-executable bound probe differs from the qualification binary" >&2
|
|
return 1
|
|
fi
|
|
if docker exec "${container_id}" sh -c 'ip route | grep -q "^default "'; then
|
|
echo "ERROR: ${runtime_name} qualification container unexpectedly has a default route" >&2
|
|
return 1
|
|
fi
|
|
mounts="$(docker inspect "${container_id}" --format '{{range .Mounts}}{{println .Source "->" .Destination}}{{end}}')"
|
|
if grep -E '/(var/)?run/(docker|podman)(\.sock)?' <<<"${mounts}"; then
|
|
echo "ERROR: host runtime socket was mounted into ${runtime_name} qualification container" >&2
|
|
return 1
|
|
fi
|
|
|
|
if ! docker exec \
|
|
-e PULSE_SECURE_RUNTIME_ROOTFUL_QUALIFICATION=disposable-v1 \
|
|
-e "PULSE_ROOTFUL_RUNTIME=${runtime_name}" \
|
|
-e PULSE_ROOTFUL_RECEIPT=/opt/pulse/result/rootful-receipt.json \
|
|
-e PULSE_ROOTFUL_SOURCE_HASHES=/opt/pulse/packet/source-hashes.json \
|
|
-e "PULSE_ROOTFUL_SOURCE_COMMIT=${SOURCE_COMMIT}" \
|
|
-e "PULSE_ROOTFUL_UBUNTU_IMAGE=${UBUNTU_IMAGE}" \
|
|
-e "PULSE_ROOTFUL_BOUND_PROBE_BINARY=${BOUND_PROBE_PATH}" \
|
|
-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 '^TestSecureRuntimeRootfulQualification$' -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
|
|
readiness_status=0
|
|
rootful_qualification_systemd_readiness "${container_id}" || readiness_status=$?
|
|
if (( readiness_status != 0 )); then
|
|
capture_qualification_container_diagnostics "${runtime_name}" "${container_id}"
|
|
chmod 0600 "${OUTPUT_DIR}/${runtime_name}-test.log"
|
|
echo "ERROR: ${runtime_name} systemd readiness changed before receipt acceptance" >&2
|
|
return 1
|
|
fi
|
|
docker exec "${container_id}" test -f /opt/pulse/result/rootful-receipt.json || {
|
|
capture_qualification_container_diagnostics "${runtime_name}" "${container_id}"
|
|
echo "ERROR: ${runtime_name} qualification did not retain its receipt" >&2
|
|
return 1
|
|
}
|
|
docker cp "${container_id}:/opt/pulse/result/rootful-receipt.json" "${local_receipt}"
|
|
capture_qualification_container_diagnostics "${runtime_name}" "${container_id}"
|
|
chmod 0600 "${local_receipt}" "${OUTPUT_DIR}/${runtime_name}-test.log"
|
|
remove_container_strict "${container_id}"
|
|
forget_container "${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", "base_image", "source_hashes", "artifacts"):
|
|
if docker.get(field) != podman.get(field):
|
|
raise SystemExit(f"per-runtime qualification field differs: {field}")
|
|
runs = docker.get("runs", []) + podman.get("runs", [])
|
|
if [run.get("runtime", {}).get("runtime") for run in runs] != ["docker", "podman"]:
|
|
raise SystemExit("per-runtime receipts are not exact Docker then Podman runs")
|
|
machine_ids = [run.get("host", {}).get("machine_id") for run in runs]
|
|
daemon_ids = [run.get("runtime", {}).get("daemon_id") for run in runs]
|
|
if len(set(machine_ids)) != 2 or len(set(daemon_ids)) != 2:
|
|
raise SystemExit("Docker and Podman qualification hosts/daemons must have distinct identities")
|
|
combined = {
|
|
"schema_version": docker["schema_version"], "kind": docker["kind"], "result": "passed",
|
|
"source_commit": docker["source_commit"],
|
|
"base_image": docker["base_image"],
|
|
"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": 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_rootful_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"
|
|
|
|
if [[ -n "$(docker ps -aq --no-trunc --filter "label=${CONTAINER_RUN_LABEL}=${CONTAINER_RUN_NONCE}")" ]]; then
|
|
echo "ERROR: labeled qualification containers remain after strict cleanup" >&2
|
|
exit 1
|
|
fi
|
|
remove_image_strict
|
|
if [[ -n "$(docker images -q --filter "label=${CONTAINER_RUN_LABEL}=${CONTAINER_RUN_NONCE}")" ]]; then
|
|
echo "ERROR: labeled qualification images remain after strict cleanup" >&2
|
|
exit 1
|
|
fi
|
|
while IFS= read -r retained; do
|
|
[[ -z "${retained}" ]] && continue
|
|
if [[ "$(portable_uid "${retained}")" != "$(id -u)" ]]; then
|
|
echo "ERROR: retained output is not owned by the invoking user: ${retained}" >&2
|
|
exit 1
|
|
fi
|
|
chmod go-rwx "${retained}"
|
|
done < <(find "${OUTPUT_DIR}" -mindepth 1 -maxdepth 1 -type f -print)
|
|
|
|
echo "Rootful qualification passed: ${OUTPUT_DIR}"
|