diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..870756d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,137 @@ +name: Release + +on: + push: + tags: + - 'v*.*.*' + +permissions: + contents: write + packages: write + id-token: write + attestations: write + +concurrency: + group: pod-pause-image-release-${{ github.ref }} + cancel-in-progress: false + +jobs: + release: + runs-on: ubuntu-24.04 + timeout-minutes: 60 + env: + TARGET_REPOSITORY: ghcr.io/pasturestack/pod-pause-image + TRIVY_IMAGE: aquasec/trivy:0.74.0@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969 + steps: + - name: Check out immutable tag + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Validate release identity + shell: bash + run: | + set -euo pipefail + test "$GITHUB_REF_TYPE" = tag + [[ "$GITHUB_REF_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] + test "$(git cat-file -t "refs/tags/$GITHUB_REF_NAME")" = tag + test "$(git rev-list -n 1 "$GITHUB_REF_NAME")" = "$GITHUB_SHA" + test "$(sed -n 's/^ARG IMAGE_VERSION=//p' Dockerfile)" = "$GITHUB_REF_NAME" + test -z "$(git status --porcelain)" + bash scripts/validate + git diff --check + + - name: Build and smoke-test exact runtime + shell: bash + run: | + set -euo pipefail + image="$TARGET_REPOSITORY:$GITHUB_REF_NAME" + docker build --pull \ + --build-arg "IMAGE_VERSION=$GITHUB_REF_NAME" \ + --build-arg "SOURCE_REVISION=$GITHUB_SHA" \ + --tag "$image" . + test "$(docker image inspect --format '{{.Config.User}}' "$image")" = 65532:65532 + test "$(docker image inspect --format '{{index .Config.Labels "org.opencontainers.image.version"}}' "$image")" = "$GITHUB_REF_NAME" + test "$(docker image inspect --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' "$image")" = "$GITHUB_SHA" + for signal_name in TERM INT; do + container="$(docker run --detach "$image")" + trap 'docker rm -f "$container" >/dev/null 2>&1 || true' EXIT + test "$(docker inspect --format '{{.State.Running}}' "$container")" = true + docker kill --signal "$signal_name" "$container" >/dev/null + test "$(docker wait "$container")" = 0 + docker rm "$container" + trap - EXIT + done + + - name: Scan runtime and generate SBOM + shell: bash + run: | + set -euo pipefail + image="$TARGET_REPOSITORY:$GITHUB_REF_NAME" + mkdir -p evidence "$RUNNER_TEMP/trivy-cache" + docker pull "$TRIVY_IMAGE" >/dev/null + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v "$PWD/evidence:/evidence" \ + -v "$RUNNER_TEMP/trivy-cache:/root/.cache/trivy" \ + "$TRIVY_IMAGE" image --scanners vuln,secret \ + --severity CRITICAL,HIGH --exit-code 1 \ + --format json --output /evidence/runtime-security.json "$image" + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v "$PWD/evidence:/evidence" \ + -v "$RUNNER_TEMP/trivy-cache:/root/.cache/trivy" \ + "$TRIVY_IMAGE" image --format cyclonedx \ + --output /evidence/runtime.cdx.json "$image" + jq -e '.bomFormat == "CycloneDX" and (.components | length > 0)' \ + evidence/runtime.cdx.json >/dev/null + sha256sum evidence/runtime-security.json evidence/runtime.cdx.json \ + > evidence/SHA256SUMS + + - name: Publish immutable runtime image + id: publish + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + image="$TARGET_REPOSITORY:$GITHUB_REF_NAME" + printf '%s' "$GH_TOKEN" | docker login ghcr.io -u "$GITHUB_ACTOR" --password-stdin + docker push "$image" + reference="$(docker image inspect --format '{{index .RepoDigests 0}}' "$image")" + digest="${reference#*@}" + [[ "$digest" =~ ^sha256:[0-9a-f]{64}$ ]] + printf 'digest=%s\n' "$digest" >> "$GITHUB_OUTPUT" + + - name: Attest release evidence + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-checksums: evidence/SHA256SUMS + + - name: Attest runtime image + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-name: ${{ env.TARGET_REPOSITORY }} + subject-digest: ${{ steps.publish.outputs.digest }} + push-to-registry: true + + - name: Publish GitHub release + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + gh release create "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --verify-tag \ + --title "Pod Pause Image $GITHUB_REF_NAME" \ + --notes 'Pure numeric PastureStack pod-infrastructure image release with signal smoke tests, vulnerability scan, SBOM, checksums, and provenance.' \ + evidence/runtime-security.json evidence/runtime.cdx.json evidence/SHA256SUMS + + - name: Clean run-owned resources + if: always() + shell: bash + run: | + set +e + docker logout ghcr.io >/dev/null 2>&1 + docker image rm -f "$TARGET_REPOSITORY:$GITHUB_REF_NAME" "$TRIVY_IMAGE" >/dev/null 2>&1 + rm -rf -- evidence "$RUNNER_TEMP/trivy-cache" diff --git a/.github/workflows/security-release-gate.yml b/.github/workflows/security-release-gate.yml index 5842625..0155334 100644 --- a/.github/workflows/security-release-gate.yml +++ b/.github/workflows/security-release-gate.yml @@ -65,7 +65,8 @@ jobs: test -s evidence/manifests/builder-ubuntu-packages.tsv test -s evidence/manifests/builder-toolchain.tsv test -s evidence/manifests/runtime-ubuntu-packages.tsv - grep -F $'metadata\tubuntu_snapshot\t-\t20260825T000000Z' \ + . ./ubuntu-apt.lock + grep -F $'metadata\tubuntu_snapshot\t-\t'"${UBUNTU_APT_LOCKED_SNAPSHOT}" \ evidence/manifests/builder-ubuntu-packages.tsv >/dev/null grep -F $'gcc\t4:15.2.0-5ubuntu1\t15.2.0' \ evidence/manifests/builder-toolchain.tsv >/dev/null diff --git a/Dockerfile b/Dockerfile index eadeb38..5adf550 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ ARG UBUNTU_IMAGE=ubuntu:26.04@sha256:2260313b31c8c011cd2eebe728008efac1b3982be73 FROM ${UBUNTU_IMAGE} AS snapshot-ca-bootstrap ADD --checksum=sha256:6077d27c6b6f8b23590cb01ff877ed8c804a67a5442cc32b5a33da10d2bd0e90 \ - https://snapshot.ubuntu.com/ubuntu/20260825T000000Z/pool/main/c/ca-certificates/ca-certificates_20260601~26.04.1_all.deb \ + https://snapshot.ubuntu.com/ubuntu/20260909T000000Z/pool/main/c/ca-certificates/ca-certificates_20260601~26.04.1_all.deb \ /tmp/ca-certificates.deb RUN set -eux; \ @@ -18,7 +18,7 @@ RUN set -eux; \ FROM ${UBUNTU_IMAGE} AS build -ARG UBUNTU_APT_SNAPSHOT=20260825T000000Z +ARG UBUNTU_APT_SNAPSHOT=20260909T000000Z ENV DEBIAN_FRONTEND=noninteractive diff --git a/README.md b/README.md index f3a5816..8e4b49e 100644 --- a/README.md +++ b/README.md @@ -6,15 +6,18 @@ PastureStack is an independent community effort to preserve, audit, and moderniz **Origin:** This is an independent Ubuntu 26.04 compatibility implementation. No public upstream repository could be verified, so it is intentionally not represented as a GitHub fork. -## Release image +## Maintained image -The reviewed `linux/amd64` release used by the catalog is: +The reviewed `linux/amd64` runtime uses the pure numeric coordinate: ```text ghcr.io/pasturestack/pod-pause-image:v3.0.2 ``` -The catalog uses this semantic version tag. Release evidence records the immutable digest separately so a long digest never appears in the user interface. +Product identity and provenance are carried by the package name, OCI labels, +SBOM, and attestations rather than a text qualifier in the tag. Existing +Catalog revisions remain immutable; maintained Catalog revisions use this +numeric successor after its release and integration gates pass. ## Build @@ -22,7 +25,7 @@ The catalog uses this semantic version tag. Release evidence records the immutab docker build --pull \ --build-arg IMAGE_VERSION=v3.0.2 \ --build-arg SOURCE_REVISION="$(git rev-parse HEAD)" \ - -t ghcr.io/pasturestack/pod-pause-image:v3.0.2 . + -t local/pasturestack/pod-pause-image:v3.0.2 . ``` The runtime base is pinned to the reviewed Ubuntu 26.04 `linux/amd64` manifest. The build stage uses the HTTPS Ubuntu snapshot and exact direct-package versions recorded in [`ubuntu-apt.lock`](ubuntu-apt.lock). The final image carries the resolved builder toolchain, builder package, and runtime package manifests under `/usr/share/pasturestack/manifests/`; CI also records builder and runtime image inspections, CycloneDX SBOMs, vulnerability reports, and signal-handling smoke tests as a 30-day review artifact. Runtime High and Critical findings are rejected. The non-shipping builder additionally rejects every High or Critical finding except Ubuntu `linux-libc-dev` kernel-header records for which the vendor has not published a fixed package; those records remain explicit review evidence and become blocking as soon as a fixed version exists. @@ -30,7 +33,7 @@ The runtime base is pinned to the reviewed Ubuntu 26.04 `linux/amd64` manifest. ## Smoke test ```sh -docker run -d --name pod-pause-poc ghcr.io/pasturestack/pod-pause-image:v3.0.2 +docker run -d --name pod-pause-poc local/pasturestack/pod-pause-image:v3.0.2 docker inspect --format '{{.State.Running}} {{.Config.User}}' pod-pause-poc docker stop --time 5 pod-pause-poc docker inspect --format '{{.State.ExitCode}}' pod-pause-poc diff --git a/scripts/validate b/scripts/validate index b353b84..e13dd1a 100644 --- a/scripts/validate +++ b/scripts/validate @@ -20,17 +20,17 @@ test -f pause.c test -f .github/workflows/security-release-gate.yml . ./ubuntu-apt.lock -test "$UBUNTU_APT_LOCKED_SNAPSHOT" = '20260825T000000Z' +test "$UBUNTU_APT_LOCKED_SNAPSHOT" = '20260909T000000Z' test "$UBUNTU_APT_BUILD_ESSENTIAL_VERSION" = '12.12ubuntu2.26.04.2' test "$UBUNTU_APT_CA_CERTIFICATES_VERSION" = '20260601~26.04.1' test "$UBUNTU_APT_DPKG_DEV_VERSION" = '1.23.7ubuntu1' test "$UBUNTU_APT_GPP_VERSION" = '4:15.2.0-5ubuntu1' test "$UBUNTU_APT_GCC_VERSION" = '4:15.2.0-5ubuntu1' -test "$UBUNTU_APT_LIBC6_DEV_VERSION" = '2.43-2ubuntu2.3' +test "$UBUNTU_APT_LIBC6_DEV_VERSION" = '2.43-2ubuntu2.4' test "$UBUNTU_APT_MAKE_VERSION" = '4.4.1-3' grep -F 'ARG UBUNTU_IMAGE=ubuntu:26.04@sha256:' Dockerfile >/dev/null -grep -F 'https://snapshot.ubuntu.com/ubuntu/20260825T000000Z/' Dockerfile >/dev/null +grep -F 'https://snapshot.ubuntu.com/ubuntu/20260909T000000Z/' Dockerfile >/dev/null grep -F 'https://snapshot.ubuntu.com/ubuntu/%s' Dockerfile >/dev/null grep -F 'build-essential="${UBUNTU_APT_BUILD_ESSENTIAL_VERSION}"' Dockerfile >/dev/null grep -F 'gcc="${UBUNTU_APT_GCC_VERSION}"' Dockerfile >/dev/null @@ -39,6 +39,8 @@ grep -F 'builder-toolchain.tsv' Dockerfile >/dev/null grep -F 'runtime-ubuntu-packages.tsv' Dockerfile >/dev/null grep -F 'rm -f /usr/bin/pebble' Dockerfile >/dev/null grep -F 'ARG IMAGE_VERSION=v3.0.2' Dockerfile >/dev/null +grep -F '. ./ubuntu-apt.lock' .github/workflows/security-release-gate.yml >/dev/null +grep -F 'UBUNTU_APT_LOCKED_SNAPSHOT' .github/workflows/security-release-gate.yml >/dev/null grep -F 'aquasec/trivy:0.74.0@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969' \ .github/workflows/security-release-gate.yml >/dev/null grep -F 'builder-unfixed-kernel-header-findings.json' \ diff --git a/ubuntu-apt.lock b/ubuntu-apt.lock index 23e56ef..2c56847 100644 --- a/ubuntu-apt.lock +++ b/ubuntu-apt.lock @@ -3,12 +3,12 @@ # Source indexes are the official resolute, resolute-updates, and # resolute-security Packages files below the dated snapshot URL. -UBUNTU_APT_LOCKED_SNAPSHOT='20260825T000000Z' +UBUNTU_APT_LOCKED_SNAPSHOT='20260909T000000Z' UBUNTU_APT_BUILD_ESSENTIAL_VERSION='12.12ubuntu2.26.04.2' UBUNTU_APT_CA_CERTIFICATES_VERSION='20260601~26.04.1' UBUNTU_APT_DPKG_DEV_VERSION='1.23.7ubuntu1' UBUNTU_APT_GPP_VERSION='4:15.2.0-5ubuntu1' UBUNTU_APT_GCC_VERSION='4:15.2.0-5ubuntu1' -UBUNTU_APT_LIBC6_DEV_VERSION='2.43-2ubuntu2.3' +UBUNTU_APT_LIBC6_DEV_VERSION='2.43-2ubuntu2.4' UBUNTU_APT_MAKE_VERSION='4.4.1-3'