From a8f763e717a2c39b4fd5ad45eb817d2867f1297c Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Thu, 3 Sep 2026 11:52:10 -0700 Subject: [PATCH] Sign the published Docker images with cosign (#11129) * ci: composite action that signs and verifies an image with cosign Keyless, by digest, with a verification pass against the calling workflow's own identity right after signing. Signatures use the .sig tag layout rather than the OCI-referrer bundle cosign 3 writes by default, since that is what the verifiers people run today read. Dependabot is pointed at the action so the cosign-installer pin keeps moving. Claude-Session: https://claude.ai/code/session_01A5zMqzaUg1Snur4Yg8xJGa * docker release: sign every variant on both registries The merge job signs each variant's multi-arch index on GHCR and Docker Hub once the tag exists, recursively so the platform images are covered too. latest re-tags the same manifest and inherits the signature. Claude-Session: https://claude.ai/code/session_01A5zMqzaUg1Snur4Yg8xJGa * docker dev: sign the dev image Claude-Session: https://claude.ai/code/session_01A5zMqzaUg1Snur4Yg8xJGa * docker latest: sign a latest rebuilt by hand Claude-Session: https://claude.ai/code/session_01A5zMqzaUg1Snur4Yg8xJGa * docker release: sign the foundationdb image Claude-Session: https://claude.ai/code/session_01A5zMqzaUg1Snur4Yg8xJGa * docker: sign the per-version foundationdb and rocksdb builds They push to the same repository as the releases, so an admission policy that verifies chrislusf/seaweedfs would otherwise reject them. Claude-Session: https://claude.ai/code/session_01A5zMqzaUg1Snur4Yg8xJGa * docker: document image signature verification Claude-Session: https://claude.ai/code/session_01A5zMqzaUg1Snur4Yg8xJGa * ci: pin the actions the signing jobs newly run by commit These run with registry credentials and the OIDC token that signs under the repository's identity, so a retargeted tag upstream must not be able to reach them. Claude-Session: https://claude.ai/code/session_01A5zMqzaUg1Snur4Yg8xJGa * docker latest: pass the dispatch tag through env, not the script Claude-Session: https://claude.ai/code/session_01A5zMqzaUg1Snur4Yg8xJGa * docker: complete Kyverno policy, digest note, identity scope Claude-Session: https://claude.ai/code/session_01A5zMqzaUg1Snur4Yg8xJGa * docker latest: keep the dispatch tag out of the manifest script too The step predates signing, but the job now holds the OIDC identity. Claude-Session: https://claude.ai/code/session_01A5zMqzaUg1Snur4Yg8xJGa * ci: pin every action in the jobs that sign The jobs that hold the OIDC identity run these with registry credentials, so a retargeted tag upstream must not reach them. Claude-Session: https://claude.ai/code/session_01A5zMqzaUg1Snur4Yg8xJGa * docker release: copy and sign the digest the run created, pin the rest crane copy and the signature both resolved the tag, which another publisher could move between the two steps. The index digest is read once, right after it is created, and the Docker Hub copy and both signatures use it. The manual latest rebuild gets the same treatment. The actions in these jobs are pinned to commits, crane to v0.22.0 by checksum, and the sparse checkout no longer keeps the token. Claude-Session: https://claude.ai/code/session_01A5zMqzaUg1Snur4Yg8xJGa * docker latest: the signing job checks out the workflow's own commit The job only assembles and signs manifests, so nothing there needs the source_ref checkout; the local signing action now comes from the same revision as the workflow file that calls it. Claude-Session: https://claude.ai/code/session_01A5zMqzaUg1Snur4Yg8xJGa * docker release: take the index digest from the create result imagetools create writes the descriptor it pushed with --metadata-file (buildx 0.32+, the runners ship 0.36), so the digest no longer comes from re-resolving the tag even within the same step. Claude-Session: https://claude.ai/code/session_01A5zMqzaUg1Snur4Yg8xJGa --- .github/actions/sign-image/action.yml | 39 +++++++++++++++ .github/dependabot.yml | 4 +- .github/workflows/container_dev.yml | 28 +++++++---- .../container_foundationdb_version.yml | 21 ++++++--- .github/workflows/container_latest.yml | 41 +++++++++++----- .../container_release_foundationdb.yml | 22 ++++++--- .../workflows/container_release_unified.yml | 47 +++++++++++++++---- .../workflows/container_rocksdb_version.yml | 9 ++++ docker/README.md | 43 +++++++++++++++++ 9 files changed, 213 insertions(+), 41 deletions(-) create mode 100644 .github/actions/sign-image/action.yml diff --git a/.github/actions/sign-image/action.yml b/.github/actions/sign-image/action.yml new file mode 100644 index 000000000..32b7db04e --- /dev/null +++ b/.github/actions/sign-image/action.yml @@ -0,0 +1,39 @@ +name: Sign container images +description: > + Keyless cosign signature on each image, then a verification pass against the + identity the signature should carry, so a misconfigured job fails here and not + on someone's cluster. That identity is the calling workflow's own, + https://github.com///.github/workflows/@. + The calling job needs `id-token: write` and a registry login for every image. + +inputs: + images: + description: Image references by digest (name@sha256:...), whitespace separated. + required: true + +runs: + using: composite + steps: + - name: Install cosign + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 + + - name: Sign + shell: bash + env: + IMAGES: ${{ inputs.images }} + run: | + set -euo pipefail + # The .sig tag layout: the OCI-referrer bundle cosign 3 writes by default + # is not read by the Kyverno and policy-controller releases in use today. + cosign sign --yes --recursive --new-bundle-format=false $IMAGES + + - name: Verify + shell: bash + env: + IMAGES: ${{ inputs.images }} + run: | + set -euo pipefail + cosign verify \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + --certificate-identity "https://github.com/$GITHUB_WORKFLOW_REF" \ + $IMAGES diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 2e7a327bd..57d44a6ac 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,7 +1,9 @@ version: 2 updates: - package-ecosystem: "github-actions" - directory: "/" + directories: + - "/" + - "/.github/actions/sign-image" schedule: interval: "weekly" - package-ecosystem: gomod diff --git a/.github/workflows/container_dev.yml b/.github/workflows/container_dev.yml index 2aa5a702b..53d98b94d 100644 --- a/.github/workflows/container_dev.yml +++ b/.github/workflows/container_dev.yml @@ -101,13 +101,16 @@ jobs: build-dev-containers: needs: [build-rust-binaries] runs-on: [ubuntu-latest] + permissions: + contents: read + id-token: write steps: - name: Checkout - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Download pre-built Rust binaries - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: pattern: rust-bins-* merge-multiple: true @@ -136,7 +139,7 @@ jobs: - name: Docker meta id: docker_meta - uses: docker/metadata-action@v6 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6 with: images: | chrislusf/seaweedfs @@ -149,7 +152,7 @@ jobs: org.opencontainers.image.vendor=Chris Lu - name: Set up QEMU - uses: docker/setup-qemu-action@v4.2.0 + uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 - name: Create BuildKit config run: | @@ -159,28 +162,29 @@ jobs: EOF - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4 with: buildkitd-flags: "--debug" buildkitd-config: /tmp/buildkitd.toml - name: Login to Docker Hub if: github.event_name != 'pull_request' - uses: docker/login-action@v4.6.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Login to GHCR if: github.event_name != 'pull_request' - uses: docker/login-action@v4.6.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ secrets.GHCR_USERNAME }} password: ${{ secrets.GHCR_TOKEN }} - name: Build - uses: docker/build-push-action@v7 + id: build + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 with: context: ./docker push: ${{ github.event_name != 'pull_request' }} @@ -188,3 +192,11 @@ jobs: platforms: linux/amd64, linux/arm64 tags: ${{ steps.docker_meta.outputs.tags }} labels: ${{ steps.docker_meta.outputs.labels }} + + - name: Sign + if: github.event_name != 'pull_request' + uses: ./.github/actions/sign-image + with: + images: >- + chrislusf/seaweedfs@${{ steps.build.outputs.digest }} + ghcr.io/chrislusf/seaweedfs@${{ steps.build.outputs.digest }} diff --git a/.github/workflows/container_foundationdb_version.yml b/.github/workflows/container_foundationdb_version.yml index 5e2ac35e1..076cf3781 100644 --- a/.github/workflows/container_foundationdb_version.yml +++ b/.github/workflows/container_foundationdb_version.yml @@ -30,10 +30,13 @@ permissions: jobs: build-foundationdb-image: runs-on: ubuntu-latest + permissions: + contents: read + id-token: write steps: - name: Checkout - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: fetch-depth: 0 @@ -58,7 +61,7 @@ jobs: sudo ldconfig - name: Set up Go - uses: actions/setup-go@v7 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7 with: go-version-file: go.mod @@ -126,14 +129,14 @@ jobs: echo "seaweedfs_ref=$seaweed" >> "$GITHUB_OUTPUT" - name: Set up QEMU - uses: docker/setup-qemu-action@v4.2.0 + uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4 - name: Login to Docker Hub if: github.event_name != 'pull_request' - uses: docker/login-action@v4.6.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} @@ -154,7 +157,8 @@ jobs: fi - name: Build and push image - uses: docker/build-push-action@v7 + id: build + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 with: context: ./docker push: ${{ github.event_name != 'pull_request' }} @@ -171,3 +175,8 @@ jobs: org.opencontainers.image.description=SeaweedFS is a distributed storage system for blobs, objects, files, and data lake, to store and serve billions of files fast! org.opencontainers.image.vendor=Chris Lu + - name: Sign + if: github.event_name != 'pull_request' + uses: ./.github/actions/sign-image + with: + images: chrislusf/seaweedfs@${{ steps.build.outputs.digest }} diff --git a/.github/workflows/container_latest.yml b/.github/workflows/container_latest.yml index 2a611c5dd..cedfa27d7 100644 --- a/.github/workflows/container_latest.yml +++ b/.github/workflows/container_latest.yml @@ -441,15 +441,19 @@ jobs: runs-on: ubuntu-latest needs: [setup, build, trivy-scan] if: needs.setup.outputs.publish == 'true' && github.event_name != 'pull_request' + permissions: + contents: read + id-token: write strategy: matrix: variant: ${{ fromJSON(needs.setup.outputs.variants) }} steps: - - name: Checkout - uses: actions/checkout@v7 + - name: Checkout the signing action + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: - ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.source_ref || github.ref }} - + sparse-checkout: .github/actions + persist-credentials: false + - name: Configure variant id: config run: | @@ -461,19 +465,19 @@ jobs: - name: Docker meta id: docker_meta - uses: docker/metadata-action@v6 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6 with: images: | chrislusf/seaweedfs ghcr.io/chrislusf/seaweedfs tags: type=raw,value=${{ github.event_name == 'workflow_dispatch' && github.event.inputs.image_tag || 'latest' }},suffix=${{ steps.config.outputs.tag_suffix }} - name: Login to Docker Hub - uses: docker/login-action@v4.6.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Login to GHCR - uses: docker/login-action@v4.6.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ secrets.GHCR_USERNAME }} @@ -482,21 +486,29 @@ jobs: run: | # Install crane for efficient multi-arch image copying cd $(mktemp -d) - curl -sL "https://github.com/google/go-containerregistry/releases/latest/download/go-containerregistry_Linux_x86_64.tar.gz" | tar xz + curl -sLO https://github.com/google/go-containerregistry/releases/download/v0.22.0/go-containerregistry_Linux_x86_64.tar.gz + echo "edb74d53fad9a596860f59d1c5d04a43dfb5f441dc71f57060dd0bf39483c833 go-containerregistry_Linux_x86_64.tar.gz" | sha256sum -c - + tar xzf go-containerregistry_Linux_x86_64.tar.gz crane sudo mv crane /usr/local/bin/ crane version - name: Create and push manifest + id: manifest + env: + BASE_TAG: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.image_tag || 'latest' }} run: | SUFFIX="${{ steps.config.outputs.tag_suffix }}" - BASE_TAG="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.image_tag || 'latest' }}" # Create manifest on GHCR first (no rate limits) echo "Creating GHCR manifest (no rate limits)..." docker buildx imagetools create -t ghcr.io/chrislusf/seaweedfs:${BASE_TAG}${SUFFIX} \ + --metadata-file /tmp/manifest.json \ ghcr.io/chrislusf/seaweedfs:${BASE_TAG}${SUFFIX}-amd64 \ ghcr.io/chrislusf/seaweedfs:${BASE_TAG}${SUFFIX}-arm64 \ ghcr.io/chrislusf/seaweedfs:${BASE_TAG}${SUFFIX}-arm \ ghcr.io/chrislusf/seaweedfs:${BASE_TAG}${SUFFIX}-386 + # The copy and the signature below use the digest this run pushed, not whatever the tag points at by then. + DIGEST=$(jq -er '."containerimage.descriptor".digest' /tmp/manifest.json) + echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT" # Copy the complete multi-arch image from GHCR to Docker Hub # This only requires one pull from GHCR (no rate limit) and one push to Docker Hub @@ -532,10 +544,10 @@ jobs: # Use crane or skopeo to copy, fallback to docker if not available if command -v crane &> /dev/null; then echo "Using crane to copy..." - retry_with_backoff crane copy ghcr.io/chrislusf/seaweedfs:${BASE_TAG}${SUFFIX} chrislusf/seaweedfs:${BASE_TAG}${SUFFIX} + retry_with_backoff crane copy ghcr.io/chrislusf/seaweedfs@${DIGEST} chrislusf/seaweedfs:${BASE_TAG}${SUFFIX} elif command -v skopeo &> /dev/null; then echo "Using skopeo to copy..." - retry_with_backoff skopeo copy --all docker://ghcr.io/chrislusf/seaweedfs:${BASE_TAG}${SUFFIX} docker://chrislusf/seaweedfs:${BASE_TAG}${SUFFIX} + retry_with_backoff skopeo copy --all docker://ghcr.io/chrislusf/seaweedfs@${DIGEST} docker://chrislusf/seaweedfs:${BASE_TAG}${SUFFIX} else echo "Using docker buildx imagetools (pulling 4 images from Docker Hub)..." # Fallback: create manifest directly on Docker Hub (pulls from Docker Hub - rate limited) @@ -545,3 +557,10 @@ jobs: ghcr.io/chrislusf/seaweedfs:${BASE_TAG}${SUFFIX}-arm \ ghcr.io/chrislusf/seaweedfs:${BASE_TAG}${SUFFIX}-386 fi + + - name: Sign + uses: ./.github/actions/sign-image + with: + images: >- + ghcr.io/chrislusf/seaweedfs@${{ steps.manifest.outputs.digest }} + chrislusf/seaweedfs@${{ steps.manifest.outputs.digest }} diff --git a/.github/workflows/container_release_foundationdb.yml b/.github/workflows/container_release_foundationdb.yml index ebb098aab..73dae1142 100644 --- a/.github/workflows/container_release_foundationdb.yml +++ b/.github/workflows/container_release_foundationdb.yml @@ -21,15 +21,18 @@ jobs: build-large-release-container_foundationdb: runs-on: [ubuntu-latest] + permissions: + contents: read + id-token: write steps: - name: Checkout - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Docker meta id: docker_meta - uses: docker/metadata-action@v6 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6 with: images: | chrislusf/seaweedfs @@ -43,14 +46,14 @@ jobs: org.opencontainers.image.vendor=Chris Lu - name: Set up QEMU - uses: docker/setup-qemu-action@v4.2.0 + uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4 - name: Login to Docker Hub if: github.event_name != 'pull_request' - uses: docker/login-action@v4.6.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} @@ -65,7 +68,8 @@ jobs: fi - name: Build - uses: docker/build-push-action@v7 + id: build + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 with: context: ./docker push: ${{ github.event_name != 'pull_request' }} @@ -76,4 +80,10 @@ jobs: platforms: linux/amd64 tags: ${{ steps.docker_meta.outputs.tags }} labels: ${{ steps.docker_meta.outputs.labels }} + - + name: Sign + if: github.event_name != 'pull_request' + uses: ./.github/actions/sign-image + with: + images: chrislusf/seaweedfs@${{ steps.build.outputs.digest }} diff --git a/.github/workflows/container_release_unified.yml b/.github/workflows/container_release_unified.yml index 97caaa3a3..007b259f6 100644 --- a/.github/workflows/container_release_unified.yml +++ b/.github/workflows/container_release_unified.yml @@ -295,10 +295,14 @@ jobs: if-no-files-found: error retention-days: 1 - # Assemble each variant's per-platform digests into one tag, then mirror to Docker Hub. + # Assemble each variant's per-platform digests into one tag, mirror it to + # Docker Hub, and sign the result on both registries. merge: needs: [build] runs-on: ubuntu-latest + permissions: + contents: read + id-token: write strategy: fail-fast: false matrix: @@ -309,9 +313,16 @@ jobs: - { variant: large_disk_full, tag_suffix: _large_disk_full } - { variant: rocksdb, tag_suffix: _large_disk_rocksdb } steps: + - name: Checkout the signing action + if: github.event_name != 'workflow_dispatch' || github.event.inputs.variant == 'all' || github.event.inputs.variant == matrix.variant + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + sparse-checkout: .github/actions + persist-credentials: false + - name: Download digests if: github.event_name != 'workflow_dispatch' || github.event.inputs.variant == 'all' || github.event.inputs.variant == matrix.variant - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: pattern: digest-${{ matrix.variant }}-* merge-multiple: true @@ -319,11 +330,11 @@ jobs: - name: Set up Docker Buildx if: github.event_name != 'workflow_dispatch' || github.event.inputs.variant == 'all' || github.event.inputs.variant == matrix.variant - uses: docker/setup-buildx-action@v4 + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4 - name: Login to GHCR if: github.event_name != 'workflow_dispatch' || github.event.inputs.variant == 'all' || github.event.inputs.variant == matrix.variant - uses: docker/login-action@v4.6.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ secrets.GHCR_USERNAME }} @@ -331,16 +342,21 @@ jobs: - name: Create multi-arch tag on GHCR if: github.event_name != 'workflow_dispatch' || github.event.inputs.variant == 'all' || github.event.inputs.variant == matrix.variant + id: manifest working-directory: /tmp/digests run: | docker buildx imagetools create \ -t ${{ env.IMAGE }}:${{ env.RELEASE_TAG }}${{ matrix.tag_suffix }} \ + --metadata-file /tmp/manifest.json \ $(printf '${{ env.IMAGE }}@sha256:%s ' *) docker buildx imagetools inspect ${{ env.IMAGE }}:${{ env.RELEASE_TAG }}${{ matrix.tag_suffix }} + # The copy and the signature below use the digest this run pushed, not whatever the tag points at by then. + digest=$(jq -er '."containerimage.descriptor".digest' /tmp/manifest.json) + echo "digest=${digest}" >> "$GITHUB_OUTPUT" - name: Login to Docker Hub if: github.event_name != 'workflow_dispatch' || github.event.inputs.variant == 'all' || github.event.inputs.variant == matrix.variant - uses: docker/login-action@v4.6.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} @@ -349,7 +365,9 @@ jobs: if: github.event_name != 'workflow_dispatch' || github.event.inputs.variant == 'all' || github.event.inputs.variant == matrix.variant run: | cd $(mktemp -d) - curl -sL "https://github.com/google/go-containerregistry/releases/latest/download/go-containerregistry_Linux_x86_64.tar.gz" | tar xz + curl -sLO https://github.com/google/go-containerregistry/releases/download/v0.22.0/go-containerregistry_Linux_x86_64.tar.gz + echo "edb74d53fad9a596860f59d1c5d04a43dfb5f441dc71f57060dd0bf39483c833 go-containerregistry_Linux_x86_64.tar.gz" | sha256sum -c - + tar xzf go-containerregistry_Linux_x86_64.tar.gz crane sudo mv crane /usr/local/bin/ crane version @@ -380,10 +398,18 @@ jobs: echo "Copying ${{ matrix.variant }} from GHCR to Docker Hub..." retry_with_backoff crane copy \ - ${{ env.IMAGE }}:${{ env.RELEASE_TAG }}${{ matrix.tag_suffix }} \ + ${{ env.IMAGE }}@${{ steps.manifest.outputs.digest }} \ chrislusf/seaweedfs:${{ env.RELEASE_TAG }}${{ matrix.tag_suffix }} echo "Copied ${{ matrix.variant }} to Docker Hub" + - name: Sign ${{ matrix.variant }} + if: github.event_name != 'workflow_dispatch' || github.event.inputs.variant == 'all' || github.event.inputs.variant == matrix.variant + uses: ./.github/actions/sign-image + with: + images: >- + ${{ env.IMAGE }}@${{ steps.manifest.outputs.digest }} + chrislusf/seaweedfs@${{ steps.manifest.outputs.digest }} + # Report-only trivy scan: uploads fixable HIGH/CRITICAL findings to GitHub # Security for visibility, but never blocks the release. Releases (including # `latest`) ship regardless — vulnerabilities are tracked, not gated, since @@ -439,7 +465,8 @@ jobs: # image. crane tag adds an extra tag to an existing manifest — no rebuild, # no QEMU, no separate workflow. Replaces the old container_latest.yml # rebuild that often failed or lagged behind the release. Independent of - # trivy-scan: vuln findings are reported but do not block `latest`. + # trivy-scan: vuln findings are reported but do not block `latest`. The + # cosign signature is attached to the digest, so `latest` carries it too. tag-latest: runs-on: ubuntu-latest needs: [merge] @@ -468,7 +495,9 @@ jobs: - name: Install crane run: | cd $(mktemp -d) - curl -sL "https://github.com/google/go-containerregistry/releases/latest/download/go-containerregistry_Linux_x86_64.tar.gz" | tar xz + curl -sLO https://github.com/google/go-containerregistry/releases/download/v0.22.0/go-containerregistry_Linux_x86_64.tar.gz + echo "edb74d53fad9a596860f59d1c5d04a43dfb5f441dc71f57060dd0bf39483c833 go-containerregistry_Linux_x86_64.tar.gz" | sha256sum -c - + tar xzf go-containerregistry_Linux_x86_64.tar.gz crane sudo mv crane /usr/local/bin/ crane version diff --git a/.github/workflows/container_rocksdb_version.yml b/.github/workflows/container_rocksdb_version.yml index 0ab6eeb3e..f54190d10 100644 --- a/.github/workflows/container_rocksdb_version.yml +++ b/.github/workflows/container_rocksdb_version.yml @@ -22,6 +22,9 @@ permissions: jobs: build-rocksdb-image: runs-on: ubuntu-latest + permissions: + contents: read + id-token: write steps: - name: Checkout @@ -94,6 +97,7 @@ jobs: password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and push image + id: build uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v2 with: context: ./docker @@ -108,3 +112,8 @@ jobs: org.opencontainers.image.title=seaweedfs org.opencontainers.image.description=SeaweedFS is a distributed storage system for blobs, objects, files, and data lake, to store and serve billions of files fast! org.opencontainers.image.vendor=Chris Lu + + - name: Sign + uses: ./.github/actions/sign-image + with: + images: chrislusf/seaweedfs@${{ steps.build.outputs.digest }} diff --git a/docker/README.md b/docker/README.md index bec6c4bf9..731587ca1 100644 --- a/docker/README.md +++ b/docker/README.md @@ -31,6 +31,49 @@ docker compose -f seaweedfs-dev-compose.yml -p seaweedfs up ``` +## Verify an image signature + +Every image CI pushes to `chrislusf/seaweedfs` and `ghcr.io/chrislusf/seaweedfs` is signed with [cosign](https://docs.sigstore.dev/cosign/verifying/verify/), keyless, by the GitHub Actions workflow that built it, so there is no key to fetch or pin. The signature is attached to the image digest and covers the multi-arch index and each platform image in it; `latest` is the release image under another tag and verifies the same way. Images published before September 2026 predate signing. + +```bash +cosign verify \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + --certificate-identity-regexp '^https://github.com/seaweedfs/seaweedfs/\.github/workflows/container_release_unified\.yml@' \ + chrislusf/seaweedfs:latest +``` + +cosign prints the digest it verified. Deploy by that digest, or let an admission controller resolve the tag, so what runs is what was checked. + +The identity is `https://github.com/seaweedfs/seaweedfs/.github/workflows/@`. The ref is `refs/tags/` for a release and `refs/heads/master` when a variant was republished by hand. The workflow is `container_release_unified.yml` for the release images, `container_dev.yml` for `dev`, `container_latest.yml` for a `latest` rebuilt by hand, `container_release_foundationdb.yml` for the `_large_disk_foundationdb` release image, and `container_foundationdb_version.yml` or `container_rocksdb_version.yml` for the per-version builds. The regexp above accepts release images only; `container_[a-z_]+\.yml@` accepts everything this repository publishes, `dev` included. + +The same check as a Kyverno policy, release images only: + +```yaml +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: verify-seaweedfs-images +spec: + validationFailureAction: Enforce + webhookTimeoutSeconds: 30 + rules: + - name: signed-by-the-release-workflow + match: + any: + - resources: + kinds: + - Pod + verifyImages: + - imageReferences: + - "docker.io/chrislusf/seaweedfs:*" + - "ghcr.io/chrislusf/seaweedfs:*" + attestors: + - entries: + - keyless: + issuer: https://token.actions.githubusercontent.com + subject: https://github.com/seaweedfs/seaweedfs/.github/workflows/container_release_unified.yml@refs/tags/* +``` + ## Local Development ```bash