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
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ jobs:
echo "tag/commit mismatch unexpectedly accepted" >&2
exit 1
fi
if sh scripts/verify-release-ref.sh v0.0.0-m11.2 "$GITHUB_SHA" invalid-mode; then
echo "invalid verification mode unexpectedly accepted" >&2
exit 1
fi
parent="$(git rev-parse HEAD^)"
git tag v0.0.0-m11.5-audit "$parent"
if sh scripts/verify-release-ref.sh v0.0.0-m11.5-audit "$parent"; then
echo "strict mode unexpectedly accepted historical tag" >&2
exit 1
fi
sh scripts/verify-release-ref.sh v0.0.0-m11.5-audit "$parent" --allow-head-mismatch-for-audit

- name: Verify exact-digest policy
run: |
Expand Down
166 changes: 166 additions & 0 deletions .github/workflows/post-release-audit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
name: Post-release audit

on:
workflow_run:
workflows: [Release]
types: [completed]
workflow_dispatch:
inputs:
tag:
description: Existing release tag to audit
required: true
type: string

permissions:
contents: read
packages: read
id-token: write

env:
IMAGE_NAME: ghcr.io/ploos-as/soju

jobs:
audit:
if: >-
github.event_name == 'workflow_dispatch' ||
github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
fetch-depth: 0

- name: Select release bound to producer commit
id: release
env:
GH_TOKEN: ${{ github.token }}
INPUT_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || '' }}
PRODUCER_SHA: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || '' }}
shell: bash
run: |
set -euo pipefail
git fetch --force --tags origin
tag="$INPUT_TAG"
if [[ -z "$tag" ]]; then
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
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
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
echo "tag=$tag" >> "$GITHUB_OUTPUT"
echo "commit=$tag_commit" >> "$GITHUB_OUTPUT"

- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f

- name: Log in to GHCR
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Resolve immutable release digest and verify platforms
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"

- name: Install Cosign
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62

- name: Verify exact-digest signature
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

- name: Verify immutable labels and runtime after release
env:
IMAGE_DIGEST: ${{ steps.image.outputs.digest }}
TAG: ${{ steps.release.outputs.tag }}
RELEASE_COMMIT: ${{ steps.release.outputs.commit }}
shell: bash
run: |
set -euo pipefail
image_ref="${IMAGE_NAME}@${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"}}')"
test "$revision" = "$RELEASE_COMMIT"
test "$version" = "$TAG"
test "$(docker run --rm --platform linux/amd64 --entrypoint id "$image_ref" -u)" = "1000"
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")
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
sleep 1
done
test "${healthy:-false}" = true
docker rm -f "$cid"
trap - EXIT

- name: Record post-release audit summary
env:
TAG: ${{ steps.release.outputs.tag }}
IMAGE_DIGEST: ${{ steps.image.outputs.digest }}
run: |
{
echo '### M11.5 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 '- OCI revision/version labels: verified'
echo '- non-root UID/GID 1000: verified'
echo '- exact-digest post-publication SQLite + admin-socket runtime: verified'
} >> "$GITHUB_STEP_SUMMARY"
23 changes: 17 additions & 6 deletions scripts/verify-release-ref.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,21 @@ set -eu

tag=${1:-${GITHUB_REF_NAME:-}}
expected_sha=${2:-${GITHUB_SHA:-}}
mode=${3:-strict}

if [ -z "$tag" ] || [ -z "$expected_sha" ]; then
echo "usage: verify-release-ref.sh <tag> <expected-commit-sha>" >&2
echo "usage: verify-release-ref.sh <tag> <expected-commit-sha> [--allow-head-mismatch-for-audit]" >&2
exit 2
fi

case "$mode" in
strict|--allow-head-mismatch-for-audit) ;;
*)
echo "invalid verification mode: $mode" >&2
exit 2
;;
esac

if ! printf '%s\n' "$tag" | grep -Eq '^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$'; then
echo "invalid release tag: $tag" >&2
exit 1
Expand All @@ -24,10 +33,12 @@ if [ "$actual_sha" != "$expected_sha" ]; then
exit 1
fi

head_sha=$(git rev-parse HEAD)
if [ "$head_sha" != "$expected_sha" ]; then
echo "checkout/commit mismatch: HEAD=$head_sha expected=$expected_sha" >&2
exit 1
if [ "$mode" = "strict" ]; then
head_sha=$(git rev-parse HEAD)
if [ "$head_sha" != "$expected_sha" ]; then
echo "checkout/commit mismatch: HEAD=$head_sha expected=$expected_sha" >&2
exit 1
fi
fi

printf 'release ref verified: tag=%s commit=%s\n' "$tag" "$expected_sha"
printf 'release ref verified: tag=%s commit=%s mode=%s\n' "$tag" "$expected_sha" "$mode"
Loading