diff --git a/.github/workflows/post-release-audit.yml b/.github/workflows/post-release-audit.yml index 7adc507..8306a82 100644 --- a/.github/workflows/post-release-audit.yml +++ b/.github/workflows/post-release-audit.yml @@ -17,7 +17,8 @@ permissions: id-token: write env: - IMAGE_NAME: ghcr.io/ploos-as/soju + GHCR_IMAGE: ghcr.io/ploos-as/soju + DOCKERHUB_IMAGE: ${{ secrets.DOCKERHUB_USERNAME }}/soju jobs: audit: @@ -45,27 +46,27 @@ jobs: while IFS= read -r candidate; do [[ -n "$candidate" ]] || continue commit="$(git rev-list -n 1 "$candidate" 2>/dev/null || true)" - if [[ "$commit" == "$PRODUCER_SHA" ]]; then - tag="$candidate" - break - fi + if [[ "$commit" == "$PRODUCER_SHA" ]]; then tag="$candidate"; break; fi done < <(gh release list --repo "$GITHUB_REPOSITORY" --limit 100 --json tagName,isDraft --jq '.[] | select(.isDraft == false) | .tagName') fi [[ -n "$tag" ]] tag_commit="$(git rev-list -n 1 "$tag")" - if [[ -n "$INPUT_TAG" ]]; then - sh scripts/verify-release-ref.sh "$tag" "$tag_commit" --allow-head-mismatch-for-audit - else - sh scripts/verify-release-ref.sh "$tag" "$tag_commit" - fi + if [[ -n "$INPUT_TAG" ]]; then sh scripts/verify-release-ref.sh "$tag" "$tag_commit" --allow-head-mismatch-for-audit; else sh scripts/verify-release-ref.sh "$tag" "$tag_commit"; fi release_json="$(gh release view "$tag" --repo "$GITHUB_REPOSITORY" --json tagName,isDraft,isPrerelease)" jq -e --arg tag "$tag" '.tagName == $tag and .isDraft == false' <<<"$release_json" >/dev/null - if [[ -n "$PRODUCER_SHA" ]]; then - test "$tag_commit" = "$PRODUCER_SHA" - fi + if [[ -n "$PRODUCER_SHA" ]]; then test "$tag_commit" = "$PRODUCER_SHA"; fi echo "tag=$tag" >> "$GITHUB_OUTPUT" echo "commit=$tag_commit" >> "$GITHUB_OUTPUT" + - name: Validate Docker Hub audit configuration + env: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} + run: | + set -eu + test -n "$DOCKERHUB_USERNAME" + test -n "$DOCKERHUB_TOKEN" + - name: Set up QEMU uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 @@ -79,37 +80,48 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Resolve immutable release digest and verify platforms + - name: Log in to Docker Hub + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Resolve immutable release digest and verify registry parity id: image env: TAG: ${{ steps.release.outputs.tag }} shell: bash run: | set -euo pipefail - image_tag="${IMAGE_NAME}:${TAG#v}" - docker manifest inspect "$image_tag" > /tmp/manifest.json - jq -e 'any(.manifests[]; .platform.os == "linux" and .platform.architecture == "amd64")' /tmp/manifest.json >/dev/null - jq -e 'any(.manifests[]; .platform.os == "linux" and .platform.architecture == "arm64")' /tmp/manifest.json >/dev/null - docker pull --platform linux/amd64 "$image_tag" >/dev/null - repo_digest="$(docker image inspect "$image_tag" --format '{{index .RepoDigests 0}}')" - digest="${repo_digest##*@}" - sh scripts/verify-exact-digest-runtime.sh "$IMAGE_NAME" "$digest" >/dev/null - echo "digest=$digest" >> "$GITHUB_OUTPUT" + ghcr_tag="${GHCR_IMAGE}:${TAG#v}" + dockerhub_tag="${DOCKERHUB_IMAGE}:${TAG#v}" + docker manifest inspect "$ghcr_tag" > /tmp/ghcr-manifest.json + jq -e 'any(.manifests[]; .platform.os == "linux" and .platform.architecture == "amd64")' /tmp/ghcr-manifest.json >/dev/null + jq -e 'any(.manifests[]; .platform.os == "linux" and .platform.architecture == "arm64")' /tmp/ghcr-manifest.json >/dev/null + docker manifest inspect "$dockerhub_tag" > /tmp/dockerhub-manifest.json + jq -e 'any(.manifests[]; .platform.os == "linux" and .platform.architecture == "amd64")' /tmp/dockerhub-manifest.json >/dev/null + jq -e 'any(.manifests[]; .platform.os == "linux" and .platform.architecture == "arm64")' /tmp/dockerhub-manifest.json >/dev/null + ghcr_digest="$(docker buildx imagetools inspect "$ghcr_tag" | awk '/^Digest:/ {print $2; exit}')" + dockerhub_digest="$(docker buildx imagetools inspect "$dockerhub_tag" | awk '/^Digest:/ {print $2; exit}')" + test -n "$ghcr_digest" + test "$dockerhub_digest" = "$ghcr_digest" + sh scripts/verify-exact-digest-runtime.sh "$GHCR_IMAGE" "$ghcr_digest" >/dev/null + sh scripts/verify-exact-digest-runtime.sh "$DOCKERHUB_IMAGE" "$dockerhub_digest" >/dev/null + echo "digest=$ghcr_digest" >> "$GITHUB_OUTPUT" - name: Install Cosign uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 - - name: Verify exact-digest signature + - name: Verify exact-digest signatures env: IMAGE_DIGEST: ${{ steps.image.outputs.digest }} shell: bash run: | set -euo pipefail - cosign verify \ - --certificate-oidc-issuer https://token.actions.githubusercontent.com \ - --certificate-identity-regexp '^https://github.com/Ploos-AS/soju/.github/workflows/release.yml@refs/tags/v.*$' \ - "${IMAGE_NAME}@${IMAGE_DIGEST}" > /tmp/cosign-verification.json - jq -e 'length > 0' /tmp/cosign-verification.json >/dev/null + for image in "$GHCR_IMAGE" "$DOCKERHUB_IMAGE"; do + cosign verify --certificate-oidc-issuer https://token.actions.githubusercontent.com --certificate-identity-regexp '^https://github.com/Ploos-AS/soju/.github/workflows/release.yml@refs/tags/v.*$' "${image}@${IMAGE_DIGEST}" > /tmp/cosign-verification.json + jq -e 'length > 0' /tmp/cosign-verification.json >/dev/null + done - name: Verify immutable labels and runtime after release env: @@ -119,7 +131,7 @@ jobs: shell: bash run: | set -euo pipefail - image_ref="${IMAGE_NAME}@${IMAGE_DIGEST}" + image_ref="${GHCR_IMAGE}@${IMAGE_DIGEST}" docker pull --platform linux/amd64 "$image_ref" >/dev/null revision="$(docker image inspect "$image_ref" --format '{{index .Config.Labels "org.opencontainers.image.revision"}}')" version="$(docker image inspect "$image_ref" --format '{{index .Config.Labels "org.opencontainers.image.version"}}')" @@ -129,18 +141,10 @@ jobs: test "$(docker run --rm --platform linux/amd64 --entrypoint id "$image_ref" -g)" = "1000" mkdir -p data-audit run-audit sudo chown 1000:1000 data-audit run-audit - cid=$(docker run -d --rm --platform linux/amd64 \ - -v "$PWD/data-audit:/var/lib/soju" \ - -v "$PWD/run-audit:/run/soju" \ - "$image_ref") + cid=$(docker run -d --rm --platform linux/amd64 -v "$PWD/data-audit:/var/lib/soju" -v "$PWD/run-audit:/run/soju" "$image_ref") trap 'docker logs "$cid" 2>&1 || true; docker rm -f "$cid" >/dev/null 2>&1 || true' EXIT for i in $(seq 1 30); do - if docker ps --filter "id=$cid" --format '{{.ID}}' | grep -q . \ - && test -f data-audit/main.db \ - && test -S run-audit/admin; then - healthy=true - break - fi + if docker ps --filter "id=$cid" --format '{{.ID}}' | grep -q . && test -f data-audit/main.db && test -S run-audit/admin; then healthy=true; break; fi sleep 1 done test "${healthy:-false}" = true @@ -153,13 +157,14 @@ jobs: IMAGE_DIGEST: ${{ steps.image.outputs.digest }} run: | { - echo '### M11.5 post-release audit' + echo '### M12.1 dual-registry post-release audit' echo echo "- release: $TAG" echo "- immutable digest: $IMAGE_DIGEST" echo '- GitHub Release/tag/commit binding: verified' - echo '- linux/amd64 + linux/arm64 manifest coverage: verified' - echo '- Cosign keyless signature and release workflow OIDC identity: verified' + echo '- GHCR + Docker Hub digest parity: verified' + echo '- linux/amd64 + linux/arm64 manifest coverage in both registries: verified' + echo '- Cosign keyless signatures in both registries: verified' echo '- OCI revision/version labels: verified' echo '- non-root UID/GID 1000: verified' echo '- exact-digest post-publication SQLite + admin-socket runtime: verified' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a750503..db47617 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,8 @@ permissions: attestations: write env: - IMAGE_NAME: ghcr.io/ploos-as/soju + GHCR_IMAGE: ghcr.io/ploos-as/soju + DOCKERHUB_IMAGE: ${{ secrets.DOCKERHUB_USERNAME }}/soju jobs: publish: @@ -25,6 +26,15 @@ jobs: - name: Verify release tag and commit binding run: sh scripts/verify-release-ref.sh "$GITHUB_REF_NAME" "$GITHUB_SHA" + - name: Validate Docker Hub publication configuration + env: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} + run: | + set -eu + test -n "$DOCKERHUB_USERNAME" + test -n "$DOCKERHUB_TOKEN" + - name: Set up QEMU uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 @@ -38,11 +48,19 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Log in to Docker Hub + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Docker metadata id: meta uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 with: - images: ${{ env.IMAGE_NAME }} + images: | + ${{ env.GHCR_IMAGE }} + ${{ env.DOCKERHUB_IMAGE }} flavor: | latest=auto tags: | @@ -74,65 +92,61 @@ jobs: IMAGE_DIGEST: ${{ steps.push.outputs.digest }} run: | set -eu - image_ref="$(sh scripts/verify-exact-digest-runtime.sh "$IMAGE_NAME" "$IMAGE_DIGEST")" - docker pull "$image_ref" - resolved="$(docker image inspect "$image_ref" --format '{{index .RepoDigests 0}}')" - test "$resolved" = "$image_ref" - revision="$(docker image inspect "$image_ref" --format '{{index .Config.Labels "org.opencontainers.image.revision"}}')" - version="$(docker image inspect "$image_ref" --format '{{index .Config.Labels "org.opencontainers.image.version"}}')" + ghcr_ref="$(sh scripts/verify-exact-digest-runtime.sh "$GHCR_IMAGE" "$IMAGE_DIGEST")" + dockerhub_ref="$(sh scripts/verify-exact-digest-runtime.sh "$DOCKERHUB_IMAGE" "$IMAGE_DIGEST")" + docker pull "$ghcr_ref" + ghcr_resolved="$(docker image inspect "$ghcr_ref" --format '{{index .RepoDigests 0}}')" + test "$ghcr_resolved" = "$ghcr_ref" + docker pull "$dockerhub_ref" + dockerhub_resolved="$(docker image inspect "$dockerhub_ref" --format '{{range .RepoDigests}}{{println .}}{{end}}' | grep -F "${DOCKERHUB_IMAGE}@${IMAGE_DIGEST}" | head -n 1)" + test "$dockerhub_resolved" = "$dockerhub_ref" + revision="$(docker image inspect "$ghcr_ref" --format '{{index .Config.Labels "org.opencontainers.image.revision"}}')" + version="$(docker image inspect "$ghcr_ref" --format '{{index .Config.Labels "org.opencontainers.image.version"}}')" test "$revision" = "$GITHUB_SHA" test "$version" = "$GITHUB_REF_NAME" - test "$(docker run --rm --entrypoint id "$image_ref" -u)" = "1000" - test "$(docker run --rm --entrypoint id "$image_ref" -g)" = "1000" + test "$(docker run --rm --entrypoint id "$ghcr_ref" -u)" = "1000" + test "$(docker run --rm --entrypoint id "$ghcr_ref" -g)" = "1000" mkdir -p data-release run-release sudo chown 1000:1000 data-release run-release - cid=$(docker run -d --rm \ - -v "$PWD/data-release:/var/lib/soju" \ - -v "$PWD/run-release:/run/soju" \ - "$image_ref") + cid=$(docker run -d --rm -v "$PWD/data-release:/var/lib/soju" -v "$PWD/run-release:/run/soju" "$ghcr_ref") trap 'docker logs "$cid" 2>&1 || true; docker rm -f "$cid" >/dev/null 2>&1 || true' EXIT for i in $(seq 1 30); do - if docker ps --filter "id=$cid" --format '{{.ID}}' | grep -q . \ - && test -f data-release/main.db \ - && test -S run-release/admin; then - qualified=true - break - fi + if docker ps --filter "id=$cid" --format '{{.ID}}' | grep -q . && test -f data-release/main.db && test -S run-release/admin; then qualified=true; break; fi sleep 1 done test "${qualified:-false}" = true docker rm -f "$cid" trap - EXIT - - name: Attest image + - name: Attest GHCR image uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a with: - subject-name: ${{ env.IMAGE_NAME }} + subject-name: ${{ env.GHCR_IMAGE }} subject-digest: ${{ steps.push.outputs.digest }} push-to-registry: true - name: Install Cosign uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 - - name: Sign exact release digest with GitHub OIDC + - name: Sign exact release digests with GitHub OIDC env: IMAGE_DIGEST: ${{ steps.push.outputs.digest }} run: | set -eu test -n "$IMAGE_DIGEST" - cosign sign --yes "${IMAGE_NAME}@${IMAGE_DIGEST}" + cosign sign --yes "${GHCR_IMAGE}@${IMAGE_DIGEST}" + cosign sign --yes "${DOCKERHUB_IMAGE}@${IMAGE_DIGEST}" - - name: Verify keyless release signature + - name: Verify keyless release signatures env: IMAGE_DIGEST: ${{ steps.push.outputs.digest }} run: | set -eu test -n "$IMAGE_DIGEST" - cosign verify \ - --certificate-oidc-issuer https://token.actions.githubusercontent.com \ - --certificate-identity-regexp '^https://github.com/Ploos-AS/soju/.github/workflows/release.yml@refs/tags/v.*$' \ - "${IMAGE_NAME}@${IMAGE_DIGEST}" > /tmp/cosign-verification.json - test -s /tmp/cosign-verification.json + for image in "$GHCR_IMAGE" "$DOCKERHUB_IMAGE"; do + cosign verify --certificate-oidc-issuer https://token.actions.githubusercontent.com --certificate-identity-regexp '^https://github.com/Ploos-AS/soju/.github/workflows/release.yml@refs/tags/v.*$' "${image}@${IMAGE_DIGEST}" > /tmp/cosign-verification.json + test -s /tmp/cosign-verification.json + done - name: Create GitHub Release env: @@ -142,9 +156,5 @@ jobs: if gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then echo "GitHub Release already exists for $GITHUB_REF_NAME" else - gh release create "$GITHUB_REF_NAME" \ - --repo "$GITHUB_REPOSITORY" \ - --title "soju $GITHUB_REF_NAME" \ - --generate-notes \ - --verify-tag + gh release create "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --title "soju $GITHUB_REF_NAME" --generate-notes --verify-tag fi diff --git a/README.md b/README.md index 90abfa7..557d0ff 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,24 @@ Production-oriented OCI packaging for [soju](https://soju.im/), the IRC bouncer maintained at Codeberg. -> Upstream already publishes a container image. This repository is an independent Ploos-AS packaging project focused on reproducible source builds, explicit upstream pinning, non-root operation, multi-architecture GHCR releases, and supply-chain attestations. It is not a fork of soju. +> Upstream already publishes a container image. This repository is an independent Ploos-AS packaging project focused on reproducible source builds, explicit upstream pinning, non-root operation, multi-architecture OCI releases, and supply-chain attestations. It is not a fork of soju. -## Image +## Images + +Primary registry: ```text ghcr.io/ploos-as/soju ``` +Secondary registry: + +```text +/soju +``` + +Release workflows publish the same multi-architecture build to GHCR and Docker Hub. Docker Hub authentication uses the GitHub Actions repository variable `DOCKERHUB_USERNAME` and secret `DOCKERHUB_TOKEN`. + Target platforms: - `linux/amd64` @@ -109,7 +119,7 @@ CI verifies that the container starts as UID/GID 1000, creates its SQLite databa ## Releases -Tags matching `v*` publish semver aliases to GHCR and build both amd64 and arm64 images. Release builds include SBOM and provenance/attestation metadata. +Tags matching `v*` publish semver aliases to both GHCR and Docker Hub and build both amd64 and arm64 images. Release builds include SBOM and provenance/attestation metadata. The exact published digest is runtime-qualified, and keyless Cosign signatures are written and verified for both registry references. ## Upstream