Skip to content
Draft
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
260 changes: 260 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
name: Release evidence

on:
pull_request:
paths:
- ".github/workflows/release.yml"
- "tests/release_evidence_contract.rs"
- "Cargo.toml"
- "Cargo.lock"
- "rust-toolchain.toml"
- "Dockerfile"
- "src/**"
- "crates/**"
- "CHANGELOG.md"
workflow_dispatch:
inputs:
version:
description: Canonical MAJOR.MINOR.PATCH version already declared by Cargo.toml
required: true
type: string

concurrency:
group: release-evidence-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

permissions:
contents: read

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

jobs:
release-evidence:
name: Build exact-source release evidence
runs-on: ubuntu-24.04
timeout-minutes: 30
permissions:
contents: read
outputs:
source_sha: ${{ steps.identity.outputs.sha }}
version: ${{ steps.identity.outputs.version }}
steps:
- name: Harden release evidence runner
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
disable-telemetry: true

- name: Check out the exact candidate without persisted credentials
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
fetch-depth: 1
persist-credentials: false

- name: Read the reviewed Rust toolchain
id: toolchain
shell: bash
run: |
set -euo pipefail
version="$(sed -n 's/^channel = "\([0-9][0-9.]*\)"$/\1/p' rust-toolchain.toml)"
if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::rust-toolchain.toml must pin an exact numeric Rust release."
exit 1
fi
echo "version=$version" >> "$GITHUB_OUTPUT"

- name: Install the reviewed Rust toolchain
uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # immutable pin
with:
toolchain: ${{ steps.toolchain.outputs.version }}
components: llvm-tools-preview, rustfmt, clippy

- name: Verify release source identity
id: identity
shell: bash
env:
REQUESTED_VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
checked_sha="$(git rev-parse HEAD)"
if [ "$checked_sha" != "$GITHUB_SHA" ] && [ "${{ github.event_name }}" != "pull_request" ]; then
echo "::error::Checked-out source does not match the workflow event commit."
exit 1
fi

package_version="$(cargo metadata --locked --no-deps --format-version=1 | jq -r '.packages[] | select(.name == "waf-ids-ai-soc") | .version')"
if ! [[ "$package_version" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
echo "::error::Cargo.toml must expose one canonical MAJOR.MINOR.PATCH package version."
exit 1
fi

if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
if [ "$GITHUB_REF" != "refs/heads/main" ] || [ "$GITHUB_REF_PROTECTED" != "true" ]; then
echo "::error::Stable release evidence must be dispatched from protected main."
exit 1
fi
if [ "$REQUESTED_VERSION" != "$package_version" ]; then
echo "::error::Requested release version must equal the reviewed Cargo.toml package version."
exit 1
fi
fi

echo "sha=$checked_sha" >> "$GITHUB_OUTPUT"
echo "version=$package_version" >> "$GITHUB_OUTPUT"

- name: Repeat exact-source quality gates
shell: bash
run: |
set -euo pipefail
cargo fmt --check
cargo test --locked --workspace
cargo clippy --locked --workspace --all-targets -- -D warnings

- name: Build locked release binary
shell: bash
run: cargo build --locked --release

- name: Assemble deterministic source-bound package evidence
shell: bash
env:
RELEASE_SHA: ${{ steps.identity.outputs.sha }}
RELEASE_VERSION: ${{ steps.identity.outputs.version }}
RUST_TOOLCHAIN: ${{ steps.toolchain.outputs.version }}
run: |
set -euo pipefail
evidence="$RUNNER_TEMP/wardnet-release-evidence"
package="$RUNNER_TEMP/wardnet-package"
rm -rf "$evidence" "$package"
mkdir -p "$evidence" "$package"

install -m 0755 target/release/waf-ids-ai-soc "$package/waf-ids-ai-soc"
install -m 0644 LICENSE "$package/LICENSE"
install -m 0644 Cargo.toml "$package/Cargo.toml"
install -m 0644 Cargo.lock "$package/Cargo.lock"
install -m 0644 rust-toolchain.toml "$package/rust-toolchain.toml"

commit_epoch="$(git show -s --format=%ct "$RELEASE_SHA")"
tar --sort=name --mtime="@$commit_epoch" --owner=0 --group=0 --numeric-owner \
-C "$package" -cf - Cargo.lock Cargo.toml LICENSE rust-toolchain.toml waf-ids-ai-soc \
| gzip -n > "$evidence/wardnet-${RELEASE_VERSION}-linux-x86_64.tar.gz"

cargo metadata --locked --format-version=1 > "$evidence/cargo-metadata.json"
sha256sum Cargo.toml Cargo.lock rust-toolchain.toml Dockerfile \
> "$evidence/source-inputs.sha256"
(
cd "$evidence"
sha256sum "wardnet-${RELEASE_VERSION}-linux-x86_64.tar.gz" cargo-metadata.json \
> artifacts.sha256
)

cargo_lock_sha="$(sha256sum Cargo.lock | awk '{print $1}')"
artifact_sha="$(sha256sum "$evidence/wardnet-${RELEASE_VERSION}-linux-x86_64.tar.gz" | awk '{print $1}')"
jq -n \
--arg repository "$GITHUB_REPOSITORY" \
--arg source_sha "$RELEASE_SHA" \
--arg version "$RELEASE_VERSION" \
--arg rust_toolchain "$RUST_TOOLCHAIN" \
--arg cargo_lock_sha256 "$cargo_lock_sha" \
--arg artifact_sha256 "$artifact_sha" \
'{schema_version:"1",repository:$repository,source_sha:$source_sha,version:$version,rust_toolchain:$rust_toolchain,cargo_lock_sha256:$cargo_lock_sha256,artifact:{name:("wardnet-"+$version+"-linux-x86_64.tar.gz"),sha256:$artifact_sha256}}' \
> "$evidence/release-manifest.json"

echo "EVIDENCE_DIR=$evidence" >> "$GITHUB_ENV"
echo "PACKAGE_DIR=$package" >> "$GITHUB_ENV"

- name: Generate SPDX JSON SBOM from packaged release contents
uses: anchore/sbom-action@3ad7283483fc7af8ff2b4ea19663c2d5ca935e26 # v0.24.2
with:
path: ${{ env.PACKAGE_DIR }}
format: spdx-json
output-file: ${{ env.EVIDENCE_DIR }}/wardnet.spdx.json
upload-artifact: false
upload-release-assets: false

- name: Bind SBOM digest into the release manifest
shell: bash
run: |
set -euo pipefail
sbom_sha="$(sha256sum "$EVIDENCE_DIR/wardnet.spdx.json" | awk '{print $1}')"
manifest_tmp="$(mktemp)"
jq --arg sbom_sha256 "$sbom_sha" '. + {sbom:{format:"spdx-json",sha256:$sbom_sha256}}' \
"$EVIDENCE_DIR/release-manifest.json" > "$manifest_tmp"
mv "$manifest_tmp" "$EVIDENCE_DIR/release-manifest.json"
(
cd "$EVIDENCE_DIR"
sha256sum wardnet.spdx.json release-manifest.json >> artifacts.sha256
)

- name: Upload exact-source release evidence
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: wardnet-release-evidence-${{ steps.identity.outputs.sha }}
path: ${{ env.EVIDENCE_DIR }}/
if-no-files-found: error
retention-days: 30

attest-release-evidence:
name: Attest protected-main release evidence
if: github.event_name == 'workflow_dispatch'
needs: release-evidence
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
id-token: write
attestations: write
steps:
- name: Harden attestation runner
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
disable-telemetry: true

- name: Download exact-source release evidence
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: wardnet-release-evidence-${{ needs.release-evidence.outputs.source_sha }}
path: ${{ runner.temp }}/wardnet-release-evidence

- name: Verify protected-main evidence before granting attestation authority
shell: bash
env:
EVIDENCE_DIR: ${{ runner.temp }}/wardnet-release-evidence
RELEASE_SHA: ${{ needs.release-evidence.outputs.source_sha }}
RELEASE_VERSION: ${{ needs.release-evidence.outputs.version }}
REQUESTED_VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
if [ "$GITHUB_REF" != "refs/heads/main" ] || [ "$GITHUB_REF_PROTECTED" != "true" ]; then
echo "::error::Attestation authority is restricted to protected main."
exit 1
fi
if [ "$REQUESTED_VERSION" != "$RELEASE_VERSION" ]; then
echo "::error::Requested release version changed across the evidence boundary."
exit 1
fi
if [ "$(jq -r '.source_sha' "$EVIDENCE_DIR/release-manifest.json")" != "$RELEASE_SHA" ]; then
echo "::error::Downloaded release manifest does not match the reviewed source SHA."
exit 1
fi
if [ "$(jq -r '.version' "$EVIDENCE_DIR/release-manifest.json")" != "$RELEASE_VERSION" ]; then
echo "::error::Downloaded release manifest does not match the reviewed version."
exit 1
fi
(
cd "$EVIDENCE_DIR"
sha256sum --check artifacts.sha256
)

- name: Attest build provenance for protected-main dispatch
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
with:
subject-path: ${{ runner.temp }}/wardnet-release-evidence/wardnet-${{ needs.release-evidence.outputs.version }}-linux-x86_64.tar.gz

- name: Attest SBOM for protected-main dispatch
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
with:
subject-path: ${{ runner.temp }}/wardnet-release-evidence/wardnet-${{ needs.release-evidence.outputs.version }}-linux-x86_64.tar.gz
sbom-path: ${{ runner.temp }}/wardnet-release-evidence/wardnet.spdx.json
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@

- Removed the distributable Kubernetes administrator `Secret` and historical placeholder credential. Production deployments must provision `waf-ids-ai-soc-admin` / `ADMIN_TOKEN` through the external secret-management control plane; the workload's `secretKeyRef` is explicitly non-optional.
- Added a structural regression contract that rejects shipped administrator Secret objects, placeholder credentials, decoy workloads, init-container false positives, and optional administrator Secret references.
- Added a source-bound release-evidence workflow that produces a deterministic release archive, SHA-256 evidence, SPDX JSON SBOM, and protected-main GitHub/Sigstore provenance and SBOM attestations without granting the workflow publication authority.

### Operations

- Documented administrator credential provisioning, rotation, rollout verification, rollback, evidence handling, and the boundary with the separate runtime-authentication fail-closed work tracked in issue #78.
- Added an executable release-workflow architecture contract and supply-chain decision record. Full immutable publication, OCI promotion, production-shaped attack verification, and rehearsed rollback remain tracked by issue #84.
50 changes: 50 additions & 0 deletions docs/doctoring/release-evidence-supply-chain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Release evidence supply-chain decision

Status: Proposed while issue #84 prerequisites remain outside protected `main`.

## Problem

Wardnet has no immutable GitHub Release and protected `main` has no repository-owned release workflow. A production release therefore cannot yet bind one reviewed source revision to a packaged binary, dependency/build inputs, an SBOM, provenance, exact quality evidence, deployment identity, and rollback evidence.

Issue #84 is intentionally broader than this slice. Authentication, fail-closed egress, durable data authority, proven WAF/IDS enforcement, pinned deployment assets, production-shaped attack tests, promotion, and rehearsed rollback remain separate prerequisites. This change establishes the smallest release-evidence boundary that can be reviewed before those product prerequisites are protected truth.

## Constraints and rejected alternatives

- Do not create a GitHub Release from a pull-request head or another mutable development branch. A stable evidence dispatch must originate from protected `main` and its requested version must equal the reviewed Cargo package version.
- Do not rebuild an artifact later merely to attach evidence. The package archive, source-input hashes, SBOM, and provenance are generated in the same workflow from one checked-out source identity.
- Do not use floating GitHub Action refs. Release actions are pinned to immutable commit SHAs.
- Do not treat an attestation as a security verdict. Provenance establishes the relationship between an artifact and its build context; it does not prove that the artifact is vulnerability-free or policy-compliant.
- Do not claim SLSA v1.2 as an approved baseline. As rechecked on 2026-09-04, SLSA v1.1 is the latest Approved Specification published by the SLSA project. Likewise, NIST SP 800-218r1 / SSDF 1.2 is an Initial Public Draft, while NIST SP 800-218 / SSDF 1.1 remains the finalized publication baseline.
- SPDX 3.1 is still a release candidate as of this decision. The workflow emits SPDX JSON through the pinned Anchore/Syft action; later promotion policy must explicitly validate the emitted schema/profile rather than inferring compliance from a filename.

## Selected boundary

`.github/workflows/release.yml` is a repository-specific release-evidence workflow rather than a copied organization-wide release implementation. Pull requests exercise the build/evidence path without publishing attestations. A manual stable-evidence dispatch is accepted only from protected `main` with a canonical `MAJOR.MINOR.PATCH` value equal to `Cargo.toml`.

The workflow repeats Wardnet's repository quality contract, builds the locked Rust release binary, creates a deterministic tar archive, records SHA-256 digests for source/build inputs and outputs, emits a machine-readable release manifest, generates an SPDX JSON SBOM, and uploads the complete evidence bundle. Protected-main dispatch additionally creates GitHub/Sigstore-backed build-provenance and SBOM attestations for the exact package archive.

The workflow deliberately has `contents: read`. It does not create a tag, GitHub Release, package-registry object, container image, deployment, or promotion. That prevents a partial evidence foundation from becoming accidental production publication before issue #84's prerequisites and rollback/promotion acceptance are complete.

## RED → GREEN evidence

`tests/release_evidence_contract.rs` is the executable architecture fence. Its first commit requires a release workflow with protected-main binding, exact quality gates, locked build metadata, SHA-256 evidence, SPDX generation, attestation, immutable action pins, and non-persisted checkout credentials. That RED precedes the workflow implementation. The GREEN candidate adds only the release-evidence workflow and supporting documentation; it does not weaken any existing gate.

Exact-head hosted execution remains authoritative. Source inspection or a predecessor run is not GREEN. The candidate remains dependent on the Rust toolchain prerequisite represented by PR #77 and must be non-force restacked or retargeted when that prerequisite reaches protected `main`.

## Risks and follow-up

The binary-only SBOM is not yet the final container-filesystem SBOM required by #84. The workflow also does not prove bit-for-bit reproducibility across independent builders, image-digest promotion, Sigstore verification at admission, Kubernetes deployment-by-digest, attack-path execution, database migration compatibility, canary criteria, or measured rollback. Those omissions remain fail-closed release gaps rather than implied future behavior.

Before #84 can close, extend the same source identity into the final OCI image and release manifest, verify attestations before promotion, exercise production-shaped deployment/attack/rollback paths, preserve all evidence independently of ephemeral workflow retention, and publish an immutable release only after the exact protected candidate satisfies the then-live ruleset and security contract.

## Traceability

GitHub. (2026). *Using artifact attestations to establish provenance for builds*. GitHub Docs. https://docs.github.com/en/actions/how-tos/secure-your-work/use-artifact-attestations/use-artifact-attestations

National Institute of Standards and Technology. (2022). *Secure software development framework (SSDF) version 1.1: Recommendations for mitigating the risk of software vulnerabilities* (NIST SP 800-218). https://doi.org/10.6028/NIST.SP.800-218

National Institute of Standards and Technology. (2025). *Secure software development framework (SSDF) version 1.2: Recommendations for mitigating the risk of software vulnerabilities* (Initial Public Draft, NIST SP 800-218r1). https://csrc.nist.gov/pubs/sp/800/218/r1/ipd

SPDX Workgroup. (2024). *SPDX specification 3.0.1*. Linux Foundation. https://spdx.github.io/spdx-spec/v3.0.1/

Supply-chain Levels for Software Artifacts. (2025). *SLSA version 1.1*. https://slsa.dev/spec/v1.1/
Loading
Loading