Skip to content

build: the image is built and scanned where the code is (ADR 0085) - #127

Merged
kfeofantov merged 1 commit into
mainfrom
build/image-scan-gate
Sep 17, 2026
Merged

kfeofantov merged 1 commit into
mainfrom
build/image-scan-gate

Conversation

@kfeofantov

Copy link
Copy Markdown
Contributor

Slice 1 of the image-security work. Nothing is published — CI builds the image and pushes nothing; signing, provenance and multi-arch are the release questions and are not answered here.

Why now

security.md §7.1 says plainly that installing the DaemonSet "is a decision to trust this agent's image with the node" — it runs as root, with hostPID and CAP_SYS_PTRACE, plus CAP_BPF/CAP_PERFMON in one profile. Everything in this repository bounds what the agent's code does; none of it bounds what other code in that container could do. The image had none of the discipline the code has:

  • nothing built it but a person, before an e2e run;
  • nothing looked inside it — make vulncheck answers about Go and cannot see the base layer at all;
  • both FROM lines were movable tags;
  • with no .dockerignore, COPY . . took CONTEXT.md into the builder stage.

What is in the image, measured

1403 entries, ~1200 of them zoneinfo and licence texts
1 executable: /usr/local/bin/agent (90 MB), no shell, no .so, usr/bin empty
6 Debian packages, versions readable from /var/lib/dpkg/status.d
83 Go modules in the binary's build info, plus the stdlib
Package Version
base-files 13.8+deb13u7
ca-certificates 20250419
tzdata / tzdata-legacy 2026c-0+deb13u1
media-types 13.0.0
netbase 6.5

The division of labour, and why it is not duplication

syft catalogues, grype scans the catalogue and fails on a fixable high-severity finding in the base layer. Go findings are printed (--show-suppressed) and do not gate.

That is not leniency — it is ADR 0038's line. Matching the binary's embedded module list against advisories fails on modules nothing calls, which §2 of that ADR decided must not fail a build. grype itself prints the reason on our image: "go vulnerability matching falls back to module granularity and may report false positives". Reachability stays make vulncheck's job; the base layer, invisible to it, is what this gates.

The job asserts the catalogue is non-empty before trusting it — a cataloguer that finds nothing reports nothing, which reads exactly like a clean image.

Pins, and the drift they invite

Both bases pinned by digest. Dependabot gains a docker ecosystem beside the gomod and github-actions entries it already had, so a moved tag arrives as a PR this job builds and scans.

That creates one new way to break ADR 0038 §3 — the Go floor in go.mod and in the build image are one fact, and Dependabot can now move one of them alone. test/image fails when they disagree, and it is proved to fail: bumping the builder to golang:1.26.8 turns both guardrails red.

.git stays in the context, deliberately

Excluding it would be the obvious hygiene move and it is wrong here: .git is what stamps vcs.revision, vcs.time and vcs.modified into the binary — the provenance this agent reads off customer workloads. An image of ours that could not answer "which commit is running" would fail the check we sell. The test asserts it is not excluded.

Evidence

make image-scan    # 6 deb + 83 go-module catalogued, 0 findings
go test -race ./... && golangci-lint run && go vet -tags e2e ./...   # 0 issues

Built and tested from a clean git archive HEAD extraction.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KeZECJXTDHL6Nts95eKMJA

The DaemonSet runs as root with hostPID and CAP_SYS_PTRACE, and security.md
says plainly that installing it is a decision to trust this image with the
node. Nothing built that image but a person, nothing looked inside it, both
bases were movable tags, and `COPY . .` took CONTEXT.md into the builder stage.

CI now builds it on every pull request and pushes nothing. syft catalogues the
image, grype scans the catalogue and fails on a fixable high-severity finding
in the base layer. Go findings are printed and do not gate: matching the
binary's embedded module list against advisories is the weaker question
ADR 0038 already answered better with reachability, and grype says so itself in
a warning about module granularity. The job first asserts the catalogue is not
empty, because a cataloguer that finds nothing reads exactly like a clean image.

Both FROM lines are pinned by digest and Dependabot gains a docker ecosystem
beside the two it already watched, so a moved tag arrives as a pull request this
job builds. A pin it can move is a new way to break the rule that the Go floor
in go.mod and in the build image are one fact, so test/image fails when they
disagree.

.git stays in the build context deliberately: it is what stamps vcs.revision
into the binary, which is the provenance this agent reads off customer
workloads.

  make image-scan   # 6 deb packages, 83 go modules catalogued, 0 findings
  go test ./test/image/ -run TestEveryBase   # fails on a tag-only FROM

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeZECJXTDHL6Nts95eKMJA
@kfeofantov
kfeofantov merged commit b495a1a into main Sep 17, 2026
5 checks passed
@kfeofantov
kfeofantov deleted the build/image-scan-gate branch September 17, 2026 20:04
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.

1 participant