fix(container-logs): let the grant serve a real Docker client - #468
Merged
Merged
Conversation
The container-logs@1 proxy has shipped since 0.11.0-rc.1 with no provider to exercise it. Packaging one (Dozzle, try-hola/apps#160) and installing it on a VM found three ways the envelope refused the clients it exists for. All three were shape, not policy — nothing here widens what the grant discloses. 1. HEAD was refused. Docker's own client pings with `HEAD /_ping` before anything else, so a GET-only allowlist rejected every standard client on its first call, and the client reported it as "Could not connect to any Docker Engine" — not as a refusal. HEAD is now allowed wherever GET is: it returns headers and no body, so it reveals strictly less than the GET it mirrors. 2. GET /info was refused outright. A client on Docker's SDK calls it to decide an engine is really there and exits without it. It is now rebuilt from an allowlist, the same way inspect is: what identifies and sizes the engine survives (name, version, OS, arch, cpu/memory, container counts) and everything describing how the host is configured does not — RegistryConfig, Labels, Plugins, DockerRootDir, SecurityOptions, Swarm, and above all HttpProxy/HttpsProxy, which routinely embed credentials. 3. The redacted inspect omitted HostConfig, Mounts and NetworkSettings entirely. A real daemon always returns them, so clients walk them without checking — Dozzle segfaults on HostConfig.PortBindings. Dropping the field denied the client, not the data. They are now present but empty: no host port map, no bind sources, no network topology, and the same nothing on the wire, in a shape an SDK client can parse. Verified on a disposable VM against the real app: before, dozzle crash-looped on "Could not connect"; after, it reports "Connected to Docker", serves, and reads a neighbouring app's logs with sh.hola.app labels intact. The envelope still holds — stats, archive, images, secrets, volumes, POST restart, POST exec and DELETE all answer 403, and the neighbour's inspect carries no Env. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vck5KSX2CLxhohx14nb5Sh
pofallon
added a commit
that referenced
this pull request
Sep 20, 2026
Carries #468: the container-logs@1 grant can now serve a real Docker client. The contract has shipped since 0.11.0-rc.1 with no provider to exercise it. Packaging one (Dozzle, try-hola/apps#160) found three ways the proxy refused the clients it exists for — HEAD was rejected outright, so every Docker client failed on its first call; `/info` was refused, which an SDK client treats as "no engine"; and the redacted inspect omitted structural fields clients walk without nil-checking, segfaulting them. All three were shape, not policy: the grant discloses exactly what it did before, and stats, archive, images, secrets, volumes, restart, exec and delete were all re-verified at 403 on a VM after the fix. This is the release a container-logs provider needs. On rc.6 or earlier such an app installs and its proxy sidecar appears, but the app itself crash-loops. Claude-Session: https://claude.ai/code/session_01Vck5KSX2CLxhohx14nb5Sh Co-authored-by: Paul O'Fallon <paulofallon@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.
Why now
container-logs@1has shipped since 0.11.0-rc.1 — proxy, grant, consent, labels, tests — with no provider to exercise it. I packaged one (Dozzle, try-hola/apps#160) and installed it on a VM. It crash-looped:Three defects, all shape rather than policy — nothing here widens what the grant discloses.
1. HEAD was refused
The allowlist was GET-only. Docker's own client pings with
HEAD /_pingbefore anything else, so every standard client was rejected on its first call — and reported it as "no engine", not as a refusal.HEAD is now allowed wherever GET is. It returns headers and no body, so it reveals strictly less than the GET it mirrors, and it opens nothing new:
HEAD /containers/{id}/archiveis still 403.2.
GET /infowas refused outrightA client on Docker's SDK calls it to decide an engine is really there, and exits without it. Refusing it meant the grant could not serve the clients it exists for.
It is now rebuilt from an allowlist, exactly as inspect is. Kept:
Name,ServerVersion,OSType,OperatingSystem,Architecture,NCPU,MemTotal, container counts — enough to identify and size the engine. Dropped:RegistryConfig,Labels,Plugins,DockerRootDir,SecurityOptions,Swarm,KernelVersion, and above allHttpProxy/HttpsProxy, which routinely embed credentials.3. The redacted inspect was shape-incompatible
It omitted
HostConfig,MountsandNetworkSettingsentirely. A real daemon always returns them, so clients walk them unchecked — Dozzle segfaults onHostConfig.PortBindings:Dropping the field denied the client, not the data. They are now present but empty —
{ PortBindings: {} },[],{ Networks: {} }— which puts the same nothing on the wire in a shape an SDK client can parse. No host port map, no bind sources, no network topology.Verified on a VM, against the real app
Connected to Docker,Accepting connections on :8080, healthysh.hola.app/sh.hola.deploymentlabels intactEnvAnd the envelope still holds — probed from inside the deployment:
Left deliberately closed
/containers/{id}/statsstays 403, so Dozzle shows logs without resource graphs. It is benign data (counters for containers the caller can already list) and would be easy to add, but nothing needs it to function and I would rather widen the envelope on a real request than on a nice-to-have. Worth a decision alongside #438.Tests
docker-proxy.test.ts26 pass: HEAD allowed on each allowed path and on none of the refused ones; every mutating verb still refused;/infoallowed versioned and unversioned;redactInfokeeps the identifying fields, drops the proxy URLs (asserted by searching the serialized body for the credential), drops host configuration; inspect keeps the structural fields present-but-empty while leaking no port map, bind source or IP. Three existing assertions were updated — they pinned the absent-field shape this fixes.Full gate green: typecheck, lint, typecheck, test (983 server + 360 web), build.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Vck5KSX2CLxhohx14nb5Sh