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
24 changes: 24 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,30 @@ env:
RUST_BACKTRACE: 1

jobs:
# Advisory, license, and source policy from `deny.toml` — see
# docs/supply-chain-policy.md. Split from `clippy-and-test` because it
# needs none of that job's FIPS build tooling: `cargo deny` reads
# `Cargo.lock` and crate metadata, it never compiles anything, so this
# finishes in seconds rather than the FIPS module's twenty-plus minutes.
# A crate landing on RUSTSEC with no matching `deny.toml` entry fails this
# job, on every push and every pull request.
advisories:
name: Dependency advisories
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install the Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Install cargo-deny
uses: taiki-e/install-action@cargo-deny

- name: cargo deny check
# `--locked` is a `cargo-deny` flag, not a `check` flag: it must
# come before the subcommand.
run: cargo deny --locked check

clippy-and-test:
name: ${{ matrix.name }}
runs-on: ${{ matrix.os }}
Expand Down
94 changes: 90 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,35 @@ jobs:
fi
echo "garrison-agent $crate"

# Advisory, license, and source policy from `deny.toml` — the same gate
# `ci.yml` runs on every push, run again here against whatever
# `Cargo.lock` the tagged commit actually carries. A tag can point to a
# commit whose CI run predates an advisory that has since been published,
# so re-checking at release time is not redundant with CI: it is the
# "and release" half of #43's "every push and release."
supply-chain:
name: Dependency advisories
needs: plan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.plan.outputs.tag }}

- name: Install the Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Install cargo-deny
uses: taiki-e/install-action@cargo-deny

- name: cargo deny check
# `--locked` is a `cargo-deny` flag, not a `check` flag: it must
# come before the subcommand.
run: cargo deny --locked check

build:
name: ${{ matrix.name }}
needs: plan
needs: [plan, supply-chain]
runs-on: ${{ matrix.os }}
timeout-minutes: 120
strategy:
Expand Down Expand Up @@ -176,6 +202,9 @@ jobs:
go-version: '1.24'
cache: false

- name: Install cargo-cyclonedx
uses: taiki-e/install-action@cargo-cyclonedx

# Deliberately not cached. A release artifact is worth the twenty
# minutes it takes to build AWS-LC from source on a clean runner, and a
# restored cache is one more thing an auditor would have to trust.
Expand Down Expand Up @@ -205,6 +234,19 @@ jobs:
"$bin" --version
echo "the FIPS provider installed; this binary is FIPS-enabled"

# One CycloneDX SBOM per binary this workspace ships, resolved for
# this leg's target so a cross-target-only dependency doesn't show up
# in a platform that never builds it. Run against the whole workspace
# rather than a single crate: cargo-cyclonedx has no per-package
# filter, so both `garrison-agent_bin.cdx.json` and
# `garrison-hooks_bin.cdx.json` are produced on every leg regardless
# of `matrix.hooks`, and the archive step below copies in only the
# one(s) it actually ships — the same guard the binary itself gets.
- name: Generate the SBOM
run: |
cargo cyclonedx --describe binaries --format json \
--spec-version 1.5 --target ${{ matrix.target }} -q

- name: Assemble the archive
id: archive
env:
Expand All @@ -216,9 +258,10 @@ jobs:
run: |
set -euo pipefail
stage="garrison-$VERSION-$TARGET"
mkdir -p "$stage/docs"
mkdir -p "$stage/docs" "$stage/sbom"
cp "target/$TARGET/release/garrison-agent" "$stage/"
cp README.md LICENSE "$stage/"
cp agent/garrison-agent_bin.cdx.json "$stage/sbom/"

# On Apple the validated module is a dylib rather than something
# linked into the binary, so it is part of the artefact: the binary
Expand All @@ -231,6 +274,7 @@ jobs:
# Regenerated into the project root by hooks-service/build.rs, and
# the plane's `[[schema_forge.hooks.bindings]]` entry needs it.
cp hooks-service/hooks_descriptor.bin "$stage/"
cp hooks-service/garrison-hooks_bin.cdx.json "$stage/sbom/"
mkdir -p "$stage/systemd"
cp packaging/systemd/garrison-agent.service "$stage/systemd/"
cp packaging/README.md "$stage/systemd/"
Expand Down Expand Up @@ -281,6 +325,15 @@ jobs:
name: Publish
needs: [plan, build]
runs-on: ubuntu-latest
# Job-level permissions replace the workflow-level block above rather
# than adding to it, so `contents: write` (the draft release, the
# upload) is repeated here alongside the one permission this job adds:
# `id-token: write`, which is what lets cosign mint a short-lived
# Sigstore certificate from this job's own OIDC identity instead of a
# long-lived signing key this repository would otherwise have to store.
permissions:
contents: write
id-token: write
steps:
- uses: actions/checkout@v4
with:
Expand All @@ -300,6 +353,24 @@ jobs:
shasum -a 256 -c SHA256SUMS
ls -l

- name: Install cosign
uses: sigstore/cosign-installer@v3

# Keyless: the certificate binds this signature to "the release.yml
# workflow, running as a push of this tag, in this repository" — not
# to a private key that would need generating, rotating, and keeping
# secret. Signing SHA256SUMS rather than every archive individually
# means one signature covers every artifact in the release, since
# each artifact's own hash is already a line in that file.
- name: Sign the checksums
working-directory: artifacts
run: |
set -euo pipefail
cosign sign-blob --yes \
--output-signature SHA256SUMS.sig \
--output-certificate SHA256SUMS.pem \
SHA256SUMS

# Left as a draft on purpose: publishing is a person's decision, and the
# draft is where they check the artifact list before an agency does.
- name: Create the draft release
Expand All @@ -324,7 +395,21 @@ jobs:
itself: keep it beside the binary, which is where the binary looks
for it.

Verify a download against `SHA256SUMS`:
Each archive also carries a `sbom/` directory: one CycloneDX 1.5
SBOM per binary it ships, generated for that archive's own target
triple from this repository's `Cargo.lock`.

`SHA256SUMS` is signed keylessly with `cosign`, against this
workflow's own GitHub Actions OIDC identity rather than a stored
private key. Verify the signature, then the archive against the
now-trusted checksums:

cosign verify-blob \
--certificate SHA256SUMS.pem \
--signature SHA256SUMS.sig \
--certificate-identity-regexp '^https://github\.com/Govcraft/garrison/\.github/workflows/release\.yml@.*$' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
SHA256SUMS

shasum -a 256 -c SHA256SUMS --ignore-missing
NOTES
Expand All @@ -334,4 +419,5 @@ jobs:
flags+=(--prerelease)
fi
gh release create "$TAG" "${flags[@]}" \
artifacts/SHA256SUMS artifacts/*.tar.gz
artifacts/SHA256SUMS artifacts/SHA256SUMS.sig artifacts/SHA256SUMS.pem \
artifacts/*.tar.gz
80 changes: 80 additions & 0 deletions deny.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Dependency-advisory, license, and source policy, enforced by `cargo deny
# check` on every push, every pull request (`.github/workflows/ci.yml`), and
# every release (`.github/workflows/release.yml`). See
# `docs/supply-chain-policy.md` for what this file means and how the ignore
# list below is reviewed.
#
# Regenerate the skeleton with `cargo deny init`; this file is hand-trimmed
# from that template, keeping only the fields whose value differs from
# cargo-deny's default.

[graph]
# `garrison-hooks` is Linux-only in practice (see ci.yml and release.yml),
# but nothing here is gated on it: a license or advisory that only affects a
# Windows-only dependency edge should still surface, since #43 is about
# catching problems before they ship, not about what today's runners build.
all-features = true

# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
[advisories]
# Every entry here is a known RUSTSEC advisory this workspace has decided
# not to act on yet, with the reason recorded next to it. An advisory that
# has no entry here fails the check — that is the point of this file.
ignore = [
# All seven of the following are pulled in transitively by `libsql`,
# which `acton-ai` 0.35.0 depends on for its embedded-database feature.
# `acton-ai` is pinned to an exact version (see
# docs/supply-chain-policy.md), so none of these can be resolved away by
# `cargo update`; clearing them means moving that pin. Tracked in
# https://github.com/Govcraft/garrison/issues/47.
{ id = "RUSTSEC-2025-0141", reason = "bincode 1.3.3 is unmaintained; pulled in by libsql via acton-ai 0.35.0, no safe upgrade exists on 1.x. Tracked in #47." },
{ id = "RUSTSEC-2025-0134", reason = "rustls-pemfile 2.2.0 is unmaintained; pulled in by libsql's hyper-rustls via acton-ai 0.35.0. Tracked in #47." },
{ id = "RUSTSEC-2026-0049", reason = "rustls-webpki 0.102.8 CRL distribution-point matching bug; garrison does not consume CRLs. Pulled in by libsql via acton-ai 0.35.0. Tracked in #47." },
{ id = "RUSTSEC-2026-0098", reason = "rustls-webpki 0.102.8 accepted URI name constraints it does not enforce; garrison asserts no URI names. Pulled in by libsql via acton-ai 0.35.0. Tracked in #47." },
{ id = "RUSTSEC-2026-0099", reason = "rustls-webpki 0.102.8 accepted name constraints against wildcard names incorrectly; requires a misissued certificate to reach. Pulled in by libsql via acton-ai 0.35.0. Tracked in #47." },
{ id = "RUSTSEC-2026-0104", reason = "rustls-webpki 0.102.8 reachable panic parsing a CRL; garrison does not parse CRLs. Pulled in by libsql via acton-ai 0.35.0. Tracked in #47." },
{ id = "RUSTSEC-2026-0258", reason = "h2 0.3.27 unbounded empty DATA frames (low severity); garrison-agent is an outbound gRPC/HTTP client on this stack, not a server accepting attacker frames. Pulled in by libsql's tonic/hyper via acton-ai 0.35.0. Tracked in #47." },
]

# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
[licenses]
# Every SPDX identifier that appears anywhere in the dependency graph today
# (`cargo metadata --all-features`, deduplicated, OR-expressions reduced to
# their members). A license expression is satisfied if any one member of an
# OR is on this list; an AND requires every member to be. Adding a
# dependency under a license not already covered here fails the check —
# that is the review point docs/supply-chain-policy.md describes.
allow = [
"AGPL-3.0-only", # garrison's own crates
"MIT",
"Apache-2.0",
"Apache-2.0 WITH LLVM-exception",
"BSD-1-Clause",
"BSD-2-Clause",
"BSD-3-Clause",
"ISC",
"0BSD",
"BSL-1.0",
"CC0-1.0",
"MIT-0",
"CDLA-Permissive-2.0",
"MPL-2.0",
"Zlib",
"Unlicense",
"OpenSSL",
"Unicode-3.0",
]
confidence-threshold = 0.8

# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
[bans]
# Left at cargo-deny's defaults: duplicate versions warn rather than fail
# (this workspace has several, none worth blocking a release over today),
# and nothing is explicitly banned or allow-listed yet.
multiple-versions = "warn"

# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
[sources]
unknown-registry = "deny"
unknown-git = "deny"
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
Loading
Loading