ci: GitHub Actions CI/CD, supply chain hardening, reproducible builds

- .github/workflows/ci.yml — 17 jobs: fmt, clippy, test, e2e, enospc, e2e-resolve,
  build-release (gnu+musl), smoke-test, reproducibility-check (gnu+musl),
  cross-run-reproducibility (gnu+musl), lockfile-check, cargo-deny, ci-contract
- .github/workflows/release.yml — 4 jobs: build, sign (cosign OIDC), verify, publish
- .github/workflows/supply-chain-test.yml — 11 adversarial jobs: build-and-sign,
  verify-signatures, tamper-binary, tamper-sbom, tamper-signature-removal,
  adversarial-env-injection, adversarial-artifact-tampering, adversarial-build-script,
  adversarial-credential-injection, adversarial-rustflags-bypass, verify-docs-executable
- .github/actions/karapace-build/action.yml — reusable build action
- .cargo/config.toml — SOURCE_DATE_EPOCH=0, local path remapping for reproducibility
- CI_CONTRACT.md — required jobs list enforced by ci-contract gate job
- scripts/generate-sbom.sh — CycloneDX SBOM generation
- CARGO_INCREMENTAL=0 globally, cargo clean before all release builds
- Cosign keyless signing with GitHub Actions OIDC
- 32 total CI jobs across 3 workflows
This commit is contained in:
Marco Allegretti 2026-02-22 18:39:00 +01:00
parent 5306963cce
commit bb03d3adad
7 changed files with 1806 additions and 0 deletions

21
.cargo/config.toml Normal file
View file

@ -0,0 +1,21 @@
# Supply-chain hardening: deterministic builds
#
# Path remapping eliminates local filesystem paths from binaries.
# CI sets RUSTFLAGS env var which OVERRIDES build.rustflags below.
# SOURCE_DATE_EPOCH=0 ensures no build timestamps leak.
#
# NOTE: Local dev builds use build.rustflags for path remapping.
# CI builds use the RUSTFLAGS env var instead (takes precedence).
# This means local builds get local remapping, CI gets CI remapping.
[env]
SOURCE_DATE_EPOCH = "0"
[build]
# Remap common local paths in release builds.
# These are best-effort for local dev — CI RUSTFLAGS override this entirely.
# The $HOME/.cargo/registry/src prefix covers most dependency crate paths.
rustflags = [
"--remap-path-prefix", "/home/lateuf/.cargo/registry/src=crate",
"--remap-path-prefix", "/home/lateuf/Projects/Karapace=src",
]

View file

@ -0,0 +1,62 @@
name: 'Karapace Build'
description: 'Build a Karapace environment from a manifest file'
inputs:
manifest:
description: 'Path to the karapace.toml manifest file'
required: true
default: 'karapace.toml'
name:
description: 'Optional name for the environment'
required: false
karapace-version:
description: 'Version of Karapace to install (or "latest")'
required: false
default: 'latest'
store-path:
description: 'Path to the Karapace store directory'
required: false
default: '/tmp/karapace-store'
outputs:
env-id:
description: 'The environment ID of the built environment'
value: ${{ steps.build.outputs.env_id }}
short-id:
description: 'The short ID of the built environment'
value: ${{ steps.build.outputs.short_id }}
runs:
using: 'composite'
steps:
- name: Install prerequisites
shell: bash
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq fuse-overlayfs curl
sudo sysctl -w kernel.unprivileged_userns_clone=1 || true
- name: Install Karapace
shell: bash
run: |
if [ "${{ inputs.karapace-version }}" = "latest" ]; then
cargo install --path crates/karapace-cli --root /usr/local 2>/dev/null || \
cargo install karapace-cli --root /usr/local 2>/dev/null || \
echo "::warning::Could not install karapace; using local build"
fi
- name: Build environment
id: build
shell: bash
run: |
ARGS="--store ${{ inputs.store-path }} --json"
if [ -n "${{ inputs.name }}" ]; then
ARGS="$ARGS --name ${{ inputs.name }}"
fi
OUTPUT=$(karapace $ARGS build "${{ inputs.manifest }}" 2>&1) || {
echo "::error::Karapace build failed"
echo "$OUTPUT"
exit 1
}
echo "$OUTPUT"
ENV_ID=$(echo "$OUTPUT" | jq -r '.env_id // empty')
SHORT_ID=$(echo "$OUTPUT" | jq -r '.short_id // empty')
echo "env_id=$ENV_ID" >> "$GITHUB_OUTPUT"
echo "short_id=$SHORT_ID" >> "$GITHUB_OUTPUT"

559
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,559 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: >-
-D warnings
--remap-path-prefix /home/runner/work=src
--remap-path-prefix /home/runner/.cargo/registry/src=crate
--remap-path-prefix /home/runner/.rustup=rustup
RUST_TOOLCHAIN: "1.82"
SOURCE_DATE_EPOCH: "0"
CARGO_INCREMENTAL: "0"
jobs:
fmt:
name: Format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
components: rustfmt
- run: cargo fmt --all --check
clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
components: clippy
- uses: Swatinem/rust-cache@v2
- run: cargo clippy --workspace --all-targets -- -D warnings
test:
name: Test (${{ matrix.os }})
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu
runner: ubuntu-latest
- os: fedora
runner: ubuntu-latest
container: fedora:latest
- os: opensuse
runner: ubuntu-latest
container: opensuse/tumbleweed:latest
container: ${{ matrix.container || '' }}
steps:
- uses: actions/checkout@v4
- name: Install build deps (Fedora)
if: matrix.os == 'fedora'
run: dnf install -y gcc make curl
- name: Install build deps (openSUSE)
if: matrix.os == 'opensuse'
run: zypper install -y gcc make curl gzip tar
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- uses: Swatinem/rust-cache@v2
- run: cargo test --workspace
e2e:
name: E2E Tests
runs-on: ubuntu-latest
needs: [test]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- uses: Swatinem/rust-cache@v2
- name: Install prerequisites
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq fuse-overlayfs curl crun
sudo sysctl -w kernel.unprivileged_userns_clone=1 || true
- name: Verify prerequisites
run: |
echo "--- Prerequisite verification ---"
unshare --version
fuse-overlayfs --version || echo "fuse-overlayfs: $(which fuse-overlayfs)"
curl --version | head -1
crun --version | head -1
echo "User namespace test:"
unshare --user --map-root-user --fork true && echo "OK" || { echo "FAIL"; exit 1; }
echo "--- All prerequisites verified ---"
- name: Run E2E tests
run: cargo test --test e2e -- --ignored --test-threads=1
- name: Check for mount leaks
if: always()
run: |
if grep -q fuse-overlayfs /proc/mounts; then
echo "FATAL: stale fuse-overlayfs mounts detected"
grep fuse-overlayfs /proc/mounts
exit 1
fi
enospc:
name: ENOSPC Tests
runs-on: ubuntu-latest
needs: [test]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- uses: Swatinem/rust-cache@v2
- name: Run ENOSPC tests (requires sudo for tmpfs)
run: sudo -E cargo test --test enospc -- --ignored --test-threads=1
e2e-resolve:
name: E2E Resolver (${{ matrix.os }})
runs-on: ubuntu-latest
needs: [test]
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu
container: ubuntu:24.04
setup: "apt-get update -qq && apt-get install -y -qq build-essential curl fuse-overlayfs crun"
- os: fedora
container: fedora:latest
setup: "dnf install -y gcc make curl fuse-overlayfs fuse3 crun"
- os: opensuse
container: opensuse/tumbleweed:latest
setup: "zypper install -y gcc make curl fuse-overlayfs gzip tar crun"
container: ${{ matrix.container }}
steps:
- uses: actions/checkout@v4
- name: Install prerequisites
run: ${{ matrix.setup }}
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- uses: Swatinem/rust-cache@v2
- name: Enable user namespaces
run: sysctl -w kernel.unprivileged_userns_clone=1 || true
- name: Verify prerequisites
run: |
echo "--- Prerequisite verification (${{ matrix.os }}) ---"
which unshare && unshare --version || true
which fuse-overlayfs && (fuse-overlayfs --version || true)
curl --version | head -1
unshare --user --map-root-user --fork true && echo "Namespaces: OK" || { echo "Namespaces: FAIL"; exit 1; }
echo "--- All prerequisites verified ---"
- name: Run resolver E2E tests
run: cargo test --test e2e e2e_resolve -- --ignored --test-threads=1
build-release:
name: Release Build (${{ matrix.target }})
runs-on: ubuntu-latest
needs: [fmt, clippy, test]
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
artifact: karapace-linux-x86_64-gnu
- target: x86_64-unknown-linux-musl
artifact: karapace-linux-x86_64-musl
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.target }}
- name: Install musl-tools
if: contains(matrix.target, 'musl')
run: sudo apt-get update -qq && sudo apt-get install -y -qq musl-tools
- name: Clean before release build
run: cargo clean
- run: cargo build --release --workspace --target ${{ matrix.target }}
- name: Install cargo-cyclonedx
run: cargo install cargo-cyclonedx@0.5.5 --locked
- name: Generate SBOM
run: cargo cyclonedx --format json --all
- name: Compute and verify checksums
run: |
cd target/${{ matrix.target }}/release
sha256sum karapace karapace-dbus > SHA256SUMS
sha256sum -c SHA256SUMS
echo "SHA256 checksum verification: OK"
- name: Validate SBOM is valid JSON with components
run: |
SBOM=$(find crates/karapace-cli -name '*.cdx.json' | head -1)
python3 -c "
import json, sys
with open('$SBOM') as f:
bom = json.load(f)
assert 'components' in bom, 'SBOM missing components key'
assert len(bom['components']) > 0, 'SBOM has zero components'
print(f'SBOM valid: {len(bom[\"components\"])} components')
"
- name: Verify static linking (musl only)
if: contains(matrix.target, 'musl')
run: |
set -euo pipefail
if ldd target/${{ matrix.target }}/release/karapace 2>&1 | grep -q 'not a dynamic executable'; then
echo "PASS: karapace is statically linked"
else
echo "FAIL: karapace is NOT statically linked"
ldd target/${{ matrix.target }}/release/karapace
exit 1
fi
if ldd target/${{ matrix.target }}/release/karapace-dbus 2>&1 | grep -q 'not a dynamic executable'; then
echo "PASS: karapace-dbus is statically linked"
else
echo "FAIL: karapace-dbus is NOT statically linked"
ldd target/${{ matrix.target }}/release/karapace-dbus
exit 1
fi
- uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}
path: |
target/${{ matrix.target }}/release/karapace
target/${{ matrix.target }}/release/karapace-dbus
target/${{ matrix.target }}/release/SHA256SUMS
smoke-test:
name: Smoke Test Release (${{ matrix.target }})
runs-on: ubuntu-latest
needs: [build-release]
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
artifact: karapace-linux-x86_64-gnu
binary_path: target/x86_64-unknown-linux-gnu/release
- target: x86_64-unknown-linux-musl
artifact: karapace-linux-x86_64-musl
binary_path: target/x86_64-unknown-linux-musl/release
steps:
- uses: actions/download-artifact@v4
with:
name: ${{ matrix.artifact }}
- name: Verify binary
run: |
chmod +x ${{ matrix.binary_path }}/karapace
${{ matrix.binary_path }}/karapace --version
${{ matrix.binary_path }}/karapace doctor
${{ matrix.binary_path }}/karapace migrate
reproducibility-check:
name: Reproducibility Check (same-run)
runs-on: ubuntu-latest
needs: [fmt, clippy, test]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- name: Build A
run: cargo build --release -p karapace-cli -p karapace-dbus
- name: Record hashes A
run: |
sha256sum target/release/karapace target/release/karapace-dbus > /tmp/hashes-a.txt
cat /tmp/hashes-a.txt
- name: Clean and rebuild B
run: |
cargo clean
cargo build --release -p karapace-cli -p karapace-dbus
- name: Record hashes B
run: |
sha256sum target/release/karapace target/release/karapace-dbus > /tmp/hashes-b.txt
cat /tmp/hashes-b.txt
- name: Compare A vs B
run: |
set -euo pipefail
echo "=== Build A ==="
cat /tmp/hashes-a.txt
echo "=== Build B ==="
cat /tmp/hashes-b.txt
HASH_A_CLI=$(awk '/karapace$/{print $1}' /tmp/hashes-a.txt)
HASH_B_CLI=$(awk '/karapace$/{print $1}' /tmp/hashes-b.txt)
HASH_A_DBUS=$(awk '/karapace-dbus$/{print $1}' /tmp/hashes-a.txt)
HASH_B_DBUS=$(awk '/karapace-dbus$/{print $1}' /tmp/hashes-b.txt)
if [ "$HASH_A_CLI" != "$HASH_B_CLI" ]; then
echo "FATAL: karapace binary not reproducible"
echo " A: $HASH_A_CLI"
echo " B: $HASH_B_CLI"
exit 1
fi
if [ "$HASH_A_DBUS" != "$HASH_B_DBUS" ]; then
echo "FATAL: karapace-dbus binary not reproducible"
echo " A: $HASH_A_DBUS"
echo " B: $HASH_B_DBUS"
exit 1
fi
echo "Reproducibility check PASSED: both binaries byte-identical across builds"
cross-run-reproducibility:
name: Cross-Run Reproducibility (${{ matrix.label }})
runs-on: ${{ matrix.runner }}
needs: [fmt, clippy, test]
strategy:
fail-fast: false
matrix:
include:
- label: ubuntu-latest
runner: ubuntu-latest
- label: ubuntu-22.04
runner: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- name: Build release
run: cargo build --release -p karapace-cli -p karapace-dbus
- name: Compute hashes
run: |
sha256sum target/release/karapace target/release/karapace-dbus > hashes.txt
cat hashes.txt
- uses: actions/upload-artifact@v4
with:
name: repro-hashes-${{ matrix.label }}
path: hashes.txt
- uses: actions/upload-artifact@v4
with:
name: repro-binary-${{ matrix.label }}
path: |
target/release/karapace
target/release/karapace-dbus
verify-cross-reproducibility:
name: Verify Cross-Run Reproducibility
runs-on: ubuntu-latest
needs: [cross-run-reproducibility]
steps:
- uses: actions/download-artifact@v4
with:
name: repro-hashes-ubuntu-latest
path: build-latest
- uses: actions/download-artifact@v4
with:
name: repro-hashes-ubuntu-22.04
path: build-2204
- uses: actions/download-artifact@v4
with:
name: repro-binary-ubuntu-latest
path: bin-latest
- uses: actions/download-artifact@v4
with:
name: repro-binary-ubuntu-22.04
path: bin-2204
- name: Compare cross-run hashes
run: |
set -euo pipefail
echo "=== ubuntu-latest ==="
cat build-latest/hashes.txt
echo "=== ubuntu-22.04 ==="
cat build-2204/hashes.txt
if diff -u build-latest/hashes.txt build-2204/hashes.txt; then
echo "Cross-run reproducibility PASSED: builds are byte-identical"
else
echo "WARNING: Cross-run builds differ — generating diffoscope report"
sudo apt-get update -qq && sudo apt-get install -y -qq diffoscope
diffoscope bin-latest/target/release/karapace bin-2204/target/release/karapace \
--text /tmp/diffoscope-karapace.txt || true
diffoscope bin-latest/target/release/karapace-dbus bin-2204/target/release/karapace-dbus \
--text /tmp/diffoscope-dbus.txt || true
echo "=== Diffoscope report (karapace) ==="
cat /tmp/diffoscope-karapace.txt 2>/dev/null || echo "(empty)"
echo "=== Diffoscope report (karapace-dbus) ==="
cat /tmp/diffoscope-dbus.txt 2>/dev/null || echo "(empty)"
echo "FATAL: Cross-run builds are NOT reproducible"
exit 1
fi
reproducibility-check-musl:
name: Reproducibility Check (musl, same-run)
runs-on: ubuntu-latest
needs: [fmt, clippy, test]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
targets: x86_64-unknown-linux-musl
- name: Install musl-tools
run: sudo apt-get update -qq && sudo apt-get install -y -qq musl-tools
- name: Build A (musl)
run: |
cargo clean
cargo build --release -p karapace-cli -p karapace-dbus --target x86_64-unknown-linux-musl
- name: Record hashes A
run: |
sha256sum target/x86_64-unknown-linux-musl/release/karapace target/x86_64-unknown-linux-musl/release/karapace-dbus > /tmp/hashes-a.txt
cat /tmp/hashes-a.txt
- name: Clean and rebuild B
run: |
cargo clean
cargo build --release -p karapace-cli -p karapace-dbus --target x86_64-unknown-linux-musl
- name: Record hashes B
run: |
sha256sum target/x86_64-unknown-linux-musl/release/karapace target/x86_64-unknown-linux-musl/release/karapace-dbus > /tmp/hashes-b.txt
cat /tmp/hashes-b.txt
- name: Compare A vs B
run: |
set -euo pipefail
echo "=== Build A (musl) ==="
cat /tmp/hashes-a.txt
echo "=== Build B (musl) ==="
cat /tmp/hashes-b.txt
HASH_A_CLI=$(awk '/karapace$/{print $1}' /tmp/hashes-a.txt)
HASH_B_CLI=$(awk '/karapace$/{print $1}' /tmp/hashes-b.txt)
HASH_A_DBUS=$(awk '/karapace-dbus$/{print $1}' /tmp/hashes-a.txt)
HASH_B_DBUS=$(awk '/karapace-dbus$/{print $1}' /tmp/hashes-b.txt)
if [ "$HASH_A_CLI" != "$HASH_B_CLI" ]; then
echo "FATAL: karapace musl binary not reproducible"
echo " A: $HASH_A_CLI"
echo " B: $HASH_B_CLI"
exit 1
fi
if [ "$HASH_A_DBUS" != "$HASH_B_DBUS" ]; then
echo "FATAL: karapace-dbus musl binary not reproducible"
echo " A: $HASH_A_DBUS"
echo " B: $HASH_B_DBUS"
exit 1
fi
echo "Musl reproducibility check PASSED: both binaries byte-identical across builds"
cross-run-reproducibility-musl:
name: Cross-Run Reproducibility musl (${{ matrix.label }})
runs-on: ${{ matrix.runner }}
needs: [fmt, clippy, test]
strategy:
fail-fast: false
matrix:
include:
- label: ubuntu-latest
runner: ubuntu-latest
- label: ubuntu-22.04
runner: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
targets: x86_64-unknown-linux-musl
- name: Install musl-tools
run: sudo apt-get update -qq && sudo apt-get install -y -qq musl-tools
- name: Build release (musl)
run: |
cargo clean
cargo build --release -p karapace-cli -p karapace-dbus --target x86_64-unknown-linux-musl
- name: Compute hashes
run: |
sha256sum target/x86_64-unknown-linux-musl/release/karapace target/x86_64-unknown-linux-musl/release/karapace-dbus > hashes.txt
cat hashes.txt
- uses: actions/upload-artifact@v4
with:
name: repro-musl-hashes-${{ matrix.label }}
path: hashes.txt
verify-cross-reproducibility-musl:
name: Verify Cross-Run Reproducibility (musl)
runs-on: ubuntu-latest
needs: [cross-run-reproducibility-musl]
steps:
- uses: actions/download-artifact@v4
with:
name: repro-musl-hashes-ubuntu-latest
path: build-latest
- uses: actions/download-artifact@v4
with:
name: repro-musl-hashes-ubuntu-22.04
path: build-2204
- name: Compare musl cross-run hashes
run: |
set -euo pipefail
echo "=== ubuntu-latest (musl) ==="
cat build-latest/hashes.txt
echo "=== ubuntu-22.04 (musl) ==="
cat build-2204/hashes.txt
if diff -u build-latest/hashes.txt build-2204/hashes.txt; then
echo "Musl cross-run reproducibility PASSED: builds are byte-identical"
else
echo "WARNING: Musl cross-run builds differ"
echo "NOTE: Musl static builds should be runner-independent"
echo "FATAL: Musl cross-run builds are NOT reproducible"
exit 1
fi
lockfile-check:
name: Lockfile Integrity
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- name: Verify Cargo.lock is up-to-date
run: |
cp Cargo.lock /tmp/Cargo.lock.before
cargo update
if ! diff -u /tmp/Cargo.lock.before Cargo.lock; then
echo "FATAL: Cargo.lock is not up-to-date or has drifted"
echo "Run 'cargo update' locally and commit the result"
exit 1
fi
echo "Cargo.lock integrity: OK (no drift detected)"
cargo-deny:
name: Dependency Policy (cargo-deny)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v2
with:
command: check
arguments: --all-features
ci-contract:
name: CI Contract
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Verify all required jobs exist
run: |
set -euo pipefail
REQUIRED_JOBS="fmt clippy test e2e enospc e2e-resolve build-release smoke-test ci-contract reproducibility-check reproducibility-check-musl cross-run-reproducibility verify-cross-reproducibility cross-run-reproducibility-musl verify-cross-reproducibility-musl lockfile-check cargo-deny"
MISSING=""
for job in $REQUIRED_JOBS; do
if ! grep -qE "^ ${job}:" .github/workflows/ci.yml; then
MISSING="$MISSING $job"
fi
done
if [ -n "$MISSING" ]; then
echo "FATAL: Missing required CI jobs:$MISSING"
echo "See CI_CONTRACT.md for the full list of required jobs."
exit 1
fi
echo "All required CI jobs present: $REQUIRED_JOBS"
- name: Verify CI_CONTRACT.md exists
run: |
if [ ! -f CI_CONTRACT.md ]; then
echo "FATAL: CI_CONTRACT.md missing"
exit 1
fi
echo "CI_CONTRACT.md present"

270
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,270 @@
name: Release
on:
push:
tags: ['v*']
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: >-
-D warnings
--remap-path-prefix /home/runner/work=src
--remap-path-prefix /home/runner/.cargo/registry/src=crate
--remap-path-prefix /home/runner/.rustup=rustup
RUST_TOOLCHAIN: "1.82"
SOURCE_DATE_EPOCH: "0"
CARGO_INCREMENTAL: "0"
permissions:
contents: read
jobs:
build:
name: Build Release (${{ matrix.target }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
- target: x86_64-unknown-linux-musl
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.target }}
- name: Install musl-tools
if: contains(matrix.target, 'musl')
run: sudo apt-get update -qq && sudo apt-get install -y -qq musl-tools
- name: Clean before release build
run: cargo clean
- name: Build release binaries
run: cargo build --release --workspace --target ${{ matrix.target }}
- name: Install cargo-cyclonedx
run: cargo install cargo-cyclonedx@0.5.5 --locked
- name: Generate SBOM
run: cargo cyclonedx --format json --all
- name: Compute and verify checksums
run: |
cd target/${{ matrix.target }}/release
sha256sum karapace karapace-dbus > SHA256SUMS
sha256sum -c SHA256SUMS
- name: Validate SBOM
run: |
SBOM=$(find crates/karapace-cli -name '*.cdx.json' | head -1)
python3 -c "
import json
with open('$SBOM') as f:
bom = json.load(f)
assert 'components' in bom and len(bom['components']) > 0
print(f'SBOM valid: {len(bom[\"components\"])} components')
"
- name: Verify static linking (musl only)
if: contains(matrix.target, 'musl')
run: |
set -euo pipefail
for bin in karapace karapace-dbus; do
if ldd target/${{ matrix.target }}/release/$bin 2>&1 | grep -q 'not a dynamic executable'; then
echo "PASS: $bin is statically linked"
else
echo "FAIL: $bin is NOT statically linked"
ldd target/${{ matrix.target }}/release/$bin
exit 1
fi
done
- uses: actions/upload-artifact@v4
with:
name: release-${{ matrix.target }}
path: |
target/${{ matrix.target }}/release/karapace
target/${{ matrix.target }}/release/karapace-dbus
target/${{ matrix.target }}/release/SHA256SUMS
sign:
name: Sign Release (${{ matrix.target }})
runs-on: ubuntu-latest
needs: [build]
permissions:
id-token: write
strategy:
matrix:
include:
- target: x86_64-unknown-linux-gnu
suffix: gnu
- target: x86_64-unknown-linux-musl
suffix: musl
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: release-${{ matrix.target }}
path: artifacts
- uses: sigstore/cosign-installer@v3
- name: Sign binaries
run: |
cosign sign-blob --yes \
artifacts/target/${{ matrix.target }}/release/karapace \
--output-signature artifacts/karapace-${{ matrix.suffix }}.sig \
--output-certificate artifacts/karapace-${{ matrix.suffix }}.crt
cosign sign-blob --yes \
artifacts/target/${{ matrix.target }}/release/karapace-dbus \
--output-signature artifacts/karapace-dbus-${{ matrix.suffix }}.sig \
--output-certificate artifacts/karapace-dbus-${{ matrix.suffix }}.crt
- name: Generate and sign provenance attestation
run: |
cat > artifacts/provenance-${{ matrix.suffix }}.json << EOF
{
"_type": "https://in-toto.io/Statement/v0.1",
"predicateType": "https://slsa.dev/provenance/v0.2",
"subject": [
{
"name": "karapace-${{ matrix.suffix }}",
"digest": {
"sha256": "$(sha256sum artifacts/target/${{ matrix.target }}/release/karapace | awk '{print $1}')"
}
},
{
"name": "karapace-dbus-${{ matrix.suffix }}",
"digest": {
"sha256": "$(sha256sum artifacts/target/${{ matrix.target }}/release/karapace-dbus | awk '{print $1}')"
}
}
],
"predicate": {
"builder": {
"id": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
},
"buildType": "https://github.com/actions/runner",
"invocation": {
"configSource": {
"uri": "git+https://github.com/${{ github.repository }}@${{ github.ref }}",
"digest": {
"sha1": "${{ github.sha }}"
},
"entryPoint": ".github/workflows/release.yml"
}
},
"metadata": {
"buildInvocationId": "${{ github.run_id }}",
"completeness": {
"parameters": true,
"environment": true,
"materials": true
},
"reproducible": true
},
"materials": [
{
"uri": "git+https://github.com/${{ github.repository }}@${{ github.ref }}",
"digest": {
"sha1": "${{ github.sha }}"
}
}
]
}
}
EOF
cosign sign-blob --yes \
artifacts/provenance-${{ matrix.suffix }}.json \
--output-signature artifacts/provenance-${{ matrix.suffix }}.json.sig \
--output-certificate artifacts/provenance-${{ matrix.suffix }}.json.crt
- uses: actions/upload-artifact@v4
with:
name: signatures-${{ matrix.suffix }}
path: |
artifacts/*.sig
artifacts/*.crt
artifacts/provenance-${{ matrix.suffix }}.json
verify:
name: Verify Release (${{ matrix.target }})
runs-on: ubuntu-latest
needs: [sign]
strategy:
matrix:
include:
- target: x86_64-unknown-linux-gnu
suffix: gnu
- target: x86_64-unknown-linux-musl
suffix: musl
steps:
- uses: actions/download-artifact@v4
with:
name: release-${{ matrix.target }}
path: release
- uses: actions/download-artifact@v4
with:
name: signatures-${{ matrix.suffix }}
path: sigs
- uses: sigstore/cosign-installer@v3
- name: Verify checksums
run: |
cd release/target/${{ matrix.target }}/release
sha256sum -c SHA256SUMS
- name: Verify binary signatures
run: |
cosign verify-blob release/target/${{ matrix.target }}/release/karapace \
--signature sigs/artifacts/karapace-${{ matrix.suffix }}.sig \
--certificate sigs/artifacts/karapace-${{ matrix.suffix }}.crt \
--certificate-identity-regexp 'https://github.com/marcoallegretti/karapace' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
cosign verify-blob release/target/${{ matrix.target }}/release/karapace-dbus \
--signature sigs/artifacts/karapace-dbus-${{ matrix.suffix }}.sig \
--certificate sigs/artifacts/karapace-dbus-${{ matrix.suffix }}.crt \
--certificate-identity-regexp 'https://github.com/marcoallegretti/karapace' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
- name: Verify provenance attestation
run: |
cosign verify-blob sigs/artifacts/provenance-${{ matrix.suffix }}.json \
--signature sigs/artifacts/provenance-${{ matrix.suffix }}.json.sig \
--certificate sigs/artifacts/provenance-${{ matrix.suffix }}.json.crt \
--certificate-identity-regexp 'https://github.com/marcoallegretti/karapace' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
publish:
name: Publish GitHub Release
runs-on: ubuntu-latest
needs: [verify]
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
with:
name: release-x86_64-unknown-linux-gnu
path: release-gnu
- uses: actions/download-artifact@v4
with:
name: release-x86_64-unknown-linux-musl
path: release-musl
- uses: actions/download-artifact@v4
with:
name: signatures-gnu
path: sigs-gnu
- uses: actions/download-artifact@v4
with:
name: signatures-musl
path: sigs-musl
- name: Stage release files
run: |
mkdir -p dist
# GNU binaries (renamed with -gnu suffix)
cp release-gnu/target/x86_64-unknown-linux-gnu/release/karapace dist/karapace-linux-x86_64-gnu
cp release-gnu/target/x86_64-unknown-linux-gnu/release/karapace-dbus dist/karapace-dbus-linux-x86_64-gnu
cp release-gnu/target/x86_64-unknown-linux-gnu/release/SHA256SUMS dist/SHA256SUMS-gnu
# Musl binaries (renamed with -musl suffix)
cp release-musl/target/x86_64-unknown-linux-musl/release/karapace dist/karapace-linux-x86_64-musl
cp release-musl/target/x86_64-unknown-linux-musl/release/karapace-dbus dist/karapace-dbus-linux-x86_64-musl
cp release-musl/target/x86_64-unknown-linux-musl/release/SHA256SUMS dist/SHA256SUMS-musl
# Signatures
cp sigs-gnu/artifacts/*.sig sigs-gnu/artifacts/*.crt dist/
cp sigs-gnu/artifacts/provenance-gnu.json dist/
cp sigs-musl/artifacts/*.sig sigs-musl/artifacts/*.crt dist/
cp sigs-musl/artifacts/provenance-musl.json dist/
- name: Create release
uses: softprops/action-gh-release@v2
with:
files: dist/*

794
.github/workflows/supply-chain-test.yml vendored Normal file
View file

@ -0,0 +1,794 @@
name: Supply Chain Test
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: >-
-D warnings
--remap-path-prefix /home/runner/work=src
--remap-path-prefix /home/runner/.cargo/registry/src=crate
--remap-path-prefix /home/runner/.rustup=rustup
RUST_TOOLCHAIN: "1.82"
SOURCE_DATE_EPOCH: "0"
CARGO_INCREMENTAL: "0"
permissions:
contents: read
jobs:
build-and-sign:
name: Build, Sign & Attest
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
attestations: write
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- uses: Swatinem/rust-cache@v2
- name: Clean before release build
run: cargo clean
- name: Build release binaries
run: cargo build --release -p karapace-cli -p karapace-dbus
- name: Install cargo-cyclonedx
run: cargo install cargo-cyclonedx@0.5.5 --locked
- name: Generate SBOM
run: cargo cyclonedx --format json --output-prefix karapace
- name: Compute checksums
run: |
cd target/release
sha256sum karapace karapace-dbus > SHA256SUMS
sha256sum -c SHA256SUMS
echo "Checksums verified"
- name: Verify no local paths in binary
run: |
set -euo pipefail
if strings target/release/karapace | grep -qE '/home/runner'; then
echo "FATAL: binary leaks CI runner paths"
strings target/release/karapace | grep '/home/runner' | head -5
exit 1
fi
echo "No CI runner path leakage detected"
# --- Cosign signing ---
- uses: sigstore/cosign-installer@v3
- name: Sign karapace binary
run: |
cosign sign-blob --yes \
target/release/karapace \
--output-signature target/release/karapace.sig \
--output-certificate target/release/karapace.crt
- name: Sign karapace-dbus binary
run: |
cosign sign-blob --yes \
target/release/karapace-dbus \
--output-signature target/release/karapace-dbus.sig \
--output-certificate target/release/karapace-dbus.crt
- name: Sign SBOM
run: |
cosign sign-blob --yes \
karapace_bom.json \
--output-signature karapace_bom.json.sig \
--output-certificate karapace_bom.json.crt
# --- Provenance attestation ---
- name: Generate provenance attestation
run: |
cat > /tmp/provenance.json << EOF
{
"_type": "https://in-toto.io/Statement/v0.1",
"predicateType": "https://slsa.dev/provenance/v0.2",
"subject": [
{
"name": "karapace",
"digest": {
"sha256": "$(sha256sum target/release/karapace | awk '{print $1}')"
}
},
{
"name": "karapace-dbus",
"digest": {
"sha256": "$(sha256sum target/release/karapace-dbus | awk '{print $1}')"
}
}
],
"predicate": {
"builder": {
"id": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
},
"buildType": "https://github.com/actions/runner",
"invocation": {
"configSource": {
"uri": "git+https://github.com/${{ github.repository }}@${{ github.ref }}",
"digest": {
"sha1": "${{ github.sha }}"
},
"entryPoint": "${{ github.workflow_ref }}"
}
},
"metadata": {
"buildInvocationId": "${{ github.run_id }}",
"completeness": {
"parameters": true,
"environment": true,
"materials": true
},
"reproducible": true
},
"materials": [
{
"uri": "git+https://github.com/${{ github.repository }}@${{ github.ref }}",
"digest": {
"sha1": "${{ github.sha }}"
}
}
]
}
}
EOF
echo "Provenance attestation generated"
python3 -c "import json; json.load(open('/tmp/provenance.json')); print('Valid JSON')"
- name: Sign provenance attestation
run: |
cosign sign-blob --yes \
/tmp/provenance.json \
--output-signature /tmp/provenance.json.sig \
--output-certificate /tmp/provenance.json.crt
- name: Upload all artifacts
uses: actions/upload-artifact@v4
with:
name: supply-chain-artifacts
path: |
target/release/karapace
target/release/karapace-dbus
target/release/SHA256SUMS
target/release/karapace.sig
target/release/karapace.crt
target/release/karapace-dbus.sig
target/release/karapace-dbus.crt
karapace_bom.json
karapace_bom.json.sig
karapace_bom.json.crt
/tmp/provenance.json
/tmp/provenance.json.sig
/tmp/provenance.json.crt
verify-signatures:
name: Verify Signatures & Provenance
runs-on: ubuntu-latest
needs: [build-and-sign]
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: supply-chain-artifacts
path: artifacts
- uses: sigstore/cosign-installer@v3
# --- SHA256 verification ---
- name: Verify SHA256 checksums
run: |
cd artifacts/target/release
sha256sum -c SHA256SUMS
echo "SHA256 verification: PASSED"
# --- Cosign verification (karapace) ---
- name: Verify karapace signature
run: |
cosign verify-blob \
artifacts/target/release/karapace \
--signature artifacts/target/release/karapace.sig \
--certificate artifacts/target/release/karapace.crt \
--certificate-identity-regexp 'https://github.com/marcoallegretti/karapace' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
echo "karapace signature: VERIFIED"
# --- Cosign verification (karapace-dbus) ---
- name: Verify karapace-dbus signature
run: |
cosign verify-blob \
artifacts/target/release/karapace-dbus \
--signature artifacts/target/release/karapace-dbus.sig \
--certificate artifacts/target/release/karapace-dbus.crt \
--certificate-identity-regexp 'https://github.com/marcoallegretti/karapace' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
echo "karapace-dbus signature: VERIFIED"
# --- SBOM signature verification ---
- name: Verify SBOM signature
run: |
cosign verify-blob \
artifacts/karapace_bom.json \
--signature artifacts/karapace_bom.json.sig \
--certificate artifacts/karapace_bom.json.crt \
--certificate-identity-regexp 'https://github.com/marcoallegretti/karapace' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
echo "SBOM signature: VERIFIED"
# --- Provenance verification ---
- name: Verify provenance attestation signature
run: |
cosign verify-blob \
artifacts/tmp/provenance.json \
--signature artifacts/tmp/provenance.json.sig \
--certificate artifacts/tmp/provenance.json.crt \
--certificate-identity-regexp 'https://github.com/marcoallegretti/karapace' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
echo "Provenance attestation signature: VERIFIED"
# --- Provenance content verification ---
- name: Verify provenance content matches this build
run: |
set -euo pipefail
PROV=$(cat artifacts/tmp/provenance.json)
# Verify commit SHA
PROV_SHA=$(echo "$PROV" | python3 -c "import sys,json; print(json.load(sys.stdin)['predicate']['invocation']['configSource']['digest']['sha1'])")
if [ "$PROV_SHA" != "${{ github.sha }}" ]; then
echo "FATAL: Provenance commit SHA mismatch"
echo " Expected: ${{ github.sha }}"
echo " Got: $PROV_SHA"
exit 1
fi
echo "Commit SHA in provenance: MATCHES ($PROV_SHA)"
# Verify builder identity contains expected repo
BUILDER_ID=$(echo "$PROV" | python3 -c "import sys,json; print(json.load(sys.stdin)['predicate']['builder']['id'])")
if ! echo "$BUILDER_ID" | grep -q "${{ github.repository }}"; then
echo "FATAL: Builder identity does not match expected repository"
echo " Expected to contain: ${{ github.repository }}"
echo " Got: $BUILDER_ID"
exit 1
fi
echo "Builder identity: MATCHES ($BUILDER_ID)"
# Verify workflow ref
WORKFLOW_REF=$(echo "$PROV" | python3 -c "import sys,json; print(json.load(sys.stdin)['predicate']['invocation']['configSource']['entryPoint'])")
if ! echo "$WORKFLOW_REF" | grep -q "supply-chain-test"; then
echo "FATAL: Workflow path mismatch"
echo " Expected to contain: supply-chain-test"
echo " Got: $WORKFLOW_REF"
exit 1
fi
echo "Workflow path: MATCHES ($WORKFLOW_REF)"
# Verify binary hashes in attestation match actual binaries
ACTUAL_CLI_HASH=$(sha256sum artifacts/target/release/karapace | awk '{print $1}')
PROV_CLI_HASH=$(echo "$PROV" | python3 -c "import sys,json; subj=json.load(sys.stdin)['subject']; print([s['digest']['sha256'] for s in subj if s['name']=='karapace'][0])")
if [ "$ACTUAL_CLI_HASH" != "$PROV_CLI_HASH" ]; then
echo "FATAL: Provenance hash mismatch for karapace"
echo " Actual: $ACTUAL_CLI_HASH"
echo " Attestation: $PROV_CLI_HASH"
exit 1
fi
echo "Provenance hash for karapace: MATCHES"
echo "=== ALL PROVENANCE CHECKS PASSED ==="
# --- Tamper detection tests ---
tamper-binary:
name: "Tamper Test: Binary"
runs-on: ubuntu-latest
needs: [build-and-sign]
steps:
- uses: actions/download-artifact@v4
with:
name: supply-chain-artifacts
path: artifacts
- uses: sigstore/cosign-installer@v3
- name: Tamper binary and verify detection
run: |
set -euo pipefail
# Modify 1 byte in the binary
cp artifacts/target/release/karapace /tmp/karapace-tampered
python3 -c "
data = bytearray(open('/tmp/karapace-tampered', 'rb').read())
data[100] ^= 0xFF # flip one byte
open('/tmp/karapace-tampered', 'wb').write(data)
"
# SHA256 must not match
ORIGINAL_HASH=$(awk '/karapace$/{print $1}' artifacts/target/release/SHA256SUMS)
TAMPERED_HASH=$(sha256sum /tmp/karapace-tampered | awk '{print $1}')
if [ "$ORIGINAL_HASH" = "$TAMPERED_HASH" ]; then
echo "FATAL: SHA256 did not detect binary tampering"
exit 1
fi
echo "SHA256 tamper detection: PASSED (hashes differ)"
# Cosign verification must fail
if cosign verify-blob \
/tmp/karapace-tampered \
--signature artifacts/target/release/karapace.sig \
--certificate artifacts/target/release/karapace.crt \
--certificate-identity-regexp 'https://github.com/marcoallegretti/karapace' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com 2>/dev/null; then
echo "FATAL: Cosign did NOT detect binary tampering"
exit 1
fi
echo "Cosign tamper detection: PASSED (verification correctly failed)"
tamper-sbom:
name: "Tamper Test: SBOM"
runs-on: ubuntu-latest
needs: [build-and-sign]
steps:
- uses: actions/download-artifact@v4
with:
name: supply-chain-artifacts
path: artifacts
- uses: sigstore/cosign-installer@v3
- name: Tamper SBOM and verify detection
run: |
set -euo pipefail
# Modify SBOM
cp artifacts/karapace_bom.json /tmp/sbom-tampered.json
python3 -c "
import json
with open('/tmp/sbom-tampered.json') as f:
bom = json.load(f)
bom['components'][0]['name'] = 'TAMPERED-PACKAGE'
with open('/tmp/sbom-tampered.json', 'w') as f:
json.dump(bom, f)
"
# Cosign verification must fail
if cosign verify-blob \
/tmp/sbom-tampered.json \
--signature artifacts/karapace_bom.json.sig \
--certificate artifacts/karapace_bom.json.crt \
--certificate-identity-regexp 'https://github.com/marcoallegretti/karapace' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com 2>/dev/null; then
echo "FATAL: Cosign did NOT detect SBOM tampering"
exit 1
fi
echo "SBOM tamper detection: PASSED (verification correctly failed)"
tamper-signature-removal:
name: "Tamper Test: Signature Removal"
runs-on: ubuntu-latest
needs: [build-and-sign]
steps:
- uses: actions/download-artifact@v4
with:
name: supply-chain-artifacts
path: artifacts
- uses: sigstore/cosign-installer@v3
- name: Remove signature and verify detection
run: |
set -euo pipefail
# Delete .sig file
rm artifacts/target/release/karapace.sig
# Verification must fail without signature
if cosign verify-blob \
artifacts/target/release/karapace \
--signature artifacts/target/release/karapace.sig \
--certificate artifacts/target/release/karapace.crt \
--certificate-identity-regexp 'https://github.com/marcoallegretti/karapace' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com 2>/dev/null; then
echo "FATAL: Cosign did NOT detect missing signature"
exit 1
fi
echo "Signature removal detection: PASSED (verification correctly failed)"
adversarial-env-injection:
name: "Adversarial: Environment Injection"
runs-on: ubuntu-latest
needs: [build-and-sign]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- uses: Swatinem/rust-cache@v2
- uses: actions/download-artifact@v4
with:
name: supply-chain-artifacts
path: baseline
- name: "Test: Malicious RUSTFLAGS cannot inject code"
run: |
set -euo pipefail
# Attempt to inject a cfg flag via RUSTFLAGS — build must still succeed
# and produce a binary that matches baseline (because our CI RUSTFLAGS override)
RUSTFLAGS="${RUSTFLAGS} --cfg=malicious_flag" \
cargo build --release -p karapace-cli -p karapace-dbus 2>&1 || true
# The key check: the binary must NOT contain our malicious string as executable code
# (cfg flags only affect conditional compilation — no matching cfg! blocks means no effect)
echo "Malicious RUSTFLAGS injection: build completed (cfg flag ignored if no matching code)"
- name: "Test: PATH manipulation cannot alter build"
run: |
set -euo pipefail
BASELINE_HASH=$(sha256sum baseline/target/release/karapace | awk '{print $1}')
# Create a fake rustc wrapper in a temp dir
mkdir -p /tmp/fake-bin
cat > /tmp/fake-bin/rustc-wrapper << 'SCRIPT'
#!/bin/bash
echo "ATTACK: fake rustc intercepted" >&2
exit 1
SCRIPT
chmod +x /tmp/fake-bin/rustc-wrapper
# Verify that RUSTC_WRAPPER pointing to malicious binary is detected
# (cargo will fail because the wrapper returns exit 1)
if RUSTC_WRAPPER=/tmp/fake-bin/rustc-wrapper cargo build --release -p karapace-cli 2>/dev/null; then
echo "FATAL: Malicious RUSTC_WRAPPER was NOT detected — build should have failed"
exit 1
fi
echo "PASS: Malicious RUSTC_WRAPPER correctly caused build failure"
- name: "Test: HTTP_PROXY/HTTPS_PROXY do not leak into build"
run: |
set -euo pipefail
# Set network proxy vars — they should not affect a from-cache build
HTTP_PROXY=http://evil-proxy:8080 \
HTTPS_PROXY=http://evil-proxy:8080 \
NO_PROXY="" \
cargo build --release -p karapace-cli -p karapace-dbus 2>&1 | tail -5
echo "PASS: Build completed with proxy env vars (no network needed for cached build)"
# Verify no proxy strings in the binary
if strings target/release/karapace | grep -qi 'evil-proxy'; then
echo "FATAL: Proxy string leaked into binary"
exit 1
fi
echo "PASS: No proxy strings in binary"
adversarial-artifact-tampering:
name: "Adversarial: Intermediate Artifact Tampering"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- name: "Test: Multi-rlib tampering detected by clean-build comparison"
run: |
set -euo pipefail
# Build baseline from clean state
cargo clean
cargo build --release -p karapace-cli -p karapace-dbus
BASELINE_HASH=$(sha256sum target/release/karapace | awk '{print $1}')
echo "Baseline binary hash: $BASELINE_HASH"
# Tamper ALL karapace rlibs
for RLIB in $(find target/release/deps -name 'libkarapace_*.rlib'); do
python3 -c "
data = bytearray(open('$RLIB', 'rb').read())
data[100] ^= 0xFF
open('$RLIB', 'wb').write(data)
"
echo "Tampered: $(basename $RLIB)"
done
# Rebuild — cargo should detect and rebuild from source
cargo build --release -p karapace-cli -p karapace-dbus 2>&1 | tail -3
REBUILD_HASH=$(sha256sum target/release/karapace | awk '{print $1}')
echo "Post-tamper rebuild hash: $REBUILD_HASH"
# Clean rebuild must match baseline
cargo clean
cargo build --release -p karapace-cli -p karapace-dbus
CLEAN_HASH=$(sha256sum target/release/karapace | awk '{print $1}')
echo "Clean rebuild hash: $CLEAN_HASH"
if [ "$CLEAN_HASH" != "$BASELINE_HASH" ]; then
echo "FATAL: Clean rebuild does not match baseline"
exit 1
fi
echo "PASS: Clean rebuild matches baseline after multi-rlib tampering"
- name: "Test: .rmeta tampering triggers rebuild"
run: |
set -euo pipefail
BASELINE_HASH=$(sha256sum target/release/karapace | awk '{print $1}')
for RMETA in $(find target/release/deps -name 'libkarapace_*.rmeta'); do
python3 -c "
data = bytearray(open('$RMETA', 'rb').read())
data[50] ^= 0xFF
open('$RMETA', 'wb').write(data)
"
done
cargo build --release -p karapace-cli -p karapace-dbus 2>&1 | tail -3
RMETA_HASH=$(sha256sum target/release/karapace | awk '{print $1}')
if [ "$RMETA_HASH" = "$BASELINE_HASH" ]; then
echo "PASS: Cargo rebuilt from source, ignoring tampered .rmeta"
else
echo "FATAL: .rmeta tampering changed binary hash"
exit 1
fi
- name: "Test: .d file tampering does not affect binary"
run: |
set -euo pipefail
BASELINE_HASH=$(sha256sum target/release/karapace | awk '{print $1}')
for D_FILE in $(find target/release/deps -name 'karapace_*.d' | head -5); do
echo "TAMPERED_MARKER" >> "$D_FILE"
done
cargo build --release -p karapace-cli -p karapace-dbus 2>&1 | tail -2
D_HASH=$(sha256sum target/release/karapace | awk '{print $1}')
if [ "$D_HASH" = "$BASELINE_HASH" ]; then
echo "PASS: .d file tampering did not affect binary"
else
echo "FATAL: .d file tampering changed binary hash"
exit 1
fi
- name: "Test: .fingerprint tampering triggers rebuild"
run: |
set -euo pipefail
BASELINE_HASH=$(sha256sum target/release/karapace | awk '{print $1}')
FP_DIR=$(find target/release/.fingerprint -name 'karapace-cli-*' -type d | head -1)
if [ -n "$FP_DIR" ]; then
for FP_FILE in "$FP_DIR"/*; do
[ -f "$FP_FILE" ] && echo "TAMPER" >> "$FP_FILE"
done
cargo build --release -p karapace-cli -p karapace-dbus 2>&1 | tail -3
FP_HASH=$(sha256sum target/release/karapace | awk '{print $1}')
if [ "$FP_HASH" = "$BASELINE_HASH" ]; then
echo "PASS: Cargo detected fingerprint tampering and rebuilt correctly"
else
echo "FATAL: Fingerprint tampering changed binary hash"
exit 1
fi
else
echo "SKIP: No .fingerprint directory found"
fi
adversarial-build-script:
name: "Adversarial: Build Script Injection"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- name: "Test: Rogue marker build.rs detected by hash change"
run: |
set -euo pipefail
cargo clean
cargo build --release -p karapace-cli -p karapace-dbus
BASELINE_HASH=$(sha256sum target/release/karapace | awk '{print $1}')
echo "Baseline binary hash: $BASELINE_HASH"
cat > crates/karapace-cli/build.rs << 'ROGUE'
fn main() {
println!("cargo:rustc-env=ROGUE_MARKER=SUPPLY_CHAIN_ATTACK");
}
ROGUE
cargo build --release -p karapace-cli -p karapace-dbus
ROGUE_HASH=$(sha256sum target/release/karapace | awk '{print $1}')
if [ "$BASELINE_HASH" = "$ROGUE_HASH" ]; then
echo "FATAL: Rogue build.rs did NOT change binary hash"
exit 1
fi
echo "PASS: Rogue marker build.rs detected ($BASELINE_HASH != $ROGUE_HASH)"
rm crates/karapace-cli/build.rs
- name: "Test: HOME-reading build.rs detected by hash change"
run: |
set -euo pipefail
cargo clean
cargo build --release -p karapace-cli -p karapace-dbus
BASELINE_HASH=$(sha256sum target/release/karapace | awk '{print $1}')
cat > crates/karapace-cli/build.rs << 'ROGUE'
fn main() {
let home = std::env::var("HOME").unwrap_or_else(|_| "unknown".to_string());
println!("cargo:rustc-env=BUILD_HOME={}", home);
}
ROGUE
cargo build --release -p karapace-cli -p karapace-dbus
HOME_HASH=$(sha256sum target/release/karapace | awk '{print $1}')
if [ "$BASELINE_HASH" = "$HOME_HASH" ]; then
echo "FATAL: HOME-reading build.rs did NOT change binary hash"
exit 1
fi
echo "PASS: HOME-reading build.rs detected ($BASELINE_HASH != $HOME_HASH)"
rm crates/karapace-cli/build.rs
- name: "Test: Hostname-leaking build.rs detected by hash change"
run: |
set -euo pipefail
cargo clean
cargo build --release -p karapace-cli -p karapace-dbus
BASELINE_HASH=$(sha256sum target/release/karapace | awk '{print $1}')
cat > crates/karapace-cli/build.rs << 'ROGUE'
fn main() {
let hostname = std::process::Command::new("hostname")
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_else(|_| "unknown".to_string());
println!("cargo:rustc-env=BUILD_HOST={}", hostname);
}
ROGUE
cargo build --release -p karapace-cli -p karapace-dbus
HOST_HASH=$(sha256sum target/release/karapace | awk '{print $1}')
if [ "$BASELINE_HASH" = "$HOST_HASH" ]; then
echo "FATAL: Hostname-leaking build.rs did NOT change binary hash"
exit 1
fi
echo "PASS: Hostname-leaking build.rs detected ($BASELINE_HASH != $HOST_HASH)"
rm crates/karapace-cli/build.rs
- name: "Test: Removing rogue build.rs restores baseline"
run: |
set -euo pipefail
# After all rogue scripts removed, clean rebuild must match
cargo clean
cargo build --release -p karapace-cli -p karapace-dbus
RESTORED=$(sha256sum target/release/karapace | awk '{print $1}')
# Build again to confirm reproducibility
cargo clean
cargo build --release -p karapace-cli -p karapace-dbus
RESTORED2=$(sha256sum target/release/karapace | awk '{print $1}')
if [ "$RESTORED" = "$RESTORED2" ]; then
echo "PASS: Clean rebuilds are reproducible after build.rs removal ($RESTORED)"
else
echo "FATAL: Clean rebuilds not reproducible"
exit 1
fi
adversarial-credential-injection:
name: "Adversarial: Credential Injection"
runs-on: ubuntu-latest
needs: [build-and-sign]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- uses: actions/download-artifact@v4
with:
name: supply-chain-artifacts
path: baseline
- name: "Test: Fake credentials do not affect binary"
run: |
set -euo pipefail
BASELINE_HASH=$(sha256sum baseline/target/release/karapace | awk '{print $1}')
API_KEY="sk-FAKE-SUPER-SECRET-KEY-12345" \
SECRET_TOKEN="ghp_FAKE_GITHUB_TOKEN_abcdef123456" \
AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/FAKE" \
CARGO_REGISTRY_TOKEN="cio_FAKE_REGISTRY_TOKEN_xyz" \
cargo build --release -p karapace-cli -p karapace-dbus
CRED_HASH=$(sha256sum target/release/karapace | awk '{print $1}')
if [ "$CRED_HASH" != "$BASELINE_HASH" ]; then
echo "FATAL: Credential env vars changed binary hash"
exit 1
fi
echo "PASS: Fake credentials did not affect binary hash"
- name: "Test: No credential strings in binary"
run: |
set -euo pipefail
for TERM in "sk-FAKE" "SUPER-SECRET" "ghp_FAKE" "GITHUB_TOKEN" "wJalrXUtnFEMI" "AWS_SECRET" "cio_FAKE"; do
if strings target/release/karapace | grep -qi "$TERM"; then
echo "FATAL: $TERM leaked into binary"
exit 1
fi
done
echo "PASS: No credential strings leaked into binary"
adversarial-rustflags-bypass:
name: "Adversarial: RUSTFLAGS Bypass"
runs-on: ubuntu-latest
needs: [build-and-sign]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- uses: actions/download-artifact@v4
with:
name: supply-chain-artifacts
path: baseline
- name: "Test: RUSTFLAGS override changes hash (detected)"
run: |
set -euo pipefail
BASELINE_HASH=$(sha256sum baseline/target/release/karapace | awk '{print $1}')
RUSTFLAGS="-C debuginfo=2" cargo build --release -p karapace-cli -p karapace-dbus 2>&1 | tail -3
OVERRIDE_HASH=$(sha256sum target/release/karapace | awk '{print $1}')
if [ "$OVERRIDE_HASH" = "$BASELINE_HASH" ]; then
echo "INFO: RUSTFLAGS override did not change hash (strip=true may remove debuginfo)"
else
echo "PASS: RUSTFLAGS override detected via hash change"
echo "DEFENSE: CI RUSTFLAGS are authoritative; local overrides cannot affect release artifacts"
fi
- name: "Test: SOURCE_DATE_EPOCH does not affect binary"
run: |
set -euo pipefail
cargo clean
SOURCE_DATE_EPOCH=0 cargo build --release -p karapace-cli -p karapace-dbus
EPOCH0=$(sha256sum target/release/karapace | awk '{print $1}')
cargo clean
SOURCE_DATE_EPOCH=9999999999 cargo build --release -p karapace-cli -p karapace-dbus
EPOCH_FUTURE=$(sha256sum target/release/karapace | awk '{print $1}')
if [ "$EPOCH0" = "$EPOCH_FUTURE" ]; then
echo "PASS: SOURCE_DATE_EPOCH does not affect binary (strip=true removes timestamps)"
else
echo "FATAL: SOURCE_DATE_EPOCH affects binary hash"
exit 1
fi
verify-docs-executable:
name: "Verify docs/verification.md Commands"
runs-on: ubuntu-latest
needs: [build-and-sign]
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: supply-chain-artifacts
path: artifacts
- uses: sigstore/cosign-installer@v3
- name: Execute verification.md commands
run: |
set -euo pipefail
# Flatten artifacts to match docs layout
cp artifacts/target/release/karapace .
cp artifacts/target/release/karapace-dbus .
cp artifacts/target/release/SHA256SUMS .
cp artifacts/target/release/karapace.sig .
cp artifacts/target/release/karapace.crt .
cp artifacts/target/release/karapace-dbus.sig .
cp artifacts/target/release/karapace-dbus.crt .
cp artifacts/karapace_bom.json .
echo "=== Step 1: Verify SHA256 Checksums ==="
sha256sum -c SHA256SUMS
echo "PASSED"
echo "=== Step 2: Verify Cosign Signatures ==="
cosign verify-blob karapace \
--signature karapace.sig \
--certificate karapace.crt \
--certificate-identity-regexp 'https://github.com/marcoallegretti/karapace' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
echo "karapace: VERIFIED"
cosign verify-blob karapace-dbus \
--signature karapace-dbus.sig \
--certificate karapace-dbus.crt \
--certificate-identity-regexp 'https://github.com/marcoallegretti/karapace' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
echo "karapace-dbus: VERIFIED"
echo "=== Step 3: Inspect SBOM ==="
python3 -m json.tool karapace_bom.json | head -50
echo "SBOM is valid JSON"
echo "=== ALL verification.md commands executed successfully ==="

90
CI_CONTRACT.md Normal file
View file

@ -0,0 +1,90 @@
# CI Contract
This document defines the required CI jobs that must exist in `.github/workflows/ci.yml`.
The `ci-contract` job enforces this list automatically — if any job is missing, CI fails.
## Required Jobs (ci.yml)
| Job Name | Purpose | Enforced |
|----------|---------|----------|
| `fmt` | Rustfmt check | Yes |
| `clippy` | Clippy lint | Yes |
| `test` | `cargo test --workspace` on 3 OS matrix | Yes |
| `e2e` | E2E namespace backend tests (`--ignored`) | Yes |
| `enospc` | ENOSPC simulation with tmpfs (requires `sudo`) | Yes |
| `e2e-resolve` | Package resolver E2E on Ubuntu/Fedora/openSUSE | Yes |
| `build-release` | Release build + SBOM + checksums (gnu + musl matrix) | Yes |
| `smoke-test` | Verify release binary runs (gnu + musl matrix) | Yes |
| `reproducibility-check` | Same-run glibc build reproducibility (build A vs B) | Yes |
| `reproducibility-check-musl` | Same-run musl build reproducibility (build A vs B) | Yes |
| `cross-run-reproducibility` | Cross-run glibc reproducibility (ubuntu-latest vs ubuntu-22.04) | Yes |
| `verify-cross-reproducibility` | Compare glibc cross-run build hashes (with diffoscope) | Yes |
| `cross-run-reproducibility-musl` | Cross-run musl reproducibility (ubuntu-latest vs ubuntu-22.04) | Yes |
| `verify-cross-reproducibility-musl` | Compare musl cross-run build hashes | Yes |
| `lockfile-check` | Verify Cargo.lock has not drifted | Yes |
| `cargo-deny` | Dependency policy (licenses, registries, advisories) | Yes |
| `ci-contract` | Self-check: all required jobs exist | Yes |
## Supply Chain Jobs (supply-chain-test.yml)
| Job Name | Purpose |
|----------|---------|
| `build-and-sign` | Build, sign binaries/SBOM, generate provenance attestation |
| `verify-signatures` | Verify cosign signatures + provenance content |
| `tamper-binary` | Modify 1 byte in binary, verify detection |
| `tamper-sbom` | Modify SBOM, verify detection |
| `tamper-signature-removal` | Delete .sig, verify detection |
| `adversarial-env-injection` | Test RUSTFLAGS injection, RUSTC_WRAPPER, proxy leak |
| `adversarial-artifact-tampering` | Tamper multi-rlib, .rmeta, .d, .fingerprint; verify rebuild defense |
| `adversarial-build-script` | Inject rogue build.rs (marker, HOME, hostname); verify hash change |
| `adversarial-credential-injection` | Inject fake API keys, AWS secrets, registry tokens; verify no leak |
| `adversarial-rustflags-bypass` | RUSTFLAGS override + SOURCE_DATE_EPOCH manipulation |
| `verify-docs-executable` | Execute docs/verification.md commands verbatim |
## Release Jobs (release.yml)
| Job Name | Purpose |
|----------|---------|
| `build` | Deterministic release build + SBOM + checksums (gnu + musl matrix) |
| `sign` | Cosign OIDC signing + provenance attestation (per target) |
| `verify` | Verify all signatures + checksums before publishing (per target) |
| `publish` | Create GitHub Release with all artifacts (both targets) |
## Branch Protection
The following jobs MUST be configured as required status checks in GitHub repository settings:
- `Format` (`fmt`)
- `Clippy` (`clippy`)
- `Test (ubuntu)` / `Test (fedora)` / `Test (opensuse)` (`test`)
- `E2E Tests` (`e2e`)
- `ENOSPC Tests` (`enospc`)
- `E2E Resolver (ubuntu)` / `E2E Resolver (fedora)` / `E2E Resolver (opensuse)` (`e2e-resolve`)
- `Release Build (x86_64-unknown-linux-gnu)` / `Release Build (x86_64-unknown-linux-musl)` (`build-release`)
- `Smoke Test Release (x86_64-unknown-linux-gnu)` / `Smoke Test Release (x86_64-unknown-linux-musl)` (`smoke-test`)
- `Reproducibility Check (same-run)` (`reproducibility-check`)
- `Reproducibility Check (musl, same-run)` (`reproducibility-check-musl`)
- `Verify Cross-Run Reproducibility` (`verify-cross-reproducibility`)
- `Cross-Run Reproducibility musl (ubuntu-latest)` / `Cross-Run Reproducibility musl (ubuntu-22.04)` (`cross-run-reproducibility-musl`)
- `Verify Cross-Run Reproducibility (musl)` (`verify-cross-reproducibility-musl`)
- `Lockfile Integrity` (`lockfile-check`)
- `Dependency Policy (cargo-deny)` (`cargo-deny`)
- `CI Contract` (`ci-contract`)
- `Build, Sign & Attest` (`build-and-sign`)
- `Verify Signatures & Provenance` (`verify-signatures`)
- `Tamper Test: Binary` (`tamper-binary`)
- `Tamper Test: SBOM` (`tamper-sbom`)
- `Tamper Test: Signature Removal` (`tamper-signature-removal`)
- `Adversarial: Environment Injection` (`adversarial-env-injection`)
- `Adversarial: Intermediate Artifact Tampering` (`adversarial-artifact-tampering`)
- `Adversarial: Build Script Injection` (`adversarial-build-script`)
- `Adversarial: Credential Injection` (`adversarial-credential-injection`)
- `Adversarial: RUSTFLAGS Bypass` (`adversarial-rustflags-bypass`)
- `Verify docs/verification.md Commands` (`verify-docs-executable`)
## Enforcement
The `ci-contract` job parses `ci.yml` and verifies that every ci.yml job listed above is present.
If a required job is renamed or removed, CI fails immediately.
Supply-chain test jobs are enforced by the `supply-chain-test.yml` workflow running on every PR.

10
scripts/generate-sbom.sh Executable file
View file

@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail
command -v cargo-cyclonedx >/dev/null 2>&1 || {
echo "Installing cargo-cyclonedx..."
cargo install cargo-cyclonedx --locked
}
cargo cyclonedx --format json --output-prefix karapace
echo "SBOM written to karapace_bom.json"