From 3cfcec926614ba73150e92060cacb3d90a4fdaa9 Mon Sep 17 00:00:00 2001 From: mikeappsec Date: Sun, 3 May 2026 02:48:34 +1000 Subject: [PATCH 1/2] fix: add missing pages to mkdocs nav (strict build) Add design/cache-architecture.md and operations/supply-chain.md to the nav configuration so mkdocs --strict no longer aborts on orphan pages. --- mkdocs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mkdocs.yml b/mkdocs.yml index 7bcf7ad..4b43934 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -156,6 +156,7 @@ nav: - Design: DESIGN.md - Architecture overview: ARCHITECTURE.md - HTTP / gRPC API: API.md + - Cache architecture: design/cache-architecture.md - Milestones: MILESTONES.md - Operations: - Deployment: DEPLOYMENT.md @@ -166,6 +167,7 @@ nav: - Key rotation: operations/key-rotation.md - Policy rotation: operations/policy-rotation.md - Disaster recovery: operations/disaster-recovery.md + - Supply-chain trust: operations/supply-chain.md - Request invariants: testing/request-invariants.md - Security: - v1.0 security review: security/v1.0-review.md From 239417edcd0ae6d4a21dc135b304e6be5885b9e4 Mon Sep 17 00:00:00 2001 From: mikeappsec Date: Sun, 3 May 2026 02:58:30 +1000 Subject: [PATCH 2/2] harden CI/CD pipeline and supply-chain controls - Pin all third-party GitHub Actions to immutable commit SHAs - Split release workflow into least-privilege jobs (verify-tag, goreleaser, sign, provenance, helm) - Add concurrency guard to prevent parallel release runs - Verify release tags exist on main before proceeding - Enable PIE and trimpath build flags in GoReleaser - Resolve image digests before pull in air-gap docs (TOCTOU) - Remove deprecated COSIGN_EXPERIMENTAL env var (Cosign v3 keyless) - Add Dependabot config for automated action SHA and Go module updates --- .github/dependabot.yml | 24 ++++++++++ .github/workflows/build.yaml | 30 ++++++------ .github/workflows/release.yaml | 84 +++++++++++++++++++++++++-------- .goreleaser.yaml | 6 +++ docs/operations/supply-chain.md | 11 +++-- 5 files changed, 117 insertions(+), 38 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..6a0dd21 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,24 @@ +# Dependabot keeps third-party GitHub Actions pinned SHAs up to date. +# This ensures we receive automated PRs when actions release security +# fixes, without relying on mutable major-version tags. +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + commit-message: + prefix: "ci" + include: "scope" + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "weekly" + commit-message: + prefix: "deps" + include: "scope" + ignore: + # k8s.io/* is pinned to v0.35.4 due to controller-runtime + # compatibility — see DESIGN.md §7 (M12 dependency refresh). + - dependency-name: "k8s.io/*" + update-types: ["version-update:semver-minor"] diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 51c58af..491bc40 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -30,8 +30,8 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version: '1.26.2' cache: true @@ -53,8 +53,8 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version: '1.26.2' cache: true @@ -79,23 +79,23 @@ jobs: packages: write id-token: write steps: - - uses: actions/checkout@v4 - - uses: docker/setup-buildx-action@v3 - - uses: docker/login-action@v3 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 + - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 if: github.event_name != 'pull_request' with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE }} tags: | type=sha type=ref,event=branch type=semver,pattern={{version}} - - uses: docker/build-push-action@v6 + - uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0 with: context: . platforms: linux/amd64,linux/arm64 @@ -125,16 +125,16 @@ jobs: packages: write id-token: write steps: - - uses: actions/checkout@v4 - - uses: docker/setup-buildx-action@v3 - - uses: docker/login-action@v3 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 + - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 if: github.event_name != 'pull_request' with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE }} flavor: | @@ -143,7 +143,7 @@ jobs: type=sha type=ref,event=branch type=semver,pattern={{version}} - - uses: docker/build-push-action@v6 + - uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0 with: context: . file: Dockerfile.fips @@ -164,7 +164,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Compose up run: docker compose -f deploy/docker/compose.yaml up -d --build --wait - name: Smoke test diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 0484bf7..4a146ad 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -9,6 +9,12 @@ # # Docker images (stock + FIPS) are produced by build.yaml which already # triggers on v* tags — this workflow adds the binary/Helm/signing layer. +# +# Security: +# - All third-party actions pinned by full commit SHA. +# - Jobs split by least-privilege scope. +# - Concurrency guard prevents parallel release races. +# - Tag-on-main verification prevents rogue tag releases. name: release @@ -17,6 +23,11 @@ on: tags: - "v*" +# Prevent parallel releases from racing on the same tag or overlapping tags. +concurrency: + group: release-${{ github.ref_name }} + cancel-in-progress: false + permissions: contents: read @@ -25,24 +36,45 @@ env: CHART_REPO: ghcr.io/mikeappsec/charts jobs: + # ------------------------------------------------------------------ + # Job 0: Verify the tag points to a commit on main + # ------------------------------------------------------------------ + verify-tag: + runs-on: ubuntu-24.04 + permissions: + contents: read + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + + - name: Verify tag is on main branch + run: | + git fetch origin main + if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then + echo "ERROR: Tag $GITHUB_REF_NAME does not point to a commit on main." + echo "Release tags must be created on the main branch." + exit 1 + fi + echo "OK: Tag $GITHUB_REF_NAME is on main." + # ------------------------------------------------------------------ # Job 1: Build binaries, generate SBOMs, create GitHub Release # ------------------------------------------------------------------ goreleaser: runs-on: ubuntu-24.04 + needs: verify-tag permissions: contents: write # create release + upload assets - packages: write # push Helm OCI chart - id-token: write # Cosign keyless signing (Sigstore OIDC) outputs: version: ${{ steps.version.outputs.version }} hashes: ${{ steps.hash.outputs.hashes }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 # GoReleaser needs full history for changelog - - uses: actions/setup-go@v5 + - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version: "1.26.2" cache: true @@ -53,7 +85,7 @@ jobs: # GoReleaser builds cross-platform binaries and creates the # GitHub Release with archives and checksums. - - uses: goreleaser/goreleaser-action@v6 + - uses: goreleaser/goreleaser-action@9ed2f89a662bf1735a48bc8557fd212fa902bebf # v6.1.0 id: goreleaser with: distribution: goreleaser @@ -73,31 +105,44 @@ jobs: # uploaded alongside the release archives so consumers can # verify the dependency tree. - name: Generate SBOMs (syft) - uses: anchore/sbom-action@v0 + uses: anchore/sbom-action@f325610c9f50a54015d37c8d16cb3b0e2c8f4de0 # v0.18.0 with: path: ./dist output-file: dist/lightweightauth_${{ steps.version.outputs.version }}.sbom.json format: spdx-json - name: Upload SBOMs to release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2.2.1 with: files: dist/*.sbom.json env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # Cosign keyless signing of the checksums file. Verifiers can - # check provenance without any out-of-band key exchange. + # ------------------------------------------------------------------ + # Job 2: Sign release checksums with Cosign (keyless) + # ------------------------------------------------------------------ + sign: + runs-on: ubuntu-24.04 + needs: goreleaser + permissions: + contents: write # upload signature files to release + id-token: write # mint OIDC token for Sigstore + steps: + - name: Download release asset (checksums.txt) + run: | + mkdir -p dist + gh release download "$GITHUB_REF_NAME" --pattern "checksums.txt" --dir dist/ + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Install Cosign - uses: sigstore/cosign-installer@v3 + uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.0 - name: Sign checksums with Cosign (keyless) run: cosign sign-blob --yes dist/checksums.txt --output-signature dist/checksums.txt.sig --output-certificate dist/checksums.txt.pem - env: - COSIGN_EXPERIMENTAL: "1" - name: Upload signature + cert to release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2.2.1 with: files: | dist/checksums.txt.sig @@ -106,7 +151,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # ------------------------------------------------------------------ - # Job 2: SLSA provenance for Go binaries (level 3) + # Job 3: SLSA provenance for Go binaries (level 3) # ------------------------------------------------------------------ provenance: needs: goreleaser @@ -120,7 +165,7 @@ jobs: upload-assets: true # ------------------------------------------------------------------ - # Job 3: Package and push Helm chart as OCI artifact + # Job 4: Package and push Helm chart as OCI artifact # ------------------------------------------------------------------ helm: runs-on: ubuntu-24.04 @@ -128,11 +173,12 @@ jobs: permissions: contents: read packages: write + id-token: write # Cosign chart signing steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Install Helm - uses: azure/setup-helm@v4 + uses: azure/setup-helm@b9e51907a09c216f16ebe8536097933489208112 # v4.3.0 with: version: "v3.16.0" @@ -156,9 +202,7 @@ jobs: # Sign the Helm chart OCI artifact with Cosign so consumers can # verify the chart was published by this workflow. - name: Install Cosign - uses: sigstore/cosign-installer@v3 + uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.0 - name: Sign Helm OCI artifact (keyless) run: cosign sign --yes ${{ env.CHART_REPO }}/lightweightauth:${{ needs.goreleaser.outputs.version }} - env: - COSIGN_EXPERIMENTAL: "1" diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 5731edd..b9511c8 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -24,6 +24,9 @@ builds: goarch: - amd64 - arm64 + flags: + - -buildmode=pie + - -trimpath ldflags: - -s -w - -X github.com/mikeappsec/lightweightauth/pkg/buildinfo.Version={{ .Version }} @@ -42,6 +45,9 @@ builds: goarch: - amd64 - arm64 + flags: + - -buildmode=pie + - -trimpath ldflags: - -s -w - -X github.com/mikeappsec/lightweightauth/pkg/buildinfo.Version={{ .Version }} diff --git a/docs/operations/supply-chain.md b/docs/operations/supply-chain.md index 2b7e55e..538c885 100644 --- a/docs/operations/supply-chain.md +++ b/docs/operations/supply-chain.md @@ -71,9 +71,14 @@ to an internal registry. ```bash VERSION=1.2.0 -# Pull from public registry (on a connected host) -crane pull ghcr.io/mikeappsec/lightweightauth:${VERSION} lwauth-${VERSION}.tar -crane pull ghcr.io/mikeappsec/lightweightauth:${VERSION}-fips lwauth-${VERSION}-fips.tar +# Resolve tags to immutable digests first (prevents tag-swap attacks +# between verification and pull). +DIGEST=$(crane digest ghcr.io/mikeappsec/lightweightauth:${VERSION}) +DIGEST_FIPS=$(crane digest ghcr.io/mikeappsec/lightweightauth:${VERSION}-fips) + +# Pull by digest from public registry (on a connected host) +crane pull ghcr.io/mikeappsec/lightweightauth@${DIGEST} lwauth-${VERSION}.tar +crane pull ghcr.io/mikeappsec/lightweightauth@${DIGEST_FIPS} lwauth-${VERSION}-fips.tar # Transfer tarballs to air-gapped network, then push crane push lwauth-${VERSION}.tar registry.internal/lwauth/lightweightauth:${VERSION}