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
updates:
- package-ecosystem: "github-actions"
directory: "/"
directories:
- "/"
- "/.github/actions/sign-image"
schedule:
interval: "weekly"
- package-ecosystem: gomod
+20 -8
View File
@@ -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 }}
@@ -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 }}
+29 -10
View File
@@ -441,14 +441,18 @@ 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
@@ -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 }}
@@ -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 }}
@@ -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
@@ -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 }}
+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
```bash