Harden cluster trust: ephemeral binding + HostInCluster specificity floor#1309
Conversation
A successful /.well-known discovery match used to persist
cluster_contexts[host] = <context> to contexts.json. That binding is
durable and silent: after a single drive-by clone of an attacker URL
(e.g. a malicious submodule whose /.well-known names the victim's real
core), every future op against that host skipped discovery and minted
identity-bearing JWTs against the bound context — a persistent
identity-leak channel with no UX to notice or revoke it.
Stop persisting the binding. ResolveContextForCluster now resolves the
match for the current invocation only and re-evaluates the live
/.well-known each time, so the trust decision stays fresh and a one-off
clone leaves no durable state. The sole caller (the non-interactive
git-remote-entire helper) can't prompt, so ephemeral resolution is the
right scoping; explicit bindings (from `entire-core context bind` or a
teammate's tooling) are still honored as before. Re-resolving also makes
`entire auth use` take effect immediately for unbound clusters.
Surface any bindings that do exist so they're auditable:
- `entire auth contexts` now lists cluster_contexts with an unbind hint;
- `entire auth unbind <host>` revokes a binding without touching the
underlying login context (ClusterBindings / UnbindCluster helpers).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 71cbb8f5add9
HostInCluster treated any `*.cluster` host as same-cluster via a bare suffix match. With a broad cluster host that makes an entire public suffix in-cluster: for cluster "io" every *.io host would inherit the Authorization-carry / replica-trust boundary, so a relinquished or misconfigured subdomain anywhere under that suffix becomes a credential receiver. Gate the subdomain (wildcard) match on the cluster host being strictly more specific than its own public suffix (via x/net/publicsuffix): a registrable domain or deeper. Exact host matches are unaffected, and IP literals stay allowed. So *.entire.io still matches entire.io, but *.io and *.co.uk no longer wildcard-match. This bounds the blast radius of the trust boundary independently of how the cluster host was obtained. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Entire-Checkpoint: e167309b8dc1
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit d8bc4b9. Configure here.
There was a problem hiding this comment.
Pull request overview
Hardens the entire:// clone trust model by (1) making cluster→context resolution via /.well-known/entire-cluster.json ephemeral (non-persistent) and (2) tightening same-cluster hostname matching to avoid overly-broad wildcard trust at public-suffix boundaries. It also adds UX to audit and revoke explicit cluster_contexts bindings via entire auth contexts and entire auth unbind.
Changes:
- Stop persisting implicit cluster auto-bindings from discovery; re-run discovery each invocation and prefer the active context among eligible matches.
- Add a public-suffix specificity floor to
HostInClusterwildcard matching (viax/net/publicsuffix). - Surface and revoke explicit
cluster_contextsbindings viaauth contextsoutput and a newauth unbindcommand (with tests).
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/entireclient/discovery/parse_replicas.go | Tightens HostInCluster wildcard semantics using a public-suffix specificity floor. |
| internal/entireclient/discovery/parse_replicas_test.go | Adds test cases for the new HostInCluster specificity floor behavior. |
| internal/entireclient/clusterdiscovery/resolve.go | Makes discovery-based context resolution ephemeral (no persisted cluster binding). |
| internal/entireclient/clusterdiscovery/resolve_test.go | Updates tests to assert discovery is re-run and no binding is persisted. |
| go.mod | Promotes golang.org/x/net to a direct dependency for publicsuffix. |
| cmd/entire/cli/auth/context_store.go | Adds helpers to list and remove cluster_contexts bindings. |
| cmd/entire/cli/auth.go | Registers the new entire auth unbind subcommand. |
| cmd/entire/cli/auth_context.go | Extends auth contexts to print bindings and implements auth unbind. |
| cmd/entire/cli/auth_context_test.go | Adds coverage for bindings surfacing and unbind idempotence. |
Two fixes from automated review on #1309: - clusterAllowsSubdomains returned true for IP literals, so HostInCluster would suffix-match a host like "evil.127.0.0.1" against cluster "127.0.0.1" and treat it as in-cluster. IPs have no subdomain semantics — reject them from the wildcard path so only the exact-host match applies. Adds a regression test. (Single-label hosts remain rejected: a bare label is its own public suffix.) - runAuthContexts returned early when there were no login contexts, so printClusterBindings never ran. A cluster binding can outlive every context (manual edit, deleted context); that orphan is exactly what the audit path must surface. Always fall through to the bindings section. Adds a test for the orphaned-binding case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 3083e0770e5e
|
Thanks bots — addressed in 61b0c10:
|

Follow-up security hardening to the remote-helper credential-scoping work (merged via #1306). Two independent fixes to the
entire://clone trust model.1. Stop silent persistent cluster auto-binding (
clusterdiscovery)A successful
/.well-known/entire-cluster.jsondiscovery match used to persistcluster_contexts[host] = <context>tocontexts.json. That binding is durable and silent: after a single drive-by clone of an attacker URL (e.g. a malicious submodule whose.well-knownnames the victim's real core), every future op against that host skipped discovery and minted identity-bearing JWTs against the bound context — a persistent identity-leak channel with no UX to notice or revoke it.ResolveContextForClusterno longer persists a binding. It resolves the match for the current invocation only and re-evaluates the live.well-knowneach time, so the trust decision stays fresh and a one-off clone leaves no durable state. The sole caller (the non-interactive git-remote-entire helper) can't prompt, so ephemeral resolution is the right scoping. Explicit bindings (entire-core context bind, teammate tooling) are still honored.entire auth contextslists anycluster_contextsentries with an unbind hint, and newentire auth unbind <host>revokes one without touching the underlying login context.2. Floor
HostInClusterwildcard at a registrable domain (discovery)HostInClustertreated any*.clusterhost as same-cluster via a bare suffix match. With a broad cluster host that makes an entire public suffix in-cluster (clusterio→ every*.ioinherits the Authorization-carry / replica-trust boundary), so a relinquished or misconfigured subdomain anywhere under that suffix becomes a credential receiver.x/net/publicsuffix).*.entire.iostill matchesentire.io;*.ioand*.co.ukno longer wildcard-match. Exact matches and IP literals are unaffected.Scope note
With #1 in place an attacker can no longer durably pin
*.example.comtrust via a one-off clone. Theexample.com(eTLD+1, attacker-registrable) case still passes the public-suffix floor — fully closing that needs a signed/endorsed cluster manifest, which is intentionally out of scope here.Testing
HostInClusterspecificity-floor cases, binding surfacing inauth contexts, andauth unbind(idempotent, preserves the context).mise run fmt && mise run lintclean;go test -tags=integration,authfilestore -race ./...and the e2e canary pass.🤖 Generated with Claude Code
Note
High Risk
Changes authentication trust boundaries for entire:// git operations and redirect/replica credential scoping; mistakes could break clones or leak tokens.
Overview
Hardens the
entire://clone trust model in two ways.Cluster auth resolution no longer persists
cluster_contextsafter a successful/.well-knowndiscovery match—each git operation re-reads discovery so a one-off malicious clone cannot leave a silent, durable auto-auth channel. Explicit bindings (e.g. deliberatecontext bind) still apply.entire auth contextsnow lists those bindings with an unbind hint, andentire auth unbind <host>removes a binding without deleting the login context.HostInClustersubdomain matching now requires the cluster host to be a registrable domain (viagolang.org/x/net/publicsuffix), so bare suffixes likeioorco.ukcannot treat an entire TLD as in-cluster for credential-carrying redirects/replicas.Tests cover ephemeral discovery, binding audit/unbind, and the hostname specificity floor.
Reviewed by Cursor Bugbot for commit d8bc4b9. Configure here.