Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 48 additions & 43 deletions .github/workflows/post-release-audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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

Expand All @@ -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:
Expand All @@ -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"}}')"
Expand All @@ -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
Expand All @@ -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'
Expand Down
80 changes: 45 additions & 35 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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

Expand All @@ -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: |
Expand Down Expand Up @@ -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:
Expand All @@ -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
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<dockerhub-namespace>/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`
Expand Down Expand Up @@ -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

Expand Down
Loading