Repository navigation
fix(router): authenticate worker-image pulls against private registries - #1539
Merged
Merged
Conversation
All router-side registry pulls (spawn self-heal, worker-image validation, Dockerfile-build base refresh) funnel through pullImageOnce, whose dockerode pull sends no credentials — the daemon has no ambient login and never reads ~/.docker/config.json. Anonymous pulls 401 on private registries, so a host image prune that removed the worker image turned every spawn into `Head .../manifests/latest: unauthorized` (verified live 2026-08-20). New optional env: WORKER_IMAGE_REGISTRY_USERNAME / _PASSWORD (+ _SERVER override; host otherwise derived from the image ref, Docker Hub as the no-host fallback). Unset preserves anonymous pulls; partial credentials warn and stay anonymous. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
CI's `npm audit --omit=dev --audit-level=high` step began failing on the newly disclosed CVE-2026-59870 (GHSA-5p4m-2wfm-xmqj): quadratic CPU consumption in js-yaml's !!omap resolution, affecting >=4.0.0 <4.3.1. js-yaml is a direct production dependency; the 4.3.1 patch release fixes it and all transitive consumers (llmist, @llmist/cli) dedupe to it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nhopeatall
approved these changes
Aug 20, 2026
nhopeatall
left a comment
Collaborator
There was a problem hiding this comment.
Summary
APPROVE — Correct, well-scoped fix for anonymous private-registry 401s, with a single funnel point (pullImageOnce), deterministic auth resolution, fail-safe partial-credential handling, and thorough tests. Verified locally: 121/121 affected router tests pass, typecheck clean, CI 7/7 green.
What I verified
- Single funnel holds. All three router-side pulls route through
pullImageOnce: spawn self-heal (container-manager.ts:224), worker-image validation (worker-image-validation.ts:155), and the Dockerfile-build base refresh (worker-image-build.ts:279viaresolveBaseImageRef). No otherdocker.pull/createImagecall sites exist, so the auth applies everywhere it needs to. registryHostFromImageRefmatches Docker's reference grammar. Host-detection (dot / port-colon /localhost, gated on presence of/) correctly distinguishesghcr.io/...,host:port/...,localhost:5000/...from Docker Hub refs (library/redis:7,redis:7-alpine). Tag-colon-vs-port ambiguity is handled.- Gating is fail-safe and non-leaky. none → anonymous, partial → warn + anonymous (public pulls keep working, private 401 stays attributable), full → authconfig. The warning logs only
imageRef; credentials are never logged. - Backward compatible.
docker.pull(image, {})is behavior-identical to the priordocker.pull(image); the updated pins inworker-snapshots/container-manager/snapshot-integrationtests confirm the anonymous default. js-yamlbump is a deliberate security fix (separate commit clearing a high-severity audit failure), not unrelated churn.- Documentation is updated in the canonical place (CLAUDE.md);
.env.exampleis intentionally non-exhaustive (omitsPM_COALESCE_WINDOW_MS,SLOT_WAIT_TIMEOUT_MS, etc.), so leaving the new vars out of it is consistent with convention.
Non-blocking note
- Credentials are attached to every worker-image pull, with
serveraddressderived per-ref. In today's architecture every router pull targets the single configured registry, and worker-image refs are superadmin-only (projects.ts:142), so this stays within a trusted boundary. Worth keeping in mind as per-project reference images (specs 022/023) proliferate: if a project's image ever lives on a different public registry than the configured credentials, the pull would send mismatched creds and 401 rather than falling back to anonymous. A futureserver/host match-gate could scope the auth to the intended registry. Not required for this PR.
🕵️ claude-code · claude-opus-4-8 · run details
…registry-auth # Conflicts: # CLAUDE.md
Collaborator
|
Error: Claude Code returned an error result: You've hit your weekly limit · resets 11pm (UTC) Manual intervention may be required. |
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.
Problem
Every router-side registry pull — the spawn self-heal in
container-manager.ts, worker-image validation, and the Dockerfile-build base refresh — funnels throughpullImageOnce, whosedocker.pull()carries no credentials: dockerode sends nothing unless given an explicitauthconfig, the Docker daemon has no ambient login, and~/.docker/config.jsonis a CLI-side concept the Engine API never reads.Against a private registry every such pull 401s. Verified live 2026-08-20: a weekly host
docker image prune --allremoved the worker image (nothing references it between ephemeral worker runs), and every subsequent spawn failed instantly with:The self-heal path could never have worked for private images — it only appeared to work while deploy-time pulls kept the image present on the host.
Fix
New optional router env (documented in CLAUDE.md,
src/router/registry-auth.ts):WORKER_IMAGE_REGISTRY_USERNAME/WORKER_IMAGE_REGISTRY_PASSWORD— passed as dockerodeauthconfigon everypullImageOncepull (for ghcr.io a PAT withread:packagesis the password).WORKER_IMAGE_REGISTRY_SERVER— optional override for theserveraddress; otherwise derived from the image ref per Docker's reference grammar (first segment with a dot/port/localhost; Docker Hubhttps://index.docker.io/v1/for bare refs).Behavior is unchanged when unset (anonymous pulls). Partial credentials (only one of the pair) log a warning and stay anonymous, so public-image pulls keep working while the private-image 401 stays attributable.
This matters more as per-project worker images (specs 022/023) put more registry pulls on the router path.
Tests
tests/unit/router/registry-auth.test.ts— host derivation (ghcr / port / localhost / hub namespaced / bare ref), credential gating (none → anonymous, partial → warn + anonymous, full → authconfig), server override, routerConfig defaulting.pullImageOncepins: authconfig pass-through for private registries, anonymous{}default.🤖 Generated with Claude Code