From 2ee84e475ae3da0b6459d875e41549912a1750ae Mon Sep 17 00:00:00 2001 From: mlsmaycon Date: Sat, 12 Sep 2026 17:04:41 +0000 Subject: [PATCH] [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 Claude-Session: https://claude.ai/code/session_01Sa3DsBDP3VciAi4PPG17L6 --- .../internals/modules/agentnetwork/manager.go | 13 +++---- .../agentnetwork/settings_bootstrap_test.go | 35 +++++++++++++++++++ 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/management/internals/modules/agentnetwork/manager.go b/management/internals/modules/agentnetwork/manager.go index e277e65c8..5cc8ecced 100644 --- a/management/internals/modules/agentnetwork/manager.go +++ b/management/internals/modules/agentnetwork/manager.go @@ -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 { diff --git a/management/internals/modules/agentnetwork/settings_bootstrap_test.go b/management/internals/modules/agentnetwork/settings_bootstrap_test.go index 7fc9a906a..b0a7255ee 100644 --- a/management/internals/modules/agentnetwork/settings_bootstrap_test.go +++ b/management/internals/modules/agentnetwork/settings_bootstrap_test.go @@ -526,3 +526,38 @@ func TestCreateSettingsRejectsHostAnotherAccountPinned(t *testing.T) { } }) } + +// TestCreateSettingsSelfAddressedRequiresPrivateCluster pins that the +// capability gate applies to a self-addressed endpoint too: the service behind +// it is the same private one, so a proxy that already declares the hostname +// must be an embedded one, whether the account's own or a shared cluster's. A +// hostname no proxy declares yet stays claimable (TestCreateSettingsSelfAddressed). +func TestCreateSettingsSelfAddressedRequiresPrivateCluster(t *testing.T) { + ctx := context.Background() + + t.Run("centralised proxy at the hostname is refused", func(t *testing.T) { + f := newBootstrapFixture(t) + f.seedProxy(t, "central", "", "gw.example.com", ptrTo(false)) + f.expectPermission("account1", "user1", modules.AgentNetworkSettings, operations.Create, true) + + _, err := f.createSettings(ctx, "account1", "user1", "", "gw.example.com") + require.Error(t, err, "a self-addressed endpoint on a centralised proxy can never be served") + var sErr *status.Error + require.ErrorAs(t, err, &sErr) + assert.Equal(t, status.InvalidArgument, sErr.Type()) + assert.Contains(t, err.Error(), "embedded proxy") + + _, err = f.store.GetAgentNetworkSettings(ctx, store.LockingStrengthNone, "account1") + assert.Error(t, err, "no row may be left behind by a rejected bootstrap") + }) + + t.Run("embedded proxy at the hostname is accepted", func(t *testing.T) { + f := newBootstrapFixture(t) + f.seedProxy(t, "embedded", "", "gw.example.com", ptrTo(true)) + f.expectPermission("account1", "user1", modules.AgentNetworkSettings, operations.Create, true) + + created, err := f.createSettings(ctx, "account1", "user1", "", "gw.example.com") + require.NoError(t, err) + assert.Equal(t, "gw.example.com", created.ProxyAddress) + }) +}