diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 058879c9..38b7a3e3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,18 +39,27 @@ jobs: - name: Build application run: pnpm run build - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to Docker Hub - id: docker-login - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + # The Docker build/push that used to live here has been REMOVED. + # + # It pushed nself/nself-admin:, :., : and + # :latest with `push: true` and NO vulnerability scan, on the same + # `push: tags: v*` trigger as docker-publish.yml. docker-publish.yml has a + # Trivy CRITICAL gate; this workflow did not. So every tag push raced two + # publishers, and the ungated one could ship an image the gate had just + # rejected — which makes the gate decorative. + # + # That is exactly what happened on v1.3.6 (2026-09-12): docker-publish + # correctly blocked on four CRITICAL Go stdlib CVEs in mkcert, while this + # workflow was mid-push of the same image and had to be cancelled by hand + # to keep :latest from moving to a known-vulnerable build. + # + # Image publication now belongs to docker-publish.yml alone — it owns the + # Trivy gate, the multi-arch manifest verification, and the NSELF_VERSION + # build-arg that this job did not even pass. This workflow owns the GitHub + # Release only. Do not reintroduce a push here without the gate. + # + # Security-Always-Free doctrine: a security gate that another workflow can + # walk around is not a gate. - name: Extract version from tag id: version @@ -60,36 +69,6 @@ jobs: echo "major=$(echo $VERSION | cut -d. -f1)" >> $GITHUB_OUTPUT echo "minor=$(echo $VERSION | cut -d. -f1-2)" >> $GITHUB_OUTPUT - - name: Extract metadata - if: steps.docker-login.outcome == 'success' - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.DOCKER_IMAGE }} - tags: | - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} - type=raw,value=latest - - - name: Build and push Docker image - if: steps.docker-login.outcome == 'success' - uses: docker/build-push-action@v5 - with: - context: . - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - platforms: linux/amd64,linux/arm64 - cache-from: type=registry,ref=${{ env.DOCKER_IMAGE }}:buildcache - cache-to: type=registry,ref=${{ env.DOCKER_IMAGE }}:buildcache,mode=max - - - name: Docker Hub credentials not configured - if: steps.docker-login.outcome != 'success' - run: | - echo "::warning::Docker Hub credentials not configured. Skipping Docker image build." - echo "To enable Docker image publishing, add DOCKERHUB_USERNAME and DOCKERHUB_TOKEN secrets to this repository." - - name: Generate changelog id: changelog uses: metcalfc/changelog-generator@v4.1.0 diff --git a/.trivyignore.yaml b/.trivyignore.yaml index 9d1bff71..5b7f7fc2 100644 --- a/.trivyignore.yaml +++ b/.trivyignore.yaml @@ -5,7 +5,15 @@ secrets: - id: gcp-service-account + # Trivy reports image paths with a LEADING SLASH — the gate log shows + # "/app/.next/server/app/cloud/gcp/page.js". These entries were written + # without it, so they matched nothing and this finding kept blocking the + # publish even though the suppression was in place. Both forms are listed: + # the absolute form is what the image scan emits, the relative form is what + # a filesystem scan of the repo would emit. paths: + - "/app/.next/server/app/cloud/gcp/page.js" + - "/app/.next/static/chunks/app/cloud/gcp/*.js" - "app/.next/server/app/cloud/gcp/page.js" - "app/.next/static/chunks/app/cloud/gcp/*.js" statement: >- diff --git a/Dockerfile b/Dockerfile index b1570bd7..a9d47ac0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,29 @@ +# mkcert, built from source rather than taken from upstream's pre-built binary. +# +# The upstream binary (v1.4.4, published 2022-04-26) is compiled with Go 1.18, +# and Trivy's CRITICAL gate flagged four Go stdlib vulnerabilities baked into it: +# CVE-2023-24538, CVE-2023-24540, CVE-2024-24790 and CVE-2025-68121. That gate +# blocked every docker-publish run, which is why Docker Hub sat at 1.0.13 while +# the CLI reached 1.3.6. +# +# There is no newer mkcert release to bump to — v1.4.4 IS the latest and has been +# since 2022. So compile the same source with a current Go toolchain: identical +# mkcert behaviour, patched stdlib. +# +# --platform=$BUILDPLATFORM + GOOS/GOARCH cross-compilation keeps this stage +# native on the builder instead of emulated per-arch under QEMU. `go build -o` is +# used rather than `go install` because when cross-compiling `go install` writes +# to /go/bin/${GOOS}_${GOARCH}/ rather than /go/bin, and the COPY would silently +# miss it. +FROM --platform=$BUILDPLATFORM golang:1.27-alpine AS mkcert-builder +ARG TARGETOS +ARG TARGETARCH +RUN apk add --no-cache git +RUN git clone --depth 1 --branch v1.4.4 https://github.com/FiloSottile/mkcert /src +WORKDIR /src +RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ + go build -trimpath -ldflags "-s -w" -o /out/mkcert . + # Multi-stage production Dockerfile for nself-admin # Optimized for minimal size with standalone Next.js build # Multi-platform support: linux/amd64, linux/arm64 @@ -87,15 +113,11 @@ RUN apk add --no-cache \ /usr/local/bin/npm \ /usr/local/bin/npx -# Install mkcert for local SSL certificate generation -# Using pre-built binary for Alpine Linux -RUN ARCH=$(uname -m) && \ - if [ "$ARCH" = "x86_64" ]; then MKCERT_ARCH="amd64"; \ - elif [ "$ARCH" = "aarch64" ]; then MKCERT_ARCH="arm64"; \ - else MKCERT_ARCH="amd64"; fi && \ - curl -fsSL "https://github.com/FiloSottile/mkcert/releases/download/v1.4.4/mkcert-v1.4.4-linux-${MKCERT_ARCH}" \ - -o /usr/local/bin/mkcert && \ - chmod +x /usr/local/bin/mkcert +# Install mkcert for local SSL certificate generation. +# Built from source in the mkcert-builder stage at the top of this file — see +# there for why the upstream pre-built binary cannot be used. +COPY --from=mkcert-builder /out/mkcert /usr/local/bin/mkcert +RUN chmod +x /usr/local/bin/mkcert # Install nself CLI pre-built binary ARG NSELF_VERSION=1.3.6