diff --git a/.github/workflows/docker-rds-images.yml b/.github/workflows/docker-rds-images.yml index fe2b4384..7594235e 100644 --- a/.github/workflows/docker-rds-images.yml +++ b/.github/workflows/docker-rds-images.yml @@ -116,6 +116,7 @@ jobs: permissions: contents: read packages: write + id-token: write strategy: fail-fast: false matrix: @@ -170,10 +171,83 @@ jobs: - name: Create manifest list and push working-directory: /tmp/digests + id: manifest run: | + set -euo pipefail docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ $(printf '${{ env.IMAGE_BASE }}@sha256:%s ' *) + # Resolve the digest of the freshly-pushed manifest list so + # cosign can sign the immutable reference rather than a tag + # that could be moved out from under us. + DIGEST=$(docker buildx imagetools inspect \ + "${IMAGE_BASE}:${{ steps.meta.outputs.version }}" \ + --format '{{ .Manifest.Digest }}') + echo "digest=$DIGEST" >> "$GITHUB_OUTPUT" - name: Inspect image run: | docker buildx imagetools inspect ${{ env.IMAGE_BASE }}:${{ steps.meta.outputs.version }} + + - name: Install cosign + uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3.8.1 + with: + cosign-release: v2.4.1 + + - name: Sign image with cosign (keyless, GitHub OIDC) + env: + COSIGN_EXPERIMENTAL: "1" + run: | + cosign sign --yes "${IMAGE_BASE}@${{ steps.manifest.outputs.digest }}" + + scan: + if: github.event_name != 'pull_request' + runs-on: ubuntu-24.04 + needs: merge + permissions: + contents: read + packages: read + strategy: + fail-fast: false + matrix: + target: + - { engine: postgres, version: "13" } + - { engine: postgres, version: "14" } + - { engine: postgres, version: "15" } + - { engine: postgres, version: "16" } + - { engine: mysql, version: "8.0" } + - { engine: mariadb, version: "10.6" } + - { engine: mariadb, version: "10.11" } + - { engine: mariadb, version: "11.4" } + env: + IMAGE_BASE: ghcr.io/${{ github.repository_owner }}/fakecloud-${{ matrix.target.engine }} + + steps: + - name: Log in to GitHub Container Registry + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Resolve short SHA + id: sha + run: echo "short=$(echo "${{ github.sha }}" | cut -c1-7)" >> "$GITHUB_OUTPUT" + + - name: Extract metadata + id: meta + uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5 + with: + images: ${{ env.IMAGE_BASE }} + tags: | + type=semver,pattern=${{ matrix.target.version }}-{{version}} + type=raw,value=${{ matrix.target.version }}-dev-${{ steps.sha.outputs.short }},enable=${{ github.event_name == 'workflow_dispatch' }} + + - name: Trivy vulnerability scan + uses: aquasecurity/trivy-action@6c175e9c4083a92bbca2f9724c8a5e33bc2d97a5 # 0.30.0 + with: + image-ref: ${{ env.IMAGE_BASE }}:${{ steps.meta.outputs.version }} + format: table + exit-code: '1' + ignore-unfixed: true + vuln-type: os,library + severity: CRITICAL,HIGH diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index b0076a07..a4ff79cd 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -73,6 +73,7 @@ jobs: permissions: contents: read packages: write + id-token: write steps: - name: Download digests @@ -105,11 +106,62 @@ jobs: type=sha - name: Create manifest list and push + id: manifest working-directory: /tmp/digests run: | + set -euo pipefail docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ $(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *) + DIGEST=$(docker buildx imagetools inspect \ + "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}" \ + --format '{{ .Manifest.Digest }}') + echo "digest=$DIGEST" >> "$GITHUB_OUTPUT" - name: Inspect image run: | docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} + + - name: Install cosign + uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3.8.1 + with: + cosign-release: v2.4.1 + + - name: Sign image with cosign (keyless, GitHub OIDC) + env: + COSIGN_EXPERIMENTAL: "1" + run: | + cosign sign --yes "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.manifest.outputs.digest }}" + + scan: + runs-on: ubuntu-24.04 + needs: merge + permissions: + contents: read + packages: read + + steps: + - name: Log in to GitHub Container Registry + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=semver,pattern={{version}} + type=raw,value=latest,enable={{is_default_branch}} + + - name: Trivy vulnerability scan + uses: aquasecurity/trivy-action@6c175e9c4083a92bbca2f9724c8a5e33bc2d97a5 # 0.30.0 + with: + image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} + format: table + exit-code: '1' + ignore-unfixed: true + vuln-type: os,library + severity: CRITICAL,HIGH diff --git a/README.md b/README.md index 1e7cbd0b..f1045c9b 100644 --- a/README.md +++ b/README.md @@ -215,6 +215,18 @@ Use fakecloud as a local AWS emulator for integration tests. - **[Reference](https://fakecloud.dev/docs/reference)** — configuration, introspection endpoints, persistence - **[Blog](https://fakecloud.dev/blog)** — essays and hot takes on testing, AWS, and AI-assisted development +### Verifying image signatures + +Every published image (`fakecloud`, `fakecloud-postgres`, `fakecloud-mysql`, `fakecloud-mariadb`) is Trivy-scanned and cosign-signed via GitHub OIDC. Verify before pulling: + +```sh +cosign verify ghcr.io/faiscadev/fakecloud:latest \ + --certificate-identity-regexp '^https://github\.com/faiscadev/fakecloud/' \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com +``` + +See [security docs](https://fakecloud.dev/docs/reference/security/#image-supply-chain-cosign--trivy) for details. + ## Contributing Contributions welcome. Fork, branch, write tests, open a PR. diff --git a/website/content/docs/reference/security.md b/website/content/docs/reference/security.md index f095a486..16c25b39 100644 --- a/website/content/docs/reference/security.md +++ b/website/content/docs/reference/security.md @@ -264,6 +264,23 @@ AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= \ # -> AccessDeniedException ``` +## Image supply-chain (cosign + Trivy) + +Every published container image — `ghcr.io/faiscadev/fakecloud` and the prebuilt RDS support images (`fakecloud-postgres`, `fakecloud-mysql`, `fakecloud-mariadb`) — is: + +1. **Scanned** by [Trivy](https://github.com/aquasecurity/trivy) for `CRITICAL`/`HIGH` OS and library vulnerabilities (`ignore-unfixed: true`). The release fails closed if any are found. +2. **Signed** with [cosign](https://github.com/sigstore/cosign) keyless mode using the GitHub Actions OIDC token, so attestations are anchored to the workflow that built the image — no key management, no detached secret. + +To verify an image before pulling: + +```bash +cosign verify ghcr.io/faiscadev/fakecloud-postgres:16-0.13.1 \ + --certificate-identity-regexp '^https://github\.com/faiscadev/fakecloud/' \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com +``` + +Same shape works for `fakecloud-mysql`, `fakecloud-mariadb`, and the main `fakecloud` image. A successful verification means the image was built by a workflow run in the `faiscadev/fakecloud` repository — not republished by anyone else. + ## See also - [Limitations](@/docs/reference/limitations.md) — what fakecloud doesn't do at all