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
13 changes: 5 additions & 8 deletions .github/workflows/runtime-neutral-verification.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand All @@ -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
41 changes: 41 additions & 0 deletions docs/M0_14_NEGATIVE_PATH_HARDENING.md
Original file line number Diff line number Diff line change
@@ -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<VERSION>`

`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.
59 changes: 59 additions & 0 deletions scripts/qualify_negative_verification.sh
Original file line number Diff line number Diff line change
@@ -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."
4 changes: 3 additions & 1 deletion scripts/verify-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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}"

Expand Down Expand Up @@ -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
Expand Down
Loading