name: Release on: push: tags: - 'v*' env: REGISTRY: ghcr.io # Keep in lock-step with .github/workflows/ci.yml (M-3). GO_VERSION: '1.25.9' IMAGE_NAMESPACE: shankar0123 jobs: # ---------------------------------------------------------------------- # build-binaries (M-3): matrix build every (binary × OS × arch) tuple. # For each tuple we produce: the binary, a SPDX-JSON SBOM, a keyless # Cosign signature + certificate bundle, and a single-line sha256sum # file. All artefacts are uploaded to a workflow-scoped artifact; the # aggregate-checksums job fans them back in for release upload. # ---------------------------------------------------------------------- build-binaries: name: Build ${{ matrix.binary }} (${{ matrix.os }}/${{ matrix.arch }}) runs-on: ubuntu-latest permissions: contents: read id-token: write # Cosign keyless OIDC identity token strategy: fail-fast: false matrix: binary: [agent, server, cli, mcp-server] os: [linux, darwin] arch: [amd64, arm64] steps: - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} - name: Extract version from tag id: version run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT" - name: Install govulncheck # Bundle D / Audit L-008: release.yml previously had no vulnerability # scan, so a release tag could in principle ship a binary with a # known CVE in transitive deps that ci.yml's govulncheck would have # caught on master. Pre-build scan blocks the release if anything # surfaced post-merge. Pinned to the same major as ci.yml. run: go install golang.org/x/vuln/cmd/govulncheck@latest - name: Run govulncheck (release gate) # govulncheck distinguishes called-vs-uncalled vulnerable functions. # Default exit code (0 unless an actual call site lands in a vuln # function) is the right gate for release; deferred-call advisories # are tracked separately on master via L-021. If a release-time # scan surfaces a NEW called-vuln, the release is blocked until the # bump lands on master and a new tag is cut. run: govulncheck ./... - name: Build binary id: build env: GOOS: ${{ matrix.os }} GOARCH: ${{ matrix.arch }} CGO_ENABLED: '0' VERSION: ${{ steps.version.outputs.VERSION }} run: | set -euo pipefail OUTPUT_NAME="certctl-${{ matrix.binary }}-${{ matrix.os }}-${{ matrix.arch }}" mkdir -p dist go build \ -trimpath \ -ldflags="-w -s -X main.Version=${VERSION}" \ -o "dist/${OUTPUT_NAME}" \ "./cmd/${{ matrix.binary }}" ls -lh "dist/${OUTPUT_NAME}" echo "output_name=${OUTPUT_NAME}" >> "$GITHUB_OUTPUT" - name: Generate SBOM (SPDX-JSON) uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 with: file: dist/${{ steps.build.outputs.output_name }} format: spdx-json output-file: dist/${{ steps.build.outputs.output_name }}.sbom.spdx.json upload-artifact: false upload-release-assets: false - name: Install Cosign uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1 - name: Keyless-sign binary with Cosign env: OUTPUT_NAME: ${{ steps.build.outputs.output_name }} run: | set -euo pipefail # Cosign v3.0 (shipped by cosign-installer@v4.1.1 default # cosign-release=v3.0.5) removed --output-signature/--output-certificate # on sign-blob. The replacement is --bundle, which emits a unified # Sigstore bundle (signature + cert chain + Rekor inclusion proof) as # a single .sigstore.json artefact. M-11. cosign sign-blob \ --yes \ --bundle "dist/${OUTPUT_NAME}.sigstore.json" \ "dist/${OUTPUT_NAME}" - name: Compute SHA-256 sidecar env: OUTPUT_NAME: ${{ steps.build.outputs.output_name }} run: | set -euo pipefail cd dist sha256sum "${OUTPUT_NAME}" > "${OUTPUT_NAME}.sha256" cat "${OUTPUT_NAME}.sha256" - name: Upload build artefacts uses: actions/upload-artifact@v4 with: name: binary-${{ steps.build.outputs.output_name }} path: | dist/${{ steps.build.outputs.output_name }} dist/${{ steps.build.outputs.output_name }}.sigstore.json dist/${{ steps.build.outputs.output_name }}.sbom.spdx.json dist/${{ steps.build.outputs.output_name }}.sha256 if-no-files-found: error retention-days: 7 # ---------------------------------------------------------------------- # aggregate-checksums (M-3): fan in every matrix artefact, produce a # single checksums.txt (sha256sum format, compatible with `sha256sum # -c`), sign it with Cosign, upload everything to the GitHub Release, # and emit a base64-encoded hash manifest for the SLSA generator. # ---------------------------------------------------------------------- aggregate-checksums: name: Aggregate checksums & sign runs-on: ubuntu-latest needs: [build-binaries] permissions: contents: write id-token: write # Cosign keyless OIDC identity token outputs: hashes: ${{ steps.hashes.outputs.hashes }} steps: - name: Download binary artefacts uses: actions/download-artifact@v4 with: pattern: binary-* path: artifacts merge-multiple: true - name: Aggregate SHA-256 sums id: hashes run: | set -euo pipefail cd artifacts : > checksums.txt for f in certctl-*; do case "$f" in *.sigstore.json|*.sbom.spdx.json|*.sha256|checksums.txt) continue ;; esac sha256sum "$f" >> checksums.txt done echo "=== checksums.txt ===" cat checksums.txt # base64 hashes (single line, no wrapping) for SLSA generator. HASHES=$(base64 -w0 < checksums.txt) echo "hashes=${HASHES}" >> "$GITHUB_OUTPUT" - name: Install Cosign uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1 - name: Keyless-sign checksums.txt run: | set -euo pipefail cd artifacts # Cosign v3.0 --bundle replaces the removed v2 flag pair # --output-signature / --output-certificate. See M-11. cosign sign-blob \ --yes \ --bundle checksums.txt.sigstore.json \ checksums.txt - name: Upload artefacts to GitHub Release uses: softprops/action-gh-release@v2 if: startsWith(github.ref, 'refs/tags/') with: files: | artifacts/certctl-* artifacts/checksums.txt artifacts/checksums.txt.sigstore.json # ---------------------------------------------------------------------- # provenance-binaries (M-3): SLSA Level 3 provenance for every binary. # The SLSA generic generator reusable workflow runs in a hermetic # workflow run, producing multiple.intoto.jsonl from the base64 hash # manifest and uploading it as a release asset. # ---------------------------------------------------------------------- provenance-binaries: name: SLSA provenance (binaries) needs: [aggregate-checksums] permissions: actions: read id-token: write contents: write uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 with: base64-subjects: "${{ needs.aggregate-checksums.outputs.hashes }}" upload-assets: true provenance-name: multiple.intoto.jsonl # ---------------------------------------------------------------------- # build-and-push-docker: push container images to GHCR with native # SLSA L3 provenance (mode=max) and SBOM attestations emitted by # docker/build-push-action@v6, plus a keyless Cosign signature on the # image digest for identity-bound verification. The M-4 proxy-propagation # build-args block is retained verbatim — M-3 only adds supply-chain # steps; it never touches M-4 wiring. # ---------------------------------------------------------------------- build-and-push-docker: name: Build & Push Docker Images runs-on: ubuntu-latest permissions: contents: write packages: write id-token: write # Cosign keyless OIDC identity token steps: - uses: actions/checkout@v4 - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract version from tag id: version run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT" - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Install Cosign uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1 - name: Build and push server image id: server-push uses: docker/build-push-action@v6 with: context: . file: ./Dockerfile push: true tags: | ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/certctl-server:${{ steps.version.outputs.VERSION }} ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/certctl-server:latest # Proxy propagation (M-4, Issue #9) — forwards runner-level proxy # secrets into the Docker build so self-hosted runners behind # corporate proxies can reach public registries. GitHub-hosted # runners don't need proxies, so the secrets are optional and # resolve to empty strings when unset — byte-identical to the # pre-fix behaviour for the public-runner path. build-args: | HTTP_PROXY=${{ secrets.HTTP_PROXY }} HTTPS_PROXY=${{ secrets.HTTPS_PROXY }} NO_PROXY=${{ secrets.NO_PROXY }} # Supply-chain hardening (M-3): emit native SLSA L3 provenance # and SBOM attestations bound to the image manifest. provenance: mode=max sbom: true cache-from: type=gha cache-to: type=gha,mode=max - name: Keyless-sign server image with Cosign env: DIGEST: ${{ steps.server-push.outputs.digest }} IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/certctl-server run: | set -euo pipefail cosign sign --yes "${IMAGE}@${DIGEST}" - name: Build and push agent image id: agent-push uses: docker/build-push-action@v6 with: context: . file: ./Dockerfile.agent push: true tags: | ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/certctl-agent:${{ steps.version.outputs.VERSION }} ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/certctl-agent:latest # Proxy propagation (M-4, Issue #9) — see server-image step for # rationale. Empty secrets resolve to empty build args, leaving # the un-proxied code path byte-identical to the pre-fix tree. build-args: | HTTP_PROXY=${{ secrets.HTTP_PROXY }} HTTPS_PROXY=${{ secrets.HTTPS_PROXY }} NO_PROXY=${{ secrets.NO_PROXY }} # Supply-chain hardening (M-3): emit native SLSA L3 provenance # and SBOM attestations bound to the image manifest. provenance: mode=max sbom: true cache-from: type=gha cache-to: type=gha,mode=max - name: Keyless-sign agent image with Cosign env: DIGEST: ${{ steps.agent-push.outputs.digest }} IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/certctl-agent run: | set -euo pipefail cosign sign --yes "${IMAGE}@${DIGEST}" # ---------------------------------------------------------------------- # create-release: stamp the release body. The actual asset uploads are # handled by aggregate-checksums (binaries, SBOMs, sigs, certs, # checksums.txt + signature) and the SLSA generator (multiple.intoto.jsonl). # ---------------------------------------------------------------------- create-release: name: Create Release Notes runs-on: ubuntu-latest needs: [build-binaries, aggregate-checksums, provenance-binaries, build-and-push-docker] permissions: contents: write steps: - uses: actions/checkout@v4 - name: Extract version from tag id: version run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT" - name: Create release with notes uses: softprops/action-gh-release@v2 with: generate_release_notes: true body: | ## Installation ### Quick Install (Linux/macOS) ```bash curl -sSL https://raw.githubusercontent.com/shankar0123/certctl/master/install-agent.sh | bash ``` ### Manual Binary Download Download the appropriate binary for your OS and architecture: - **Linux x86_64**: `certctl-agent-linux-amd64` - **Linux ARM64**: `certctl-agent-linux-arm64` - **macOS x86_64**: `certctl-agent-darwin-amd64` - **macOS ARM64 (Apple Silicon)**: `certctl-agent-darwin-arm64` Then make it executable and start the service: ```bash chmod +x certctl-agent-linux-amd64 sudo mv certctl-agent-linux-amd64 /usr/local/bin/certctl-agent ``` ## Docker Images Pull pre-built Docker images for server and agent: ```bash docker pull ghcr.io/shankar0123/certctl-server:${{ steps.version.outputs.VERSION }} docker pull ghcr.io/shankar0123/certctl-agent:${{ steps.version.outputs.VERSION }} ``` Or use the latest tag: ```bash docker pull ghcr.io/shankar0123/certctl-server:latest docker pull ghcr.io/shankar0123/certctl-agent:latest ``` ## Docker Compose Quick Start ```bash git clone https://github.com/shankar0123/certctl.git cd certctl cp deploy/.env.example deploy/.env docker compose -f deploy/docker-compose.yml up -d ``` ## Server Binaries Pre-compiled server binaries are also available for direct installation: - **Linux x86_64**: `certctl-server-linux-amd64` - **Linux ARM64**: `certctl-server-linux-arm64` - **macOS x86_64**: `certctl-server-darwin-amd64` - **macOS ARM64 (Apple Silicon)**: `certctl-server-darwin-arm64` ## CLI & MCP Server Binaries The `certctl-cli` (REST API wrapper) and `certctl-mcp-server` (Model Context Protocol bridge) binaries ship for all four platforms as well: - `certctl-cli-{linux,darwin}-{amd64,arm64}` - `certctl-mcp-server-{linux,darwin}-{amd64,arm64}` ## Verifying this release Every binary, `checksums.txt`, and container image is signed with Cosign keyless OIDC. Each binary ships with a SPDX-JSON SBOM. Binaries are covered by SLSA Level 3 provenance; container images carry native SLSA L3 provenance and SBOM attestations (docker/build-push-action `provenance: mode=max`, `sbom: true`) in addition to a Cosign signature on the digest. **1. Verify SHA-256 checksums:** ```bash sha256sum -c checksums.txt ``` **2. Verify the Cosign signature on checksums.txt (keyless OIDC):** ```bash cosign verify-blob \ --bundle checksums.txt.sigstore.json \ --certificate-identity-regexp '^https://github\.com/shankar0123/certctl/\.github/workflows/release\.yml@refs/tags/' \ --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ checksums.txt ``` Replace `checksums.txt` with any individual binary name to verify that artefact directly (each binary ships with its own `.sigstore.json` bundle, e.g. `cosign verify-blob --bundle certctl-agent-linux-amd64.sigstore.json …`). **3. Verify SLSA Level 3 provenance (binaries):** ```bash slsa-verifier verify-artifact \ --provenance-path multiple.intoto.jsonl \ --source-uri github.com/shankar0123/certctl \ --source-tag ${{ steps.version.outputs.VERSION }} \ certctl-agent-linux-amd64 ``` **4. Verify container image signature and attestations:** ```bash IMAGE=ghcr.io/shankar0123/certctl-server:${{ steps.version.outputs.VERSION }} cosign verify \ --certificate-identity-regexp '^https://github\.com/shankar0123/certctl/\.github/workflows/release\.yml@refs/tags/' \ --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ "$IMAGE" # SBOM attestation (SPDX-JSON) emitted by docker/build-push-action cosign verify-attestation --type spdxjson \ --certificate-identity-regexp '^https://github\.com/shankar0123/certctl/' \ --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ "$IMAGE" # SLSA provenance attestation (mode=max) cosign verify-attestation --type slsaprovenance \ --certificate-identity-regexp '^https://github\.com/shankar0123/certctl/' \ --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ "$IMAGE" ``` ## Helm Chart Deploy certctl to Kubernetes using Helm: ```bash helm repo add certctl https://github.com/shankar0123/certctl/tree/master/deploy/helm helm repo update helm install certctl certctl/certctl ``` See `deploy/helm/certctl/` for values customization.