Skip to content

Harden cluster trust: ephemeral binding + HostInCluster specificity floor#1309

Merged
Soph merged 3 commits into
mainfrom
soph/cluster-trust-hardening
Jun 1, 2026
Merged

Harden cluster trust: ephemeral binding + HostInCluster specificity floor#1309
Soph merged 3 commits into
mainfrom
soph/cluster-trust-hardening

Conversation

@Soph
Copy link
Copy Markdown
Collaborator

@Soph Soph commented Jun 1, 2026

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.json 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.

  • ResolveContextForCluster no longer persists a binding. It 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 (entire-core context bind, teammate tooling) are still honored.
  • Bindings are now auditable and revocable: entire auth contexts lists any cluster_contexts entries with an unbind hint, and new entire auth unbind <host> revokes one without touching the underlying login context.

2. Floor HostInCluster wildcard at a registrable domain (discovery)

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 (cluster io → every *.io inherits the Authorization-carry / replica-trust boundary), so a relinquished or misconfigured subdomain anywhere under that suffix becomes a credential receiver.

  • The subdomain (wildcard) match is now gated on the cluster host being strictly more specific than its own public suffix (via x/net/publicsuffix). *.entire.io still matches entire.io; *.io and *.co.uk no longer wildcard-match. Exact matches and IP literals are unaffected.

Scope note

With #1 in place an attacker can no longer durably pin *.example.com trust via a one-off clone. The example.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

  • New tests: ephemeral resolution (no binding persisted, re-discovers each call), HostInCluster specificity-floor cases, binding surfacing in auth contexts, and auth unbind (idempotent, preserves the context).
  • mise run fmt && mise run lint clean; 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_contexts after a successful /.well-known discovery 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. deliberate context bind) still apply. entire auth contexts now lists those bindings with an unbind hint, and entire auth unbind <host> removes a binding without deleting the login context.

HostInCluster subdomain matching now requires the cluster host to be a registrable domain (via golang.org/x/net/publicsuffix), so bare suffixes like io or co.uk cannot 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.

Soph and others added 2 commits June 1, 2026 17:31
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
Copilot AI review requested due to automatic review settings June 1, 2026 16:53
@Soph Soph requested a review from a team as a code owner June 1, 2026 16:53
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ 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.

Comment thread cmd/entire/cli/auth_context.go
Comment thread internal/entireclient/discovery/parse_replicas.go
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 HostInCluster wildcard matching (via x/net/publicsuffix).
  • Surface and revoke explicit cluster_contexts bindings via auth contexts output and a new auth unbind command (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.

Comment thread internal/entireclient/discovery/parse_replicas.go Outdated
Comment thread internal/entireclient/discovery/parse_replicas.go Outdated
Comment thread internal/entireclient/discovery/parse_replicas_test.go
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
@Soph
Copy link
Copy Markdown
Collaborator Author

Soph commented Jun 1, 2026

Thanks bots — addressed in 61b0c10:

  • IP literals wildcard-matching (Copilot, real bug): Fixed. clusterAllowsSubdomains now returns false for IP literals, so an IP cluster only matches the exact host — evil.127.0.0.1 no longer suffix-matches 127.0.0.1. Added a regression test and updated the doc comment to state IP clusters have no subdomain semantics.
  • Bindings hidden when no login contexts (Cursor, Medium): Fixed. runAuthContexts no longer early-returns; it prints the empty-state hint and still falls through to the bindings section, so an orphaned binding (manual edit / deleted context) is surfaced. Added a test for that case.
  • Single-label cluster regression, e.g. foo.localhost (Cursor, Low): Working as intended. A single-label name is its own public suffix, so it's rejected from the wildcard path on purpose — treating *.localhost (or any internal single-label TLD) as same-cluster is exactly the over-broad trust the floor exists to prevent. Exact-match still covers localhost/IP replicas, which is what the local-dev and test setups actually use. Documented in the clusterAllowsSubdomains comment.

@Soph Soph merged commit 8b6eaa2 into main Jun 1, 2026
9 checks passed
@Soph Soph deleted the soph/cluster-trust-hardening branch June 1, 2026 21:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

4 participants