From eea568bfb2809044265bd5290003390e1e0825e1 Mon Sep 17 00:00:00 2001 From: pos Date: Sat, 5 Sep 2026 14:18:53 +0200 Subject: [PATCH 1/4] M0.14: make signer identity testable --- scripts/verify-release.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/verify-release.sh b/scripts/verify-release.sh index 89a2c07..a274cf7 100644 --- a/scripts/verify-release.sh +++ b/scripts/verify-release.sh @@ -4,6 +4,7 @@ set -euo pipefail IMAGE="${IMAGE:-ghcr.io/ploos-as/glowing-bear}" VERSION="${VERSION:?VERSION is required}" OIDC_ISSUER="${OIDC_ISSUER:-https://token.actions.githubusercontent.com}" +EXPECTED_IDENTITY="${EXPECTED_IDENTITY:-}" PULL="${PULL:-0}" RUNTIME="${RUNTIME:-auto}" @@ -40,7 +41,8 @@ for command in skopeo cosign; do done primary="$IMAGE:$VERSION" -identity="https://github.com/Ploos-AS/glowing-bear/.github/workflows/sign-release.yml@refs/tags/$TAG" +default_identity="https://github.com/Ploos-AS/glowing-bear/.github/workflows/sign-release.yml@refs/tags/$TAG" +identity="${EXPECTED_IDENTITY:-$default_identity}" image_digest="$(skopeo inspect --no-tags --format '{{.Digest}}' "docker://$primary")" if [[ ! "$image_digest" =~ ^sha256:[0-9a-f]{64}$ ]]; then From 4876e6d43e4f2f9fcbbeb1664664e426c5deb49e Mon Sep 17 00:00:00 2001 From: pos Date: Sat, 5 Sep 2026 14:19:06 +0200 Subject: [PATCH 2/4] M0.14: add fail-closed verification matrix --- scripts/qualify_negative_verification.sh | 59 ++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 scripts/qualify_negative_verification.sh diff --git a/scripts/qualify_negative_verification.sh b/scripts/qualify_negative_verification.sh new file mode 100644 index 0000000..140e88e --- /dev/null +++ b/scripts/qualify_negative_verification.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +set -euo pipefail + +VERSION="${VERSION:-0.2.3}" + +expect_fail() { + local name="$1" + shift + local stdout_file stderr_file + stdout_file="$(mktemp)" + stderr_file="$(mktemp)" + + if "$@" >"$stdout_file" 2>"$stderr_file"; then + echo "negative test unexpectedly succeeded: $name" >&2 + cat "$stdout_file" >&2 || true + cat "$stderr_file" >&2 || true + rm -f "$stdout_file" "$stderr_file" + exit 1 + fi + + if grep -Eq '^ghcr\.io/ploos-as/glowing-bear@sha256:[0-9a-f]{64}$' "$stdout_file"; then + echo "negative test leaked an accepted immutable ref on stdout: $name" >&2 + cat "$stdout_file" >&2 || true + rm -f "$stdout_file" "$stderr_file" + exit 1 + fi + + echo "PASS (rejected): $name" + rm -f "$stdout_file" "$stderr_file" +} + +expect_fail "wrong OIDC issuer" \ + env VERSION="$VERSION" OIDC_ISSUER=https://example.invalid PULL=0 \ + bash scripts/verify-release.sh + +expect_fail "wrong signer identity" \ + env VERSION="$VERSION" EXPECTED_IDENTITY=https://github.com/Ploos-AS/glowing-bear/.github/workflows/not-the-signer.yml@refs/tags/v$VERSION PULL=0 \ + bash scripts/verify-release.sh + +expect_fail "unsigned foreign image" \ + env IMAGE=docker.io/library/alpine VERSION=3.20 PULL=0 \ + bash scripts/verify-release.sh + +expect_fail "nonexistent release tag" \ + env VERSION=99.99.99 PULL=0 \ + bash scripts/verify-release.sh + +expect_fail "invalid runtime selector" \ + env VERSION="$VERSION" RUNTIME=containerd PULL=0 \ + bash scripts/verify-release.sh + +expect_fail "invalid pull selector" \ + env VERSION="$VERSION" PULL=yes \ + bash scripts/verify-release.sh + +expect_fail "missing VERSION" \ + env -u VERSION bash scripts/verify-release.sh + +echo "All negative verification cases rejected as expected." From 1587c27c7e0a9a6ebd01ec82227a861e970c0bb2 Mon Sep 17 00:00:00 2001 From: pos Date: Sat, 5 Sep 2026 14:19:40 +0200 Subject: [PATCH 3/4] M0.14: qualify negative verification paths --- .github/workflows/runtime-neutral-verification.yml | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/runtime-neutral-verification.yml b/.github/workflows/runtime-neutral-verification.yml index 875ed83..a36f9fe 100644 --- a/.github/workflows/runtime-neutral-verification.yml +++ b/.github/workflows/runtime-neutral-verification.yml @@ -31,7 +31,9 @@ jobs: - name: Install Cosign uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0 - name: Validate script syntax - run: bash -n scripts/verify-release.sh + run: | + bash -n scripts/verify-release.sh + bash -n scripts/qualify_negative_verification.sh - name: Verify clean immutable output shell: bash run: | @@ -50,11 +52,6 @@ jobs: set -euo pipefail ref="$(VERSION="$VERSION" PULL=1 RUNTIME=podman bash scripts/verify-release.sh)" podman image inspect "$ref" >/dev/null - - name: Reject wrong OIDC issuer + - name: Qualify fail-closed negative paths shell: bash - run: | - set -euo pipefail - if VERSION="$VERSION" OIDC_ISSUER=https://example.invalid PULL=0 bash scripts/verify-release.sh >/dev/null 2>&1; then - echo "verification unexpectedly accepted the wrong issuer" >&2 - exit 1 - fi + run: VERSION="$VERSION" bash scripts/qualify_negative_verification.sh From 3b17bf1e56db67b8fda4892319d518ffd1f94dab Mon Sep 17 00:00:00 2001 From: pos Date: Sat, 5 Sep 2026 14:19:52 +0200 Subject: [PATCH 4/4] M0.14: document negative-path hardening --- docs/M0_14_NEGATIVE_PATH_HARDENING.md | 41 +++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 docs/M0_14_NEGATIVE_PATH_HARDENING.md diff --git a/docs/M0_14_NEGATIVE_PATH_HARDENING.md b/docs/M0_14_NEGATIVE_PATH_HARDENING.md new file mode 100644 index 0000000..c3d3917 --- /dev/null +++ b/docs/M0_14_NEGATIVE_PATH_HARDENING.md @@ -0,0 +1,41 @@ +# M0.14 negative-path hardening + +M0.14 proves that the verified deployment path fails closed when trust, image, release, or runtime inputs are wrong. + +## Scope + +The positive trust path remains unchanged: release tags are resolved with Skopeo, the resulting immutable digest is verified with Cosign against the expected GitHub Actions certificate identity and OIDC issuer, and only then may Docker or Podman pull the image. + +M0.14 adds an explicit negative qualification matrix. Each case must fail with a non-zero exit status and must not emit an accepted Glowing Bear immutable image reference on stdout. + +## Negative cases + +CI rejects all of the following: + +- wrong GitHub Actions OIDC issuer; +- wrong certificate identity / signer workflow; +- an unsigned unrelated container image; +- a nonexistent Glowing Bear release tag; +- an unsupported runtime selector; +- an invalid PULL selector; +- a missing VERSION value. + +The signed `v0.2.3` release remains the positive qualification target. Existing tags are not moved or rewritten. + +## Testability of signer identity + +`scripts/verify-release.sh` still defaults to the exact production signer identity: + +`https://github.com/Ploos-AS/glowing-bear/.github/workflows/sign-release.yml@refs/tags/v` + +`EXPECTED_IDENTITY` exists only as an explicit override so CI and operators can test a stricter or intentionally mismatching identity. It does not weaken the default policy: with no override, the repository's release-signing workflow and exact tag remain mandatory. + +## Acceptance criteria + +M0.14 is complete when: + +1. the normal signed release verification succeeds; +2. Docker and Podman verified pull paths still succeed; +3. every negative case fails; +4. no negative case emits an accepted immutable Glowing Bear ref on stdout; +5. all existing repository workflows remain green.