harden(docker): pin the image by hash — base digest + npm ci from a lockfile#91
Merged
Conversation
…ockfile Closes the three agent-actionable code-scanning alerts. Scorecard #25 (Dockerfile:11) — `FROM node:22-slim` is a mutable tag; pinned to the digest it currently resolves to. Scorecard #26 (Dockerfile:23) — `npm i -g <pkg>@<version>` pins a label, not bytes: the version tag can be republished and nothing in the transitive tree was pinned at all. The image now installs from docker/package-lock.json with `npm ci`, so all 108 packages carry an integrity hash and redstamp is pinned to an exact commit. A tool whose entire purpose is making agents pin their supply chain should not install its own unpinned. Two consequences worth naming: - The CLIs move from a global install to /app/node_modules/.bin, so that goes on PATH for the build steps, the ENTRYPOINT, and the downstream server the gate spawns. - npm writes hosted-git `resolved` URLs as git+ssh:// whenever the lock is generated on a machine with a GitHub SSH remote, and there is no key in the build. `git config url.https://github.com/.insteadOf` makes the transport irrelevant — the commit SHA is what pins the dependency — so regenerating the lock elsewhere can't break the build later. Pinning by hash means nothing moves the pins on its own, so Dependabot now watches both the base image (`docker`) and the image's dependency set (`/docker` npm). Verified `npm ci` resolves clean-cache with SSH disabled. CodeQL #27 (support/evidence.test.mjs:39) — `mismatches` was destructured and never used. Asserted instead of deleted: zero unverifiable hits on a real scan is the guarantee evidenceOf exists to make, and the integration test was the one place not checking it. Alert #21 (CII-Best-Practices) is unaffected — it needs the bestpractices.dev registration, not a code change.
sprayberry-reviewer
approved these changes
Jul 22, 2026
sprayberry-reviewer
left a comment
Collaborator
There was a problem hiding this comment.
Automated review from the Sprayberry Labs fleet code reviewer.
Verdict: No blocking issues found — clean, well-documented supply-chain hardening change. Approving.
What I checked
gh pr diff 91(1475 lines) — Dockerfile, Dockerfile.dockerignore, dependabot.yml, newdocker/package.json+docker/package-lock.json, and thesupport/evidence.test.mjsassertion addition.- CI check-rollup: all 10 checks green, including
build-and-smoke(the docker workflow that actually builds the image, asserts non-root uid, and does a live stdio MCP handshake to confirmtools/listisn't empty — exactly the failure mode a lockfile/PATH change could cause). - Cross-referenced
docker/pin-everything.mjsand.github/workflows/docker.ymlagainst the Dockerfile's newENV PATH=/app/node_modules/.bin:$PATH(Dockerfile:38in the new file) to confirm the local-bin CLIs (truecopy-mcp,mcp-server-everything) are actually resolvable at the point they're invoked (pin-everything.mjs,ENTRYPOINT) — they are, since the PATH change lands before both. - Confirmed
docker/package-lock.jsonrecords an integrity hash for every registry-resolved package and a pinned commit SHA (resolved: git+https://github.com/askalf/redstamp.git#3da60b9d...) for the git-sourced dependency, matching the PR's stated intent. .github/dependabot.ymldiff adds ecosystems for both the new/dockernpm manifest and thedockerbase image, consistent with the new pin points introduced.
What's good
- The base-image digest and npm lockfile close the two Scorecard hash-pinning alerts described in the PR body, and the CodeQL unused-variable fix is paired with an actual assertion (
mismatches === 0) rather than just silencing the lint. - The
git config --global url."https://github.com/".insteadOf ssh://git@github.com/line is a good catch — prevents a future lockfile regen on an SSH-configured machine from silently breaking the build. build-and-smokeCI directly exercises the two riskiest consequences of this change (root-owned-but-readablenode_modules, and PATH resolution for the relocated CLIs) rather than just asserting the image builds.
No correctness, security, or test-coverage concerns in the diff. I did not find anything to request changes on.
askalf
added a commit
that referenced
this pull request
Jul 22, 2026
…frvp-7c67-39w9) Scorecard opened a high-severity Vulnerabilities alert (#28) minutes after #91 landed. The dependency is not new — `npm i -g` was installing the same tree before — but nothing in the repo described it, so nothing could see it. The lockfile that #91 added is what made it visible, which is the point of having one. The advisory (published 2026-07-21, hours before that merge) is a path traversal in @hono/node-server's serve-static, patched in 2.0.5. It reaches this image transitively: server-everything -> @modelcontextprotocol/sdk -> @hono/node-server "^1.19.9". That caret range can never resolve to 2.0.5, the SDK is already at its latest (1.29.0), and server-everything is already at its latest (2026.7.4) — so there is no upstream bump to wait for. An npm override is the only way off it today. Overriding a transitive major the SDK was not written against is the real risk here, so it is tested rather than assumed: with the override in place, `npm ci` resolves @hono/node-server 2.0.11 and the reference server still completes a full MCP handshake over stdio, returning all 13 tools. The image only ever runs the downstream over stdio, so it never constructs a Hono HTTP server at all — the vulnerable serve-static path was unreachable here either way. This is about not shipping a flagged dependency in a supply-chain tool's own image, not about a live exploit. Drop the override once the SDK widens its range; Dependabot watches /docker (added in #91) and will offer that bump. Verified: clean-cache `npm ci --omit=dev` with SSH disabled resolves 108 packages and pins @hono/node-server to the patched 2.0.11; live stdio handshake against that exact tree returns 13 tools. The full image build + non-root check + live handshake is what docker.yml asserts on this PR.
askalf
added a commit
that referenced
this pull request
Jul 22, 2026
…frvp-7c67-39w9) (#96) Scorecard opened a high-severity Vulnerabilities alert (#28) minutes after #91 landed. The dependency is not new — `npm i -g` was installing the same tree before — but nothing in the repo described it, so nothing could see it. The lockfile that #91 added is what made it visible, which is the point of having one. The advisory (published 2026-07-21, hours before that merge) is a path traversal in @hono/node-server's serve-static, patched in 2.0.5. It reaches this image transitively: server-everything -> @modelcontextprotocol/sdk -> @hono/node-server "^1.19.9". That caret range can never resolve to 2.0.5, the SDK is already at its latest (1.29.0), and server-everything is already at its latest (2026.7.4) — so there is no upstream bump to wait for. An npm override is the only way off it today. Overriding a transitive major the SDK was not written against is the real risk here, so it is tested rather than assumed: with the override in place, `npm ci` resolves @hono/node-server 2.0.11 and the reference server still completes a full MCP handshake over stdio, returning all 13 tools. The image only ever runs the downstream over stdio, so it never constructs a Hono HTTP server at all — the vulnerable serve-static path was unreachable here either way. This is about not shipping a flagged dependency in a supply-chain tool's own image, not about a live exploit. Drop the override once the SDK widens its range; Dependabot watches /docker (added in #91) and will offer that bump. Verified: clean-cache `npm ci --omit=dev` with SSH disabled resolves 108 packages and pins @hono/node-server to the patched 2.0.11; live stdio handshake against that exact tree returns 13 tools. The full image build + non-root check + live handshake is what docker.yml asserts on this PR.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the three agent-actionable open code-scanning alerts. The fourth (#21, CII-Best-Practices) needs a bestpractices.dev registration, not a code change — left open.
Scorecard #25 —
Dockerfile:11, containerImage not pinned by hashFROM node:22-slimis a mutable tag. Pinned to the digest it resolves to today (sha256:6c74791e…, verified against the registry, matches the remediation tip).Scorecard #26 —
Dockerfile:23, npmCommand not pinned by hashnpm i -g @askalf/truecopy@0.9.0 @modelcontextprotocol/server-everything@2026.7.4pins a label, not bytes — a version tag can be republished, and nothing in the transitive tree was pinned at all. The image now installs from a lockfile:docker/package.json+docker/package-lock.json— the image's own dependency set, separate from truecopy'sRUN npm ci --omit=dev— all 108 packages carry an integrity hash; the git-sourced redstamp dependency is pinned to an exact commitA tool whose entire purpose is making agents pin their supply chain should not install its own unpinned.
Two consequences worth review attention:
/app/node_modules/.bin.ENV PATHputs that first, which covers the build steps, theENTRYPOINT, and the downstream serverpin-everything.mjsspawns.resolvedURLs asgit+ssh://whenever the lock is generated on a machine with a GitHub SSH remote, and there is no key in the build.git config url."https://github.com/".insteadOf ssh://git@github.com/makes the transport irrelevant — the commit SHA is what pins the dependency — so regenerating the lock on another machine can't break the build later. (Same trap as ci(fuzz): root .dockerignore from #72 blanks ClusterFuzzLite's build context — scope it to the MCP image #76's ClusterFuzzLite build.)Pinning by hash means nothing moves the pins on its own, so Dependabot now watches the base image (
docker) and the image dependency set (/dockernpm) alongside the existing entries.Verification:
npm ci --omit=devre-run in a clean directory with an empty npm cache andGIT_SSH_COMMAND=false— resolves and installs 108 packages over HTTPS,truecopy/truecopy-mcp/mcp-server-everythingall present innode_modules/.bin. The full image build + live MCP handshake is whatdocker.ymlasserts on this PR (it triggers onDockerfile,Dockerfile.dockerignore,docker/**), including the empty-tools/listregression and the non-root check.CodeQL #27 —
support/evidence.test.mjs:39, unused variablemismatcheswas destructured and never used. Asserted rather than deleted: zero unverifiable hits on a real scan is the guaranteeevidenceOfexists to make, and the integration test was the one place not checking it. 166 tests pass.