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
20 changes: 19 additions & 1 deletion .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ RUN apt-get update \
# ACTIONLINT_VERSION -> https://github.com/rhysd/actionlint/releases
# HADOLINT_VERSION -> https://github.com/hadolint/hadolint/releases
# VALKEY_VERSION -> https://github.com/valkey-io/valkey/releases
# COSIGN_VERSION -> https://github.com/sigstore/cosign/releases

ENV PATH="/go/bin:/usr/local/go/bin:${PATH}" \
KUBECTL_VERSION=v1.36.2 \
Expand All @@ -71,7 +72,8 @@ ENV PATH="/go/bin:/usr/local/go/bin:${PATH}" \
TASK_VERSION=v3.51.1 \
ACTIONLINT_VERSION=1.7.12 \
HADOLINT_VERSION=2.14.0 \
VALKEY_VERSION=9.1.0
VALKEY_VERSION=9.1.0 \
COSIGN_VERSION=v3.1.1

# Fail early on unsupported architectures instead of producing a partial image.
RUN test "$(dpkg --print-architecture)" = "amd64" \
Expand Down Expand Up @@ -158,6 +160,22 @@ RUN curl -fsSL "https://download.valkey.io/releases/valkey-${VALKEY_VERSION}-jam
| tar -xzO "valkey-${VALKEY_VERSION}-jammy-x86_64/bin/valkey-cli" > /usr/local/bin/valkey-cli \
&& chmod +x /usr/local/bin/valkey-cli

# Install cosign (Sigstore keyless signing/verification). Lives in the ci stage,
# not dev-only: release.yml's publish-helm job runs *inside* this same image
# (see `container:` in .github/workflows/release.yml) and signs install.yaml
# with cosign — baking it in here means that job no longer needs its own
# sigstore/cosign-installer step. Also lets you run `cosign verify`/`verify-blob`
# locally against what release.yml publishes (see docs/ci-overview.md).
RUN asset="cosign-linux-amd64" \
&& base="https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION}" \
&& tmpdir="$(mktemp -d)" \
&& curl -fsSL "${base}/${asset}" -o "${tmpdir}/${asset}" \
&& curl -fsSL "${base}/cosign_checksums.txt" -o "${tmpdir}/checksums.txt" \
&& cd "${tmpdir}" \
&& grep " ${asset}$" checksums.txt | sha256sum -c - \
&& install -m 0755 "${asset}" /usr/local/bin/cosign \
&& rm -rf "${tmpdir}"

# Set working directory
WORKDIR /workspaces

Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ jobs:
controller-gen --version
k3d version
docker --version
cosign version
echo '✅ All CI container tools verified'
"

Expand Down
58 changes: 52 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,12 @@ jobs:

- name: Install cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
with:
# Keep in sync with COSIGN_VERSION in .devcontainer/Dockerfile — this
# is the only other job that signs release artifacts (publish-helm),
# and it gets cosign from that image. Without this input the action
# defaults to its own (older) pinned cosign release.
cosign-release: v3.1.1

- name: Sign the multi-arch image (cosign keyless)
# One signature on the digest covers every tag pointing at it.
Expand Down Expand Up @@ -171,6 +177,23 @@ jobs:
sbom-path: sbom.spdx.json
push-to-registry: true

# The image itself is already signed + attested above, but OpenSSF
# Scorecard's Signed-Releases check only looks at the GitHub *release
# assets*, not the OCI registry. Sign and attest the SBOM asset directly
# so the release itself carries a signature (*.sigstore.json) and SLSA
# provenance (*.intoto.jsonl) next to it.
- name: Sign the SBOM asset (cosign keyless)
run: cosign sign-blob --bundle sbom.spdx.json.sigstore.json --yes sbom.spdx.json

- name: Attest SLSA provenance for the SBOM asset
id: attest-sbom-asset
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-path: sbom.spdx.json

- name: Rename provenance bundle to the Scorecard-recognized suffix
run: cp "${{ steps.attest-sbom-asset.outputs.bundle-path }}" sbom.spdx.json.intoto.jsonl

- name: Update release info
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
with:
Expand All @@ -181,7 +204,10 @@ jobs:
# it to published after all assets are attached.
draft: true
append_body: true
files: sbom.spdx.json
files: |
sbom.spdx.json
sbom.spdx.json.sigstore.json
sbom.spdx.json.intoto.jsonl
body: |
## Installation
### Quick Install (Single YAML)
Expand Down Expand Up @@ -226,7 +252,8 @@ jobs:
permissions:
contents: write # upload install.yaml release asset
packages: write
id-token: write # cosign keyless signing
id-token: write # cosign keyless signing + attestation OIDC
attestations: write
container:
image: ${{ needs.ci.outputs.ci-image }}
credentials:
Expand Down Expand Up @@ -262,9 +289,8 @@ jobs:
fi
echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT"

- name: Install cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2

# cosign ships baked into the ci-image this job runs in (see
# .devcontainer/Dockerfile) — no installer step needed here.
- name: Log in to registry for cosign
# `helm registry login` writes helm's own registry config; cosign reads
# ~/.docker/config.json, so it needs its own login or the signature
Expand All @@ -277,6 +303,23 @@ jobs:
- name: Sign the Helm chart (cosign keyless)
run: cosign sign --yes "${CHART_REGISTRY}/gitops-reverser@${{ steps.chart.outputs.digest }}"

# The chart is already signed above via its OCI digest, but OpenSSF
# Scorecard's Signed-Releases check only looks at the GitHub *release
# assets*, not the OCI registry. Sign and attest install.yaml directly so
# the release carries a signature (*.sigstore.json) and SLSA provenance
# (*.intoto.jsonl) next to it.
- name: Sign install.yaml (cosign keyless)
run: cosign sign-blob --bundle install.yaml.sigstore.json --yes dist/install.yaml

- name: Attest SLSA provenance for install.yaml
id: attest-install-yaml
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-path: dist/install.yaml

- name: Rename provenance bundle to the Scorecard-recognized suffix
run: cp "${{ steps.attest-install-yaml.outputs.bundle-path }}" install.yaml.intoto.jsonl

- name: Upload install.yaml as release asset
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
with:
Expand All @@ -285,7 +328,10 @@ jobs:
# every asset is in place (immutable releases reject post-publish
# uploads).
draft: true
files: dist/install.yaml
files: |
dist/install.yaml
install.yaml.sigstore.json
install.yaml.intoto.jsonl
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Expand Down
16 changes: 15 additions & 1 deletion docs/ci-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ step merges those exact digests into a multi-arch manifest tagged with the semve
| --- | --- | --- |
| Multi-arch image (`linux/amd64`, `linux/arm64`) | `ghcr.io/configbutler/gitops-reverser` | cosign keyless signature, SLSA build provenance attestation, SPDX SBOM attestation |
| Helm chart | `oci://ghcr.io/configbutler/charts/gitops-reverser` | cosign keyless signature |
| `install.yaml` + `sbom.spdx.json` | GitHub release assets | part of the signed release |
| `install.yaml`, `sbom.spdx.json` | GitHub release assets | each signed directly (`<asset>.sigstore.json`) and SLSA-attested (`<asset>.intoto.jsonl`), also uploaded as release assets |

Verify an image (also embedded in every release's notes):

Expand All @@ -98,6 +98,20 @@ gh attestation verify oci://ghcr.io/configbutler/gitops-reverser:<version> \
--repo ConfigButler/gitops-reverser
```

Verify a release asset (e.g. `install.yaml`) from its downloaded `.sigstore.json` bundle:

```bash
cosign verify-blob \
--bundle install.yaml.sigstore.json \
--certificate-identity-regexp '^https://github.com/ConfigButler/gitops-reverser/\.github/workflows/release\.yml@refs/heads/main$' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
install.yaml
```

These per-asset signatures/attestations exist specifically so release assets carry their
own verifiable integrity, independent of the OCI registry — the surface OpenSSF Scorecard's
`Signed-Releases` check actually inspects.

Signing is *keyless* (Sigstore): there is no private key to store or leak. The signature
certifies "built by the `release.yml` workflow on `main` of this repository", issued via
GitHub's OIDC identity and logged in the public Rekor transparency log.
Expand Down