Skip to content

feat(events-processor): harden the production Dockerfile onto the Wolfi bases - #811

Merged
IxDay merged 1 commit into
mainfrom
feat/events-processor-wolfi-hardened
Oct 8, 2026
Merged

IxDay merged 1 commit into
mainfrom
feat/events-processor-wolfi-hardened

Conversation

@IxDay

@IxDay IxDay commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Draft. I opened this before noticing events-processor/Dockerfile.staging already exists on main. Retargeted as the production follow-up; it should land only after the staging bake (getlago/lago-deploy#3522) passes. See "Relationship to Dockerfile.staging" below.

What this is for

Dockerfile.staging hardened the staging build path and deliberately left ./Dockerfile alone. That means the production events-processor — what prod-us-1 and prod-eu-1 actually run — is still debian:13-slim running as root. This PR closes that.

stage before after
rust rust:1.85 ghcr.io/getlago/events-processor-build
go golang:1.25 ghcr.io/getlago/events-processor-build
runtime debian:13-slim + apt-get install ca-certificates ghcr.io/getlago/events-processor-base

Result, built from this branch (lago-events-processor:sha-2e8f228): 30.6MB vs 74.7MB, and the image config carries User: 65532 instead of root.

Relationship to Dockerfile.staging

The two would be near-duplicates, which is how they drift. Once this lands, Dockerfile.staging and lago-deploy's build-events-processor-staging-image.yml should collapse back onto the single production Dockerfile. I have deliberately not done that here — deleting the staging path before its bake completes would remove the safety net this whole sequence exists to provide. Happy to fold it into this PR or leave it as a follow-up, whichever you prefer.

Where it differs from Dockerfile.staging

Not just a copy — three fixes worth keeping whichever file survives:

  1. lago-expression pinned by commit, not tag. Dockerfile.staging does git clone --tags … + git checkout v0.2.0: a ref that can be repointed, and it drags every branch and tag down to do it. This fetches the single pinned commit a22ab02 — 62 objects.
  2. Base images pinned to a lago-packages commit sha, not :latest. A base bump becomes a reviewable diff instead of a silent change under a floating tag.
  3. go mod verify alongside go mod download.

Two things worth a reviewer's attention

CGO stays enabled — this cannot be a static build. expression-go binds to libexpression_go.so via #cgo LDFLAGS: -lexpression_go. The .so is staged onto the linker path in the go stage as well as the runtime stage, because -lexpression_go resolves at link time, not just at load time. That is why the runtime base carries glibc and libgcc.

Pre-existing version skew, preserved. go.mod requires expression-go v0.1.4 while the Dockerfile builds the cdylib from v0.2.0. That predates this PR and is left exactly as-is — flagging it because it is load-bearing and easy to "tidy" into a breakage.

Verification

  • CI branch build green: run 36736148427, multi-arch amd64 + arm64
  • image config pulled from ECR: User: 65532, Entrypoint: /app/event_processors, WorkingDir: /app
  • workspace builds --package expression-go clean on rust 1.98 — what Wolfi's rust provide resolves to, vs the 1.85 pinned before, which was the main toolchain risk
  • go mod download && go mod verify → all modules verified

Replaces the rust:1.85 -> golang:1.25 -> debian:13-slim chain with the
lago-packages bases, matching what lago-gotenberg, lago-license and
lago-oauth-proxy already run.

The debian:13-slim runtime shipped apt, a full shell userland and ran
as root. events-processor-base is glibc + libgcc +
ca-certificates-bundle + tzdata and nothing else, with run-as 65532
baked into the image config. Both bases already exist and are
published; no lago-packages change is needed.

Two things worth calling out beyond the base swap:

CGO stays on. expression-go binds to libexpression_go.so through
`#cgo LDFLAGS: -lexpression_go`, so this cannot become a static build.
The .so is staged on the linker path in the go stage (not just the
runtime stage) because -lexpression_go resolves at link time, and
events-processor-base carries glibc and libgcc for it at runtime.

lago-expression is pinned by commit, not by tag. The old
`git clone --tags` + `git checkout v0.2.0` trusted a ref that can be
repointed and dragged every branch and tag along to do it; this fetches
the single pinned commit (62 objects) and is what `v0.2.0` resolved to.

Verified: the pinned shallow fetch works against GitHub, the workspace
builds `--package expression-go` clean on rust 1.98 (what Wolfi's
`rust` provide currently resolves to, vs the 1.85 pinned before), and
events-processor's modules pass `go mod verify`.

Not verified locally: no container runtime on hand for a full image
build. The branch build in CI is the real check, and staging-eu-1 is
where it gets booted.
@IxDay
IxDay marked this pull request as draft September 30, 2026 15:30
@IxDay
IxDay force-pushed the feat/events-processor-wolfi-hardened branch from 2e8f228 to 69e6260 Compare September 30, 2026 15:30
@IxDay IxDay changed the title feat(events-processor): build on the Wolfi/apko hardened bases feat(events-processor): harden the production Dockerfile onto the Wolfi bases Sep 30, 2026
@IxDay
IxDay marked this pull request as ready for review October 8, 2026 08:07
@IxDay
IxDay merged commit e399abb into main Oct 8, 2026
1 check passed
@IxDay
IxDay deleted the feat/events-processor-wolfi-hardened branch October 8, 2026 08:13
IxDay added a commit that referenced this pull request Oct 8, 2026
#811 hardened the production events-processor/Dockerfile onto the
Wolfi/apko bases, which is the only reason Dockerfile.staging existed —
staging needed a Wolfi build while prod was still on Debian. With both
on Wolfi the two files were near-duplicates, and the production one is
strictly better: immutable base pins, a sha-pinned LAGO_EXPRESSION_REF
instead of a repointable tag, a single-commit fetch instead of
`git clone --tags`, go mod verify, explicit CGO_ENABLED=1, and OCI
labels.

The staging build keeps its distinct role via build-args in
lago-deploy#3611: the bases are overridden to `:latest` so staging still
rides the daily lago-packages CVE rebuild, while production stays
reproducible on a pinned base.

Requires lago-deploy#3611 to merge first — it repoints the staging
workflow at events-processor/Dockerfile.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
IxDay added a commit that referenced this pull request Oct 8, 2026
#811 hardened the production events-processor/Dockerfile onto the
Wolfi/apko bases, which is the only reason Dockerfile.staging existed —
staging needed a Wolfi build while prod was still on Debian. With both
on Wolfi the two files were near-duplicates, and the production one is
strictly better: immutable base pins, a sha-pinned LAGO_EXPRESSION_REF
instead of a repointable tag, a single-commit fetch instead of
`git clone --tags`, go mod verify, explicit CGO_ENABLED=1, and OCI
labels.

The staging build keeps its distinct role via build-args in
lago-deploy#3611: the bases are overridden to `:latest` so staging still
rides the daily lago-packages CVE rebuild, while production stays
reproducible on a pinned base.

Requires lago-deploy#3611 to merge first — it repoints the staging
workflow at events-processor/Dockerfile.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants