#19: add Trivy scanning and multi-architecture GHCR publishing

CI gains a security job (Trivy fs: vuln, secret and misconfig) and an
image job that builds each of the four services and scans the result. The
secret scanner earns its place here: a DKIM key or Cloudflare token
committed by accident is a full compromise of every domain served.

release.yml publishes on a merge into main and on v* tags. Four services
on two architectures is eight build jobs pushing by digest, then four
publish jobs joining each pair into one manifest list, tagged latest, sha
and semver. github.repository is lowercased first because GHCR rejects an
uppercase path and this repository is lucasrguerra/LambdaMail.

Both scanners failed on first run, so this commit also fixes what they
found rather than shipping a workflow that is red on arrival:

  - All four images ran as root (DS-0002), which section 17 risk R5
    explicitly argues against. They now run unprivileged; protocols keeps
    its eight ports below 1024 through cap_net_bind_service on the binary
    rather than through being root.

  - A HIGH advisory in postcss, reached only through Next. "npm audit fix
    --force" would have resolved it by downgrading Next from 15 to 9, so
    it is pinned with an override instead.

  - The Node images copied the build stage's node_modules wholesale,
    shipping typescript, eslint and vitest into production, and the base
    image's own bundled npm accounted for the rest of the CVEs. Runtime
    dependencies now come from npm ci --omit=dev and npm is removed from
    the final layer.

  - compose-up could never have passed: the proxy network is external
    because Coolify owns it on the deploy host, and nothing creates it in
    CI, so compose refused to start the stack at all.

Verified locally: actionlint clean, all ten action refs confirmed to
exist, Trivy fs and all four image scans exit 0, the full stack reaches
Healthy on every container, and the containers run as uid 10001/1000 with
all eight privileged ports bound and SMTP answering on 25. The arm64
build itself is unverified - this machine has no buildx - and will first
be exercised by release.yml.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Lucas RayanandClaude Opus 5 committed 2026-08-02 07:58:02 -03:00
1 parent 3deb328bbf
commit 46d421c874
8 files changed
+350 -79

No files matched your search

+20 -1
View File
@@ -6,7 +6,26 @@ COPY . .
RUN CGO_ENABLED=0 go build -o /out/lambdamail-protocols ./cmd/lambdamail-protocols
FROM alpine:3.20
RUN apk add --no-cache ca-certificates
# libcap provides setcap and is dropped again once the capability is set.
RUN apk add --no-cache ca-certificates libcap
COPY --from=build /out/lambdamail-protocols /app/lambdamail-protocols
# This process terminates SMTP from the open internet, so it must not run as
# root (PLAN.md section 17, risk R5). Every mail port it binds is below 1024,
# which normally requires root - the file capability grants exactly that one
# privilege to this one binary instead.
RUN setcap 'cap_net_bind_service=+ep' /app/lambdamail-protocols \
&& apk del libcap
RUN addgroup -g 10001 -S lambdamail \
&& adduser -u 10001 -S -G lambdamail lambdamail \
# Created here so a fresh named volume mounted at this path inherits the
# ownership: Docker seeds an empty volume from the image directory.
&& mkdir -p /var/lambdamail/spool \
&& chown -R lambdamail:lambdamail /var/lambdamail
USER lambdamail:lambdamail
EXPOSE 8080
ENTRYPOINT ["/app/lambdamail-protocols"]