[management] Validate the cluster behind a self-addressed endpoint too

The service behind a self-addressed endpoint is the same private one as
behind a labeled endpoint, so a proxy that already declares the hostname
must be an embedded one for the gateway to be served at all. The check ran
only on the labeled path; a self-addressed pin onto a hostname a centralised
proxy declares went through and left a dead gateway. Both paths now validate
the cluster, with the same address-first exception for a hostname no proxy
has declared yet.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sa3DsBDP3VciAi4PPG17L6
This commit is contained in:
mlsmaycon
2026-09-12 17:04:41 +00:00
co-authored by Claude Fable 5.1
parent 5110871524
commit 2ee84e475a
2 changed files with 42 additions and 6 deletions
@@ -1045,6 +1045,9 @@ func (m *managerImpl) bootstrapSelfAddressed(ctx context.Context, settings *type
if err := m.requireNotClaimedByOtherAccount(ctx, settings.AccountID, hostname, m.store.HasGatewayClusterPinnedByOtherAccount); err != nil {
return err
}
if err := m.validateGatewayCluster(ctx, settings.AccountID, hostname); err != nil {
return err
}
settings.Domain = hostname
settings.ProxyAddress = hostname
@@ -1063,8 +1066,10 @@ func (m *managerImpl) bootstrapSelfAddressed(ctx context.Context, settings *type
return nil
}
// validateGatewayCluster rejects a labeled bootstrap pinned to a cluster that
// cannot serve the account's gateway.
// validateGatewayCluster rejects a bootstrap pinned to a cluster that cannot
// serve the account's gateway — a labeled endpoint beneath the cluster and a
// self-addressed one on the very address a proxy declares alike, since the
// service behind either is the same private one.
//
// The synthesised gateway service is unconditionally private
// (buildAccountService): agents reach it over the WireGuard tunnel and are
@@ -1162,10 +1167,6 @@ func (m *managerImpl) accountClusterSpellings(ctx context.Context, accountID, cl
// checked by read and the domain unique index stays the authority, so a
// concurrent allocation of the same tuple surfaces as a unique violation and
// another tuple is drawn.
//
// Unlike the self-addressed path this pins to a cluster that must already
// exist, so the cluster is validated before an endpoint is allocated beneath
// it.
func (m *managerImpl) bootstrapLabeled(ctx context.Context, settings *types.Settings, proxyAddress string) error {
parent, err := types.NormalizeHostname(proxyAddress)
if err != nil {