Skip to content

Stop shipping build-time caches and host identity in published images - #14

Merged
16francej merged 1 commit into
mainfrom
harden-image-builds
Jul 29, 2026
Merged

Stop shipping build-time caches and host identity in published images#14
16francej merged 1 commit into
mainfrom
harden-image-builds

Conversation

@16francej

@16francej 16francej commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

All six release images were built and scanned as filesystems — docker export, full-tree scan, per-layer whiteout analysis, metadata inspection — rather than audited by reading Dockerfiles.

No organization data, credentials, or repository content leaks in any of the six. Zero hits for ycombinator, quartermaster, work-claw, or the old AWS account, on any image. No .git, .env, SSH keys, or cloud configs. The web-ui client bundle — the only artifact that reaches browsers — has no source maps, no build-machine paths, and no inlined build env.

What the audit did find is build-environment residue that becomes public the moment these are pushed to a public registry.

/root/.npm ships in core, portal, auth, web-ui

npm's cacache stores every fetch URL verbatim as the cache key, so npm's own log redaction never applies. The core image carried a live GitHub release-asset JWT and Azure SAS signature, from resolving @earendil-works/pi-coding-agent over a signed release URL:

"key":"make-fetch-happen:request-cache:https://release-assets.githubusercontent.com/...&sig=L8WnDk34...%3D&jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...

Those specific tokens are worthless — 300-second JWT TTL, SAS expires an hour after build, and both grant read on an asset that is already public. The mechanism is the problem: any dependency resolved through an authenticated or signed URL gets published verbatim. Cached reqHeaders are empty, so header-based auth is unaffected; only URL-embedded secrets are exposed.

Removing the cache also takes core from 4.44 GB to 3.12 GB.

sandbox-base baked a fixed host identity

/etc/machine-id and /var/lib/dbus/machine-id both held b2dc9eb4b2c34319bb4861e8c984bab3 — identical across separate docker run invocations. Generated by systemd's postinst, pulled in as a chromium dependency. Every container anyone runs from the published image would present the same 128-bit identity to systemd, D-Bus, and anything keyed on it. Debian's own cloud images ship these empty for exactly this reason. The gh device-id, a side effect of the version smoke test, had the same problem.

browser-use telemetry was on by default

An agent sandbox that browses on users' behalf shipped with an unconfigured outbound data path to eu.i.posthog.com and api.browser-use.com, with exception autocapture enabled. Now off by default; operators can re-enable via env.

DEBIAN_FRONTEND was an ENV

So it persisted into the runtime and silently suppressed prompts for anything the agent ran. It is a build-time concern — demoted to ARG.

Verification

Rebuilt all six and re-inspected:

check result
/root/.npm in core/portal/auth/web-ui gone
/tmp/node-compile-cache gone
/etc/machine-id, /var/lib/dbus/machine-id 0 bytes
gh device-id gone
ANONYMIZED_TELEMETRY / BROWSER_USE_CLOUD_SYNC false
DEBIAN_FRONTEND in runtime env absent
core image size 4.44 GB → 3.12 GB

Deliberately not in this PR

  • web-ui ships 307 MB of build-only dependencies into its final stage — 112 top-level packages for a 6.3 MB bundle, none imported by the server, which uses only node builtins, lru-cache, and chassis. Bloat and vulnerability surface, not exposure: static serving is confined to dist-web behind a traversal guard. Fixing it means restructuring the multi-stage build, which deserves its own change.
  • rm package-lock.json in the web-ui Dockerfile does nothing — the file is recoverable byte-identical from the underlying COPY layer. Left alone rather than change what ships inside a hardening PR.
  • SLSA provenance embeds the git remote and SHA. Harmless here, and release-package.yml sets provenance: false so it never reaches GHCR — but that is one deleted line away from a private fork publishing its repo URL and commit SHA. Worth making durable separately.

Caveats

Images were built on arm64 under emulation; CI builds native amd64 with GHA layer cache. Detection was literal-text regex and strings, not entropy analysis, so encoded or compressed secrets would not surface. Worth adding gitleaks or trufflehog to CI for standing coverage rather than point-in-time passes.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

An audit of all six release images, built and scanned as filesystems rather
than read as Dockerfiles, found no organization data, credentials, or
repository content. It did find build-environment residue that becomes public
the moment these images are pushed to a public registry.

/root/.npm ships in core, portal, auth and web-ui. npm's cacache stores every
fetch URL verbatim as the cache key, so log redaction never applies: the core
image carried a live GitHub release-asset JWT and Azure SAS signature from
resolving @earendil-works/pi-coding-agent over a signed URL. Those particular
tokens expire minutes after the build and grant read on an already-public
asset, so the exposure is nil, but the mechanism is not: any dependency
resolved through an authenticated or signed URL is published verbatim. Cached
reqHeaders are empty, so only URL-embedded secrets are affected. Removing the
cache also drops core from 4.44 GB to 3.12 GB.

sandbox-base baked a fixed /etc/machine-id, generated by systemd's postinst as
a chromium dependency, so every container from the published image presented
the same 128-bit host identity. Debian ships this empty for the same reason.
The gh device-id, generated by the version smoke test, had the same problem.

browser-use defaults telemetry and cloud sync on, giving an agent sandbox that
browses for users an unconfigured outbound path to eu.i.posthog.com and
api.browser-use.com. Disabled by default; operators can re-enable.

DEBIAN_FRONTEND was an ENV, so it persisted into the runtime and silently
suppressed prompts for anything the agent ran. It is a build concern.

Verified by rebuilding all six and re-inspecting: caches absent from all four
node images, both machine-id files zero bytes, gh device-id gone, telemetry
off, DEBIAN_FRONTEND absent from the runtime environment.

Co-Authored-By: Claude Opus 5 <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.

1 participant