diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index eed395c60..dab68fe8d 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -182,8 +182,11 @@ jobs: exit 1 } - # Note: the local `hadolint` pre-commit hook (.pre-commit-config.yaml:81) - # runs ghcr.io/hadolint/hadolint pinned by digest to v2.14.0. We do NOT + # Note: the local `hadolint` hook in .pre-commit-config.yaml (search for + # `id: hadolint`; no line number, because this comment has already gone + # stale twice as that file shifted) runs ghcr.io/hadolint/hadolint pinned + # by digest. The version lives in that entry and is the single source of + # truth; do not restate it here, or this comment rots again. We do NOT # install a host binary here — it would be dead code (never invoked by # the hook) AND a supply-chain hole (latest tag, no checksum). Note: # an earlier version of this comment claimed the hook already pinned diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c37ad9873..0cd4ffb32 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -67,21 +67,26 @@ repos: # The upstream hadolint-docker hook (hadolint/hadolint's own # .pre-commit-hooks.yaml) declares `entry: ghcr.io/hadolint/hadolint # hadolint` with no image tag, so Docker resolves it to `:latest` on every - # run -- independent of the `rev:` pin above, which only pins which - # commit of the *hook definition* is used, not the Docker image it runs. - # When upstream published hadolint 2.15.1 as `latest`, CI silently - # started linting with a newer ruleset (new DL3066/DL3025 findings on - # unchanged Dockerfiles) with no corresponding change in this repo. + # run -- independent of any `rev:` pin, which only pins which commit of the + # *hook definition* is used, not the Docker image it runs. When upstream + # published hadolint 2.15.1 as `latest`, CI silently started linting with a + # newer ruleset (new DL3066/DL3025 findings on unchanged Dockerfiles) with no + # corresponding change in this repo, and `main` went red (issue #1695). # - # Defined as a local hook instead, pinned to the immutable digest of the - # v2.14.0 image, so the linter version can only change via an explicit - # bump here. + # Defined as a local hook instead, pinned to an immutable digest, so the + # linter version can only change through an explicit edit to this line. + # + # Pinned to 2.15.1 -- the version that surfaced the findings -- rather than + # back to 2.14.0. Pinning to the older image would also have made CI green, + # but only by un-seeing findings that are real; the four it reported are + # fixed in the Dockerfiles rather than silenced. Bumping this digest is + # expected to require fixing whatever the new version finds. - repo: local hooks: - id: hadolint name: Lint Dockerfiles language: docker_image - entry: ghcr.io/hadolint/hadolint:v2.14.0@sha256:27086352fd5e1907ea2b934eb1023f217c5ae087992eb59fde121dce9c9ff21e hadolint + entry: ghcr.io/hadolint/hadolint:v2.15.1@sha256:32dac94127fd60b7b7e3fbfc65e1383b9b5e25c9bfd7b8536de7a539fe68a12d hadolint types: [dockerfile] # Markdown linting diff --git a/Dockerfile b/Dockerfile index cf499e9b1..6e398cef8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -145,8 +145,11 @@ COPY --from=frontend-builder --chown=cudly:cudly /frontend/dist /app/static COPY --chown=cudly:cudly scripts/entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh -# Switch to non-root user -USER cudly +# Switch to non-root user. Numeric form so the identity is resolvable without +# the image's /etc/passwd: Kubernetes `runAsNonRoot` and similar admission +# checks cannot verify a name-form USER and will refuse to start the pod. +# 1000:1000 is exactly the uid:gid created above, so this is a rename only. +USER 1000:1000 # Environment defaults ENV DB_MIGRATIONS_PATH=/app/migrations \ @@ -162,8 +165,14 @@ ENV DB_MIGRATIONS_PATH=/app/migrations \ EXPOSE 8080 # Health check (works for HTTP mode, ignored in Lambda mode) +# +# JSON (exec) form, but invoking /bin/sh explicitly: the `||` is a shell +# operator, so a bare exec-form list would hand `||` and `exit` to curl as +# literal arguments and the healthcheck would never report unhealthy correctly. +# This is the same process tree the shell form produced, written so the +# dependency on a shell is declared rather than implied. HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ - CMD curl -f http://localhost:8080/health || exit 1 + CMD ["/bin/sh", "-c", "curl -f http://localhost:8080/health || exit 1"] # Unified entrypoint handles both Lambda and HTTP modes ENTRYPOINT ["/entrypoint.sh"] diff --git a/Dockerfile.dev b/Dockerfile.dev index 22528951e..ff3ccb8fb 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -55,11 +55,16 @@ RUN if [ ! -f .air.toml ]; then air init; fi EXPOSE 8080 -# Create non-root user for security; grant ownership of app dir and Go paths -RUN addgroup -S devuser && adduser -S devuser -G devuser && \ +# Create non-root user for security; grant ownership of app dir and Go paths. +# uid/gid are pinned explicitly rather than left to `adduser -S`, which picks +# the first free system id and so varies with the base image's existing users. +# A numeric USER below needs a value that is known here, not discovered later. +RUN addgroup -g 1001 devuser && \ + adduser -D -u 1001 -G devuser devuser && \ chown -R devuser:devuser /app /root/go /root/.cache 2>/dev/null || true -USER devuser +# Numeric form so the identity is resolvable without the image's /etc/passwd. +USER 1001:1001 # Start with Air for hot reload CMD ["air", "-c", ".air.toml"] diff --git a/Dockerfile.test b/Dockerfile.test index 09af056b8..a31d233d8 100644 --- a/Dockerfile.test +++ b/Dockerfile.test @@ -18,7 +18,9 @@ WORKDIR /e2e COPY --chown=e2e:e2e tests/e2e/ ./ -USER e2e +# Numeric form so the identity is resolvable without the image's /etc/passwd. +# 10001 is exactly the uid created above, so this is a rename only. +USER 10001 # Pre-compile the test binary so `docker compose up` failures are runtime # failures, not compile errors discovered after the stack is already up.