From 47188966f25e02526c9d236752ac6b9d12b9c43a Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Sat, 12 Sep 2026 12:17:23 -0400 Subject: [PATCH 1/2] fix(docker): build mkcert from source; correct the trivyignore paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The v1.3.6 tag push reached the Trivy CRITICAL gate and was blocked, so nothing was published. Two separate causes, neither of them the tar CVE that was fixed in d19a32c (that one is genuinely gone). 1. usr/local/bin/mkcert carries four CRITICAL Go stdlib vulnerabilities: CVE-2023-24538, CVE-2023-24540, CVE-2024-24790 and CVE-2025-68121. The upstream pre-built binary is v1.4.4, published 2022-04-26 and compiled with Go 1.18. There is no newer release to bump to — v1.4.4 IS the latest and has been for four years. So mkcert is now compiled from that same source with Go 1.27: identical behaviour, patched stdlib. The stage uses --platform=$BUILDPLATFORM with GOOS/GOARCH cross-compilation so it stays native on the builder rather than running under QEMU once per architecture, and `go build -o` rather than `go install`, because when cross-compiling `go install` writes to /go/bin/${GOOS}_${GOARCH}/ and the COPY would silently miss it. 2. The gcp-service-account suppression never matched. Trivy reports image paths with a leading slash — the gate log shows "/app/.next/server/app/cloud/gcp/page.js" — and .trivyignore.yaml listed the paths without one. The analysis behind that entry was right; only the paths were wrong. Both forms are now listed, so it works for an image scan and a filesystem scan alike. Still scoped to the two GCP bundles, so a real credential anywhere else in the image still fails the gate. Verified with `docker build --check` (clean, and it resolves golang:1.27-alpine). A full local build was skipped deliberately: swap was at 71%, above the 50% throttle in the local-resource-ceiling rule, and this machine has panicked from concurrent heavy compiles before. CI does the real build. Note the gate placement: Trivy only runs inside docker-publish.yml, which runs on tag push / dispatch / manual — never on a pull request. So image CVEs cannot be caught before merge; they surface only when a publish is attempted. That is how four CRITICALs sat undetected. Worth fixing separately. --- .trivyignore.yaml | 8 ++++++++ Dockerfile | 40 +++++++++++++++++++++++++++++++--------- 2 files changed, 39 insertions(+), 9 deletions(-) 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 From 8d2f606dbed358064ed0c3c3b4e80eeb112cf22e Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Sat, 12 Sep 2026 12:18:44 -0400 Subject: [PATCH 2/2] fix(ci): remove the ungated Docker push from release.yml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit release.yml 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 of the same image, and the ungated one could ship a build the gate had just rejected. A security gate another workflow can walk around is not a gate. That is not hypothetical. On the v1.3.6 tag push (2026-09-12) docker-publish correctly blocked on four CRITICAL Go stdlib CVEs in mkcert, while this workflow was already mid-push of the same image. It had to be cancelled by hand to stop :latest moving to a known-vulnerable build. Verified afterwards that no version tag landed: :latest still reads 2026-06-18, and 1.3.6 / 1.3 were never created. Image publication now belongs to docker-publish.yml alone. It owns the Trivy gate, the multi-arch manifest verification (S49-T06), and the NSELF_VERSION build-arg that this job never even passed — so the image it built pinned whatever the Dockerfile ARG defaulted to rather than the released CLI. This workflow keeps the GitHub Release: changelog, archive, release body, notification. `Extract version from tag` stays, since those steps use it. --- .github/workflows/release.yml | 63 ++++++++++++----------------------- 1 file changed, 21 insertions(+), 42 deletions(-) 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