Arm the credentials guard on a non-loopback --public-url - #100
Merged
Merged
Conversation
The startup guard only ever looked at the bind address, so a hub on 127.0.0.1 exposed through a Cloudflare Tunnel, ngrok or a local reverse proxy started without a word: no agent key, so agent_ok() passed every caller, and no operator token, so every browser was graded as operator. /register, /peers and /watch-ticket/redeem were open to whoever reached the tunnel. _mount_mcp_http already read a declared public URL as "this hub is remote" when deciding what watch_command hands an agent. The credentials gate now reads it the same way: a --public-url naming a non-loopback host demands both --operator-token and --agent-key, with --allow-insecure-bind as the same escape hatch the bind guard offers. A loopback public URL is just a nicer address for this machine and arms nothing. _insecure_public_url_message carries the refusal; the doors inventory and the openssl snippet both messages share move into _doors_block so the two cannot drift apart on a flag rename.
check_bind returned early on any loopback host, so the installer happily wrote a unit for --host 127.0.0.1 --public-url https://hub.example.net with neither credential. The hub now refuses that configuration at startup, which would turn a clean install into a unit that loads and immediately exits. Same predicate as the hub's, same reasoning: a public URL naming a non-loopback host is the operator saying agents on other machines dial this hub, whatever the socket is bound to.
Both halves of the new gate: the hub CLI (refusal, which credential is named as missing, both credentials starting, --allow-insecure-bind, a loopback public URL arming nothing, the CAUCUS_PUBLIC_URL path, and a public URL with no parseable host still falling through to validate_public_url) and check_bind in the installer. test_an_invalid_public_url_refuses_at_startup now passes both credentials: its URL names a non-loopback host, so without them the new gate answers first and the test no longer exercises validate_public_url.
The TLS section told operators that binding to loopback behind a proxy "sidesteps its own non-loopback-bind refusal entirely" and that the two credentials were worth setting anyway. They are now required, for exactly the reason that paragraph described. The flags table row for --allow-insecure-bind names the public-url case too.
obeone
force-pushed
the
fix/public-url-credentials-guard
branch
from
September 18, 2026 02:21
70e73b3 to
26ae438
Compare
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.
A hub bound to
127.0.0.1and exposed through a Cloudflare Tunnel, ngrok or areverse proxy on the same machine starts with no credentials and says nothing
about it. The startup guard on this branch only looks at the bind address, so
caucus-hub --host 127.0.0.1 --public-url https://hub.example.netwalksstraight past it. With no
--agent-key,agent_ok()returnsTrueforeveryone, which leaves
/register,/peersand/watch-ticket/redeemopen towhoever reaches the tunnel. With no
--operator-token,role_forgrades everybrowser that loads
/uias operator.The gap predates this branch, but it is the same gate, and
docs/remote-hub.mdwas recommending the shape: its TLS section said a loopback bind "sidesteps its
own non-loopback-bind refusal entirely", then suggested setting both credentials
anyway.
What changed
_mount_mcp_httpalready read a declared public URL as remote when decidingwhat
watch_commandhands an agent(
remote=public_url is not None or not is_loopback_host(host)). The credentialsgate now agrees with it: a
--public-urlwhose host is not loopback demands both--operator-tokenand--agent-key, whatever the socket is bound to.The call worth arguing about is whether declaring a public URL should arm the
guard on its own. It only arms when the URL names a non-loopback host, so
--public-url http://localhost:8765stays a free cosmetic choice and anyoneusing it that way sees no change. A URL naming a routable host is the operator
saying agents on other machines dial this hub, which is exactly the exposure the
guard exists for.
--allow-insecure-bindcovers this case too, same flag, samemeaning.
check_bindinsetup_service.pygets the same predicate. Without it theinstaller would happily write a unit the hub then refuses to start.
The refusal, in the style of
_insecure_bind_message:The doors inventory and the
openssllines that both messages share moved into_doors_block, so a flag rename cannot fix one message and miss the other.How to test
11 new tests, 7 on the hub CLI in
tests/test_remote_hub.pyand 4 oncheck_bindintests/test_setup_service.py. They cover the refusal, whichcredential the message reports as missing, both credentials starting,
--allow-insecure-bind, a loopback public URL arming nothing, theCAUCUS_PUBLIC_URLpath, and a public URL with no parseable host still fallingthrough to
validate_public_url.pytest: 1079 passed, 0 failed.mypy src/: clean.ruff check src/: 9findings, byte-identical to the ones already on this branch (checked against a
pristine
git archiveoffeat/remote-hub), none of them in the changed lines.One existing test moved:
test_an_invalid_public_url_refuses_at_startupnowpasses both credentials, because its URL names a non-loopback host and the new
gate answers before
validate_public_urlgets to reject the path. That orderingis deliberate. Missing credentials on an advertised hub outrank the shape of the
address being advertised.
Risk
Breaking for one configuration: a hub started with a non-loopback
--public-urland fewer than both credentials now exits 2 instead of starting. That is the
point, but it will bite anyone running the reverse proxy example from
docs/remote-hub.mdas it was written, and a service installed bycaucus-setup-servicein that shape stops loading.--allow-insecure-bindisthe one flag back.