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
This commit is contained in:
Chris Lu
2026-09-03 11:52:10 -07:00
committed by GitHub
parent fe98520358
commit a8f763e717
9 changed files with 213 additions and 41 deletions
+39
View File
@@ -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/<owner>/<repo>/.github/workflows/<file>@<ref>.
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
+3 -1
View File
@@ -1,7 +1,9 @@
version: 2 version: 2
updates: updates:
- package-ecosystem: "github-actions" - package-ecosystem: "github-actions"
directory: "/" directories:
- "/"
- "/.github/actions/sign-image"
schedule: schedule:
interval: "weekly" interval: "weekly"
- package-ecosystem: gomod - package-ecosystem: gomod
+20 -8
View File
@@ -101,13 +101,16 @@ jobs:
build-dev-containers: build-dev-containers:
needs: [build-rust-binaries] needs: [build-rust-binaries]
runs-on: [ubuntu-latest] runs-on: [ubuntu-latest]
permissions:
contents: read
id-token: write
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v7 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Download pre-built Rust binaries - name: Download pre-built Rust binaries
uses: actions/download-artifact@v8 uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with: with:
pattern: rust-bins-* pattern: rust-bins-*
merge-multiple: true merge-multiple: true
@@ -136,7 +139,7 @@ jobs:
- name: Docker meta - name: Docker meta
id: docker_meta id: docker_meta
uses: docker/metadata-action@v6 uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
with: with:
images: | images: |
chrislusf/seaweedfs chrislusf/seaweedfs
@@ -149,7 +152,7 @@ jobs:
org.opencontainers.image.vendor=Chris Lu org.opencontainers.image.vendor=Chris Lu
- name: Set up QEMU - 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 - name: Create BuildKit config
run: | run: |
@@ -159,28 +162,29 @@ jobs:
EOF EOF
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4 uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4
with: with:
buildkitd-flags: "--debug" buildkitd-flags: "--debug"
buildkitd-config: /tmp/buildkitd.toml buildkitd-config: /tmp/buildkitd.toml
- name: Login to Docker Hub - name: Login to Docker Hub
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: docker/login-action@v4.6.0 uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GHCR - name: Login to GHCR
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: docker/login-action@v4.6.0 uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ secrets.GHCR_USERNAME }} username: ${{ secrets.GHCR_USERNAME }}
password: ${{ secrets.GHCR_TOKEN }} password: ${{ secrets.GHCR_TOKEN }}
- name: Build - name: Build
uses: docker/build-push-action@v7 id: build
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
with: with:
context: ./docker context: ./docker
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}
@@ -188,3 +192,11 @@ jobs:
platforms: linux/amd64, linux/arm64 platforms: linux/amd64, linux/arm64
tags: ${{ steps.docker_meta.outputs.tags }} tags: ${{ steps.docker_meta.outputs.tags }}
labels: ${{ steps.docker_meta.outputs.labels }} 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 }}
@@ -30,10 +30,13 @@ permissions:
jobs: jobs:
build-foundationdb-image: build-foundationdb-image:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v7 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -58,7 +61,7 @@ jobs:
sudo ldconfig sudo ldconfig
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v7 uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with: with:
go-version-file: go.mod go-version-file: go.mod
@@ -126,14 +129,14 @@ jobs:
echo "seaweedfs_ref=$seaweed" >> "$GITHUB_OUTPUT" echo "seaweedfs_ref=$seaweed" >> "$GITHUB_OUTPUT"
- name: Set up QEMU - 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 - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4 uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4
- name: Login to Docker Hub - name: Login to Docker Hub
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: docker/login-action@v4.6.0 uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
@@ -154,7 +157,8 @@ jobs:
fi fi
- name: Build and push image - name: Build and push image
uses: docker/build-push-action@v7 id: build
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
with: with:
context: ./docker context: ./docker
push: ${{ github.event_name != 'pull_request' }} 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.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 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 }}
+29 -10
View File
@@ -441,14 +441,18 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [setup, build, trivy-scan] needs: [setup, build, trivy-scan]
if: needs.setup.outputs.publish == 'true' && github.event_name != 'pull_request' if: needs.setup.outputs.publish == 'true' && github.event_name != 'pull_request'
permissions:
contents: read
id-token: write
strategy: strategy:
matrix: matrix:
variant: ${{ fromJSON(needs.setup.outputs.variants) }} variant: ${{ fromJSON(needs.setup.outputs.variants) }}
steps: steps:
- name: Checkout - name: Checkout the signing action
uses: actions/checkout@v7 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with: with:
ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.source_ref || github.ref }} sparse-checkout: .github/actions
persist-credentials: false
- name: Configure variant - name: Configure variant
id: config id: config
@@ -461,19 +465,19 @@ jobs:
- name: Docker meta - name: Docker meta
id: docker_meta id: docker_meta
uses: docker/metadata-action@v6 uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
with: with:
images: | images: |
chrislusf/seaweedfs chrislusf/seaweedfs
ghcr.io/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 }} 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 - name: Login to Docker Hub
uses: docker/login-action@v4.6.0 uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GHCR - name: Login to GHCR
uses: docker/login-action@v4.6.0 uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ secrets.GHCR_USERNAME }} username: ${{ secrets.GHCR_USERNAME }}
@@ -482,21 +486,29 @@ jobs:
run: | run: |
# Install crane for efficient multi-arch image copying # Install crane for efficient multi-arch image copying
cd $(mktemp -d) 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/ sudo mv crane /usr/local/bin/
crane version crane version
- name: Create and push manifest - name: Create and push manifest
id: manifest
env:
BASE_TAG: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.image_tag || 'latest' }}
run: | run: |
SUFFIX="${{ steps.config.outputs.tag_suffix }}" 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) # Create manifest on GHCR first (no rate limits)
echo "Creating GHCR manifest (no rate limits)..." echo "Creating GHCR manifest (no rate limits)..."
docker buildx imagetools create -t ghcr.io/chrislusf/seaweedfs:${BASE_TAG}${SUFFIX} \ 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}-amd64 \
ghcr.io/chrislusf/seaweedfs:${BASE_TAG}${SUFFIX}-arm64 \ ghcr.io/chrislusf/seaweedfs:${BASE_TAG}${SUFFIX}-arm64 \
ghcr.io/chrislusf/seaweedfs:${BASE_TAG}${SUFFIX}-arm \ ghcr.io/chrislusf/seaweedfs:${BASE_TAG}${SUFFIX}-arm \
ghcr.io/chrislusf/seaweedfs:${BASE_TAG}${SUFFIX}-386 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 # 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 # 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 # Use crane or skopeo to copy, fallback to docker if not available
if command -v crane &> /dev/null; then if command -v crane &> /dev/null; then
echo "Using crane to copy..." 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 elif command -v skopeo &> /dev/null; then
echo "Using skopeo to copy..." 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 else
echo "Using docker buildx imagetools (pulling 4 images from Docker Hub)..." echo "Using docker buildx imagetools (pulling 4 images from Docker Hub)..."
# Fallback: create manifest directly on Docker Hub (pulls from Docker Hub - rate limited) # 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}-arm \
ghcr.io/chrislusf/seaweedfs:${BASE_TAG}${SUFFIX}-386 ghcr.io/chrislusf/seaweedfs:${BASE_TAG}${SUFFIX}-386
fi fi
- name: Sign
uses: ./.github/actions/sign-image
with:
images: >-
ghcr.io/chrislusf/seaweedfs@${{ steps.manifest.outputs.digest }}
chrislusf/seaweedfs@${{ steps.manifest.outputs.digest }}
@@ -21,15 +21,18 @@ jobs:
build-large-release-container_foundationdb: build-large-release-container_foundationdb:
runs-on: [ubuntu-latest] runs-on: [ubuntu-latest]
permissions:
contents: read
id-token: write
steps: steps:
- -
name: Checkout name: Checkout
uses: actions/checkout@v7 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- -
name: Docker meta name: Docker meta
id: docker_meta id: docker_meta
uses: docker/metadata-action@v6 uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
with: with:
images: | images: |
chrislusf/seaweedfs chrislusf/seaweedfs
@@ -43,14 +46,14 @@ jobs:
org.opencontainers.image.vendor=Chris Lu org.opencontainers.image.vendor=Chris Lu
- -
name: Set up QEMU 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 name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4 uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4
- -
name: Login to Docker Hub name: Login to Docker Hub
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: docker/login-action@v4.6.0 uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
@@ -65,7 +68,8 @@ jobs:
fi fi
- -
name: Build name: Build
uses: docker/build-push-action@v7 id: build
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
with: with:
context: ./docker context: ./docker
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}
@@ -76,4 +80,10 @@ jobs:
platforms: linux/amd64 platforms: linux/amd64
tags: ${{ steps.docker_meta.outputs.tags }} tags: ${{ steps.docker_meta.outputs.tags }}
labels: ${{ steps.docker_meta.outputs.labels }} 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 }}
@@ -295,10 +295,14 @@ jobs:
if-no-files-found: error if-no-files-found: error
retention-days: 1 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: merge:
needs: [build] needs: [build]
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -309,9 +313,16 @@ jobs:
- { variant: large_disk_full, tag_suffix: _large_disk_full } - { variant: large_disk_full, tag_suffix: _large_disk_full }
- { variant: rocksdb, tag_suffix: _large_disk_rocksdb } - { variant: rocksdb, tag_suffix: _large_disk_rocksdb }
steps: 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 - name: Download digests
if: github.event_name != 'workflow_dispatch' || github.event.inputs.variant == 'all' || github.event.inputs.variant == matrix.variant 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: with:
pattern: digest-${{ matrix.variant }}-* pattern: digest-${{ matrix.variant }}-*
merge-multiple: true merge-multiple: true
@@ -319,11 +330,11 @@ jobs:
- name: Set up Docker Buildx - name: Set up Docker Buildx
if: github.event_name != 'workflow_dispatch' || github.event.inputs.variant == 'all' || github.event.inputs.variant == matrix.variant 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 - name: Login to GHCR
if: github.event_name != 'workflow_dispatch' || github.event.inputs.variant == 'all' || github.event.inputs.variant == matrix.variant 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: with:
registry: ghcr.io registry: ghcr.io
username: ${{ secrets.GHCR_USERNAME }} username: ${{ secrets.GHCR_USERNAME }}
@@ -331,16 +342,21 @@ jobs:
- name: Create multi-arch tag on GHCR - name: Create multi-arch tag on GHCR
if: github.event_name != 'workflow_dispatch' || github.event.inputs.variant == 'all' || github.event.inputs.variant == matrix.variant if: github.event_name != 'workflow_dispatch' || github.event.inputs.variant == 'all' || github.event.inputs.variant == matrix.variant
id: manifest
working-directory: /tmp/digests working-directory: /tmp/digests
run: | run: |
docker buildx imagetools create \ docker buildx imagetools create \
-t ${{ env.IMAGE }}:${{ env.RELEASE_TAG }}${{ matrix.tag_suffix }} \ -t ${{ env.IMAGE }}:${{ env.RELEASE_TAG }}${{ matrix.tag_suffix }} \
--metadata-file /tmp/manifest.json \
$(printf '${{ env.IMAGE }}@sha256:%s ' *) $(printf '${{ env.IMAGE }}@sha256:%s ' *)
docker buildx imagetools inspect ${{ env.IMAGE }}:${{ env.RELEASE_TAG }}${{ matrix.tag_suffix }} 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 - name: Login to Docker Hub
if: github.event_name != 'workflow_dispatch' || github.event.inputs.variant == 'all' || github.event.inputs.variant == matrix.variant 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: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} 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 if: github.event_name != 'workflow_dispatch' || github.event.inputs.variant == 'all' || github.event.inputs.variant == matrix.variant
run: | run: |
cd $(mktemp -d) 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/ sudo mv crane /usr/local/bin/
crane version crane version
@@ -380,10 +398,18 @@ jobs:
echo "Copying ${{ matrix.variant }} from GHCR to Docker Hub..." echo "Copying ${{ matrix.variant }} from GHCR to Docker Hub..."
retry_with_backoff crane copy \ 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 }} chrislusf/seaweedfs:${{ env.RELEASE_TAG }}${{ matrix.tag_suffix }}
echo "Copied ${{ matrix.variant }} to Docker Hub" 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 # Report-only trivy scan: uploads fixable HIGH/CRITICAL findings to GitHub
# Security for visibility, but never blocks the release. Releases (including # Security for visibility, but never blocks the release. Releases (including
# `latest`) ship regardless — vulnerabilities are tracked, not gated, since # `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, # image. crane tag adds an extra tag to an existing manifest — no rebuild,
# no QEMU, no separate workflow. Replaces the old container_latest.yml # no QEMU, no separate workflow. Replaces the old container_latest.yml
# rebuild that often failed or lagged behind the release. Independent of # 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: tag-latest:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [merge] needs: [merge]
@@ -468,7 +495,9 @@ jobs:
- name: Install crane - name: Install crane
run: | run: |
cd $(mktemp -d) 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/ sudo mv crane /usr/local/bin/
crane version crane version
@@ -22,6 +22,9 @@ permissions:
jobs: jobs:
build-rocksdb-image: build-rocksdb-image:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps: steps:
- name: Checkout - name: Checkout
@@ -94,6 +97,7 @@ jobs:
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push image - name: Build and push image
id: build
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v2 uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v2
with: with:
context: ./docker context: ./docker
@@ -108,3 +112,8 @@ jobs:
org.opencontainers.image.title=seaweedfs 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.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 org.opencontainers.image.vendor=Chris Lu
- name: Sign
uses: ./.github/actions/sign-image
with:
images: chrislusf/seaweedfs@${{ steps.build.outputs.digest }}
+43
View File
@@ -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/<workflow>@<ref>`. The ref is `refs/tags/<version>` 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 ## Local Development
```bash ```bash