From 3bbcf56edd6ea447fede20498afaf191ee09aac5 Mon Sep 17 00:00:00 2001 From: heznpc Date: Sat, 2 May 2026 10:07:51 +0900 Subject: [PATCH] feat: digest-pin Node 22, harden compose, capture build provenance + SBOM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Dockerfile: bump from node:20-alpine (EOL) to node:22-alpine pinned to sha256:cb15fca92530… Dependabot's docker ecosystem will refresh this. - docker-compose.yml: enable read_only rootfs, /tmp tmpfs, cap_drop ALL, no-new-privileges:true. These are starter defaults — comments explain how to relax them per-workload. - cd.yml: - Build job emits provenance + SBOM (build-push-action native flags) plus an explicit anchore/sbom-action CycloneDX export. - actions/attest-build-provenance@v3 signs the digest and pushes the attestation to the registry, getting us SLSA Build L3 alignment. - VPS deploy IMAGE switches from tag-based (":latest" / ":${version}") to digest-based (@${steps.build.outputs.digest}) so the rollout pulls exactly the artifact we just attested. - permissions: + id-token:write, attestations:write (required by the attestation action). --- .github/workflows/cd.yml | 29 ++++++++++++++++++++++++++--- Dockerfile | 7 ++++++- docker-compose.yml | 9 +++++++++ 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 6e81e55..859e545 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -11,8 +11,10 @@ concurrency: cancel-in-progress: false permissions: - contents: write - packages: write + contents: write # Create the GitHub Release + packages: write # Push to GHCR + id-token: write # OIDC for actions/attest-build-provenance + attestations: write # Persist the build provenance attestation jobs: ci: @@ -70,6 +72,7 @@ jobs: uses: docker/setup-buildx-action@v4 - name: Build and push + id: build uses: docker/build-push-action@v7 with: context: . @@ -79,6 +82,23 @@ jobs: ghcr.io/${{ github.repository }}:${{ steps.version.outputs.version }} cache-from: type=gha cache-to: type=gha,mode=max + provenance: true + sbom: true + + - name: Generate SBOM (CycloneDX) + uses: anchore/sbom-action@v0 + with: + image: ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }} + format: cyclonedx-json + output-file: sbom.cdx.json + upload-artifact: true + + - name: Attest build provenance + uses: actions/attest-build-provenance@v3 + with: + subject-name: ghcr.io/${{ github.repository }} + subject-digest: ${{ steps.build.outputs.digest }} + push-to-registry: true - name: Preflight — verify ~/.env.app on VPS uses: appleboy/ssh-action@v1 @@ -117,7 +137,10 @@ jobs: - name: Deploy to VPS via SSH uses: appleboy/ssh-action@v1 env: - IMAGE: ghcr.io/${{ github.repository }}:${{ steps.version.outputs.version }} + # Pin by digest, not tag — guarantees the VPS pulls exactly what we just + # built. A tag-based deploy can race a second concurrent build that + # rewrites :latest / : mid-rollout. + IMAGE: ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }} PORT: ${{ secrets.APP_PORT || '3000' }} with: host: ${{ secrets.VPS_HOST }} diff --git a/Dockerfile b/Dockerfile index 5e134f9..935e758 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,17 @@ # Example: Node.js application # Replace with your own Dockerfile for other languages # See docs/DOCKERFILE_EXAMPLES.md for Python, Go, and more +# +# Pinned to a digest for reproducibility. Dependabot's docker ecosystem +# refreshes this on a schedule. Node 20 reached EOL on 2026-04-30, so +# this targets Node 22 (active LTS through 2027-04). -FROM node:20-alpine +FROM node:22-alpine@sha256:cb15fca92530d7ac113467696cf1001208dac49c3c64355fd1348c11a88ddf8f WORKDIR /app COPY --chown=node:node app/ . +# Run as the non-root `node` user that ships with the official image. USER node EXPOSE 3000 diff --git a/docker-compose.yml b/docker-compose.yml index fcc2f8a..1934b75 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,6 +10,15 @@ services: # Uncomment for apps with dependencies (node_modules, vendor, venv, etc.): # - /app/node_modules restart: unless-stopped + # Hardening defaults — override per workload if your app actually needs the + # capability (e.g. binding to ports <1024 → keep `cap_add: [NET_BIND_SERVICE]`). + read_only: true + tmpfs: + - /tmp + cap_drop: + - ALL + security_opt: + - no-new-privileges:true healthcheck: test: ["CMD-SHELL", "wget -qO /dev/null http://localhost:${PORT:-3000}/health || exit 1"] interval: 10s