feat: a bind address you can set, because a container cannot use loopback - #12
Merged
Conversation
…back `docker-compose.yml` shipped a container nothing could reach. It published `127.0.0.1:8787:8787`, but paddock bound `127.0.0.1` INSIDE the container, and a published port is delivered to the container's own interface — so docker-proxy dialled an address the listener refused. Measured both ways on a spare port: loopback bind answers HTTP 000 through the publish, `0.0.0.0` answers 200 with herdr connected. So `PADDOCK_HOST`, defaulting to `127.0.0.1`. Empty or whitespace resolves to loopback rather than a wildcard: `PADDOCK_HOST:` left half-written in a compose file must not silently publish an unauthenticated dashboard. A non-loopback bind warns and proceeds. It cannot be an error — a container requires one — and it must not be silent, because decision 3 gives this listener no authentication at all, so reachability IS authority: anything that can open the port can read every agent's screen and type into it. The warning names the address and says that plainly. `isLoopbackBind` is deliberately NOT `origin.ts`'s `isLoopbackHost`. That one classifies `Host` headers, which name one machine and are never wildcards; `0.0.0.0` and `::` exist only as bind addresses. Sharing the predicate would put a seam in the thing that file exists to keep seamless. The banner no longer prints `http://0.0.0.0:8787`. It reads as a link and opens nowhere; a wildcard bind gets a loopback URL plus the fact of the wildcard. Decision 3 asserted paddock "binds loopback only", which is now false, so it gets a scope note rather than a silent contradiction — including that this adds no authentication and removes none, and that what protects a non-loopback bind is whatever sits in front of the port. Test data uses RFC 5737 documentation addresses: `check-clean` refuses private address literals in a public repo, and the predicate cannot tell them apart. 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.
The bug this started from
docker-compose.ymlshipped a container nothing could reach.It published
127.0.0.1:8787:8787— correctly, that prefix is what keeps thedashboard off the LAN — but paddock bound
127.0.0.1inside the container.A published port is delivered to the container's own interface, so
docker-proxydialled an address the listener refused.Measured both ways on a spare port, so a live instance was never touched:
127.0.0.1(before)0.0.0.0(after)herdrConnected: true, 5 agentsBoth refused connections from the host's LAN address. The publish prefix is
what actually protects the port, and the compose comment now says so — it
previously explained only half of the mechanism.
The change
PADDOCK_HOST, defaulting to127.0.0.1. Empty or whitespace resolves toloopback rather than a wildcard:
PADDOCK_HOST:left half-written in a composefile must not silently publish an unauthenticated dashboard.
A non-loopback bind warns and proceeds. It cannot be an error, because a
container requires one; it must not be silent, because decision 3 gives this
listener no authentication at all. Reachability is authority here — anything
that can open the port can read every agent's screen and type into it.
The banner no longer prints
http://0.0.0.0:8787. It reads as a link and opensnowhere.
Two deliberate choices
isLoopbackBindis notorigin.ts'sisLoopbackHost. That one classifiesHostheaders, which name one machine and are never wildcards.0.0.0.0and::exist only as bind addresses. Sharing the predicate would put a seam in thething that file exists to keep seamless.
Decision 3 gets a scope note, not a silent contradiction. It asserted
paddock "binds loopback only", which is now false. The note records that this
adds no authentication and removes none, that what protects a non-loopback bind
is whatever sits in front of the port, and that it is not permission to bind
0.0.0.0on a workstation.tunnel/run.tskeeps its own hardcoded loopback bind — out of scope.Verification
make checkclean,make check-cleanclean, 1033 pass / 0 failLISTEN 127.0.0.1:8798, refused from the LAN, no warningLISTEN 0.0.0.0:8799, HTTP 200 from the LAN address, warning showndocker compose up --build, then torn down and the image removedcheck-cleantwice caught RFC1918 literals in the new test data and in acomment; both were replaced with RFC 5737 documentation addresses rather than
touching the denylist.
🤖 Generated with Claude Code