[management] Validate the proxy cluster an agent network bootstraps onto

The agent network gateway service is synthesised as private: agents reach
it over the WireGuard tunnel, authorised by their peer identity, with the
cluster itself as its only target. Only a reverse proxy cluster with
private capabilities can serve that, which management reports per cluster
as the `private` capability — the same supports_private flag the dashboard
gates NetBird-only services on.

CreateSettings took any hostname as proxy_address and only normalised it,
so a bootstrap could pin an account to a cluster without private
capabilities. The endpoint is immutable, leaving a dead gateway until the
settings row is deleted and re-bootstrapped.

Both bootstrap paths now validate the picked cluster before an endpoint is
allocated: a cluster the account can see must have a connected proxy
reporting the capability, shared and account-owned alike. Whether
management knows a cluster comes from its proxy rows, never from how fresh
their heartbeats are, so a cluster without the capability stays refused
while its proxies are merely offline. A hostname no proxy has declared
stays pinnable, the address-first order the self-addressed path documents.
Cluster identity is compared case-insensitively over the account's cluster
list, since proxies declare their address as the operator spelled it.

Rebased onto main after #7519 landed; the ownership check this builds on
is main's now.

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-14 20:25:48 +00:00
co-authored by Claude Fable 5.1
parent ea294e1d46
commit 6125b3d6f0
6 changed files with 477 additions and 1 deletions
@@ -13,6 +13,7 @@ import (
networkmap "github.com/netbirdio/netbird/management/internals/controllers/network_map"
"github.com/netbirdio/netbird/management/internals/modules/agentnetwork"
agenttypes "github.com/netbirdio/netbird/management/internals/modules/agentnetwork/types"
rpproxy "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/permissions"
"github.com/netbirdio/netbird/management/server/store"
@@ -92,6 +93,7 @@ func TestAgentNetwork_ProviderCRUD_FansOutToProxyAndClientPeers(t *testing.T) {
// UpdateAccountPeers, which is the path under test.
agentMgr := agentnetwork.NewManager(am.Store, permissions.NewManager(am.Store), am, nil)
seedPrivateProxyCluster(t, am.Store, clusterAddr)
_, err = agentMgr.CreateSettings(ctx, adminUserID, agenttypes.DefaultSettings(accountID), clusterAddr, "")
require.NoError(t, err, "CreateSettings must bootstrap the endpoint")
// The bootstrap itself reconciles and queues updates on both channels;
@@ -222,3 +224,22 @@ func synthZoneRData(sync *nbproto.SyncResponse, clusterAddr, fqdn string) string
}
return ""
}
// seedPrivateProxyCluster registers a connected proxy with private capabilities in a
// netbird client for clusterAddr, matching what a real deployment looks like
// when the account bootstraps: the agent-network gateway service is always
// private, so its cluster has to be one that can serve private services.
func seedPrivateProxyCluster(t *testing.T, st store.Store, clusterAddr string) {
t.Helper()
private := true
now := time.Now().UTC()
require.NoError(t, st.SaveProxy(context.Background(), &rpproxy.Proxy{
ID: "agent-net-proxy-" + clusterAddr,
SessionID: "agent-net-session",
ClusterAddress: clusterAddr,
LastSeen: now,
ConnectedAt: &now,
Status: rpproxy.StatusConnected,
Capabilities: rpproxy.Capabilities{Private: &private},
}), "seeding the proxy cluster must succeed")
}