Commit Graph
1245 Commits
Author SHA1 Message Date
mlsmayconandClaude Fable 5.1 4ed71f8987 [management] Fold migrated agent network identity on MySQL too
The normaliser's predicate never matched under MySQL's default
case-insensitive collation, and the comment called that the right answer
because nothing on MySQL was invisible. Only the SQL lookups are tolerant
there: the domain lookup is followed by an exact Go compare in
SynthesizeServiceForDomain, and the proxy's host map is keyed by the domain
verbatim, so a mixed-case row still missed. Spell the predicate byte-wise on
MySQL so the fold applies.

Two rows whose identities differ only by case would fold onto one endpoint,
which the unique index refuses with a driver message that names no row.
Both the reshape and the normaliser now stop first and name the hostname
that needs a human, in the same voice as the reshape's existing loud
failure.

The comments cited SNI folding as the reason the columns must be lowercase;
the SNI router folds both sides and would have matched. The readers that
miss are the exact ones, and the comments now name those.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sa3DsBDP3VciAi4PPG17L6
2026-09-12 13:36:36 +00:00
mlsmayconandClaude Fable 5.1 d80f0ff031 [management] Keep an established claim when its re-read is inconclusive
SaveProxy upserts on the proxy ID, so on a reconnect the row Connect just
wrote is the claim the account has held since its first connect, and the
session guard on DeleteProxy matches because the upsert wrote the new
session. Withdrawing that row whenever the post-write re-read errored
surrendered an established claim on a transient store error — a window in
which any other account could take the address — where the pre-existing
code left the row untouched.

An inconclusive re-read still refuses the connect, but marks the session
disconnected instead of deleting the row; only a conclusive answer that the
address is claimed withdraws it. The write-then-re-read argument moves to
the doc of the exported ErrClusterAddressUnavailable, where the API needs
it, and both helpers point there instead of carrying it twice.

Store-backed tests drive the re-read through the real queries — a
reconnect keeps its row, the account's own pin is not a competing claim,
another account's is — since the whole path now depends on the store
excluding the account's own claims.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sa3DsBDP3VciAi4PPG17L6
2026-09-12 13:36:36 +00:00
mlsmayconandClaude Fable 5.1 e6c69f674d [management] Ask ownership for self-addressed pins too
A self-addressed bootstrap stores the hostname as proxy_address, which is
exactly what a proxy registration is refused on when another account holds
it there. It asked nobody whether it could: the domain unique index
arbitrated between pins, and a foreign proxy already declaring the host was
never consulted, before or after the insert. Any account could therefore pin
an endpoint onto a host another account's proxy serves — owning nothing —
and lock that proxy out on its next reconnect, and a proxy racing such a pin
could end with both claims standing, since only the labeled path re-read
ownership after its write.

The self-addressed path now asks HasForeignAccountProxyAtHost before the
insert and confirmGatewayClusterOwnership after it, the same as the labeled
one. Address-first stays intact: only a row owned by a different account
refuses, so pinning ahead of any proxy, or onto the account's own, is
unchanged.

Also pins the bootstrap side's failure paths — an ownership re-read that
cannot answer leaves no pin behind and surfaces the store's error, and a
withdrawal that fails still reports the claim as lost — and shortens the
helper's comment to point at the shared argument on
proxy.ErrClusterAddressUnavailable rather than restate it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sa3DsBDP3VciAi4PPG17L6
2026-09-12 13:36:35 +00:00
mlsmayconandClaude Fable 5.1 68da6bf3aa [management] Withdraw a cluster address claim that is lost after the write
A cluster address is claimed two ways: an account-scoped proxy row, and an
agent network gateway pin on the address. Each side checked the other
before writing — IsClusterAddressAvailable before SaveProxy,
HasForeignAccountProxyAtHost before the settings insert — but check and
write are separate autocommit statements, so two concurrent claimants could
each pass their check and both commit, leaving a pin no proxy will ever
serve next to the proxy row that displaces it.

Both sides now re-read after they write. Manager.Connect re-asks
availability once the proxy row is committed and, if the address is no
longer free or the answer is inconclusive, deletes its own row and returns
ErrClusterAddressUnavailable, which the connect path reports as
AlreadyExists exactly as the pre-write check would have. bootstrapLabeled
re-asks ownership once the settings row is committed and withdraws the pin
on the same terms. Because both write before they re-read, of two
concurrent claimants at least one re-reads after the other has committed
and backs off — on sqlite, postgres and mysql alike, since each statement
sees every commit before it. Both may back off, which costs a retry;
neither keeps a claim the other holds.

No lock spans the proxies and settings tables portably, and a claims table
would be more machinery than the property needs, so the re-read is the
whole mechanism. DeleteProxy is session-guarded like DisconnectProxy, so a
stale session withdrawing itself cannot take out a newer session's row.

Reported by CodeRabbit on #7402 (CWE-362).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sa3DsBDP3VciAi4PPG17L6
2026-09-12 12:53:15 +00:00
mlsmayconandClaude Fable 5.1 39f8ea3f70 [management] Fold casing on agent network identity the legacy schema kept
The legacy bootstrap stored the cluster as the caller spelled it — trimmed,
never folded — and the reshape that shipped in 0.78 copied it into
proxy_address, and subdomain.cluster into domain, verbatim. Everything that
reads those columns compares against canonical lowercase: proxies
canonicalise their address at connect, the gateway-pin check a proxy
registration runs matches proxy_address exactly, cluster-scoped synthesis
finds an account's row by proxy_address, and the proxy folds the SNI host
before matching a mapping's domain. A row that kept capitals is invisible
to all of them — its pin protects nothing and its endpoint never routes.

The reshape now writes LOWER() for both columns, and an idempotent
normaliser lowercases rows a released reshape already copied, registered
right after it. The predicate selects only rows that would change, so a
normalised table costs one pass over one row per account; on MySQL the
default collation compares case-insensitively already and it is a no-op.

Reported by cubic on #7402: the exact proxy_address match a proxy
registration relies on misses a migrated pin with capitals.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sa3DsBDP3VciAi4PPG17L6
2026-09-12 12:53:15 +00:00
mlsmaycon 138cb3b3e0 [management] Cover the gateway-pin claim against a real store
Belongs with 1b6a9d8 — the file was new and untracked, so the commit that
added the query it exercises went out without it.

Drives HasGatewayPinnedByOtherAccount through sqlite: another account is
refused the host, the pinning account may still claim it (pin first,
deploy the proxy after), an unpinned host stays free, and a labeled pin
claims the cluster address rather than the labeled endpoint beneath it.
2026-09-12 12:06:18 +00:00
mlsmaycon 1b6a9d86a0 [management] Refuse a proxy claiming a host another account's gateway pinned
The bootstrap check refused pinning onto a cluster another account runs,
but nothing held that decision afterwards: a proxy registration consulted
the proxies table alone, so the same host could be claimed by another
account a moment later, or a week later, and the pin it stranded was
immutable. Validating only at bootstrap meant the check was true when it
ran and not after.

Make the claim symmetric. IsClusterAddressAvailable now treats a gateway
pin as what it is — a claim on the host, served by whichever proxy
declares that address — and refuses a proxy from a different account,
since an account-scoped proxy never receives another account's mappings
and so cannot serve the pin it would displace. Both claims are checked in
one place so a caller cannot consult one and forget the other. An account
claiming the address its own gateway is pinned to is the documented
order, not a conflict: pin first, deploy the proxy after.

Shared (NetBird-operated) proxies register without an account and are
unaffected; an account-scoped proxy reaching for a shared cluster address
that accounts are pinned to was already refused by the proxy-row conflict
and now stays refused even if those rows are momentarily gone.

Whichever of the two lands first now wins and the second is refused,
which is what the bootstrap check could not do on its own. A genuinely
concurrent pair can still pass both checks — closing that needs an
invariant spanning the proxies and settings tables, not a wider read.
2026-09-12 12:05:13 +00:00
mlsmaycon a403a8d275 [management] Decide gateway cluster ownership before the account's own view
The ownership check only ran when the account's own view of the cluster
came back empty, so an account holding any row for the host — its own
canonical proxy — skipped it entirely. A row another account left behind
under a non-canonical spelling was then never seen, and the pin the check
exists to refuse went through. That is the case worth catching, not the
one to skip: two accounts claiming one hostname is the ambiguity the
connect-time conflict check prevents going forward and cannot see for a
row written before addresses were canonicalized, and the endpoint pinned
here cannot be moved afterwards.

Ask ownership first, and narrow what counts as foreign while doing it.
The query treated a shared proxy as outside the account, which was
harmless while it ran only for a host the account had no view of — a
shared row would have given it one — but refuses the cluster most
accounts pin to once it runs first. Only a row owned by a different
account is foreign now, which is also what the name says.
2026-09-12 09:10:55 +00:00
Maycon Santos b449b31cc6 Merge branch 'main' into agent-network-validate-proxy-cluster 2026-09-12 10:45:11 +02:00
Bethuel Mmbaga 82b1c7da22 [management] Harden OIDC issuer validation and discovery (#7435) 2026-09-11 19:09:13 +03:00
Maycon Santos b789ffbb9f [management] Expire unvalidated custom domain registrations (#7497)
Prevent unvalidated registrations from reserving domain names indefinitely.

Give pending registrations a 48-hour validation window and clean up expired entries at startup and every 60 minutes. Emit CustomDomainValidationExpired for each deletion and preserve registrations referenced by services.

Reject validation after expiry and prevent concurrent validation from recreating deleted registrations. Normalize domain names with the shared parser before registration.

Migrate existing pending registrations to receive a fresh 48-hour validation window.
2026-09-11 17:57:58 +02:00
dmitri-netbird f422c41654 [management] extract peer update logic and wrap it in tests (#7338)
* extract peer update loop into a dedicated struct and wrap it in tests

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* make linter happy

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

---------

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
2026-09-11 17:07:26 +02:00
Bethuel Mmbaga 58114f98fb [management] Only trust forwarded-IP headers from configured trusted peers (#7454) 2026-09-11 15:41:52 +03:00
Pascal Fischer 1047df5fa2 [management] pass tls config for combined server (#7499) 2026-09-11 13:50:11 +02:00
Pascal Fischer add8a75981 [management] validate peer existence when adding to group (#7486) 2026-09-11 13:49:20 +02:00
Maycon Santos 21b4a83cea [management] Refuse services on unvalidated custom domains (#7341)
Require validated custom domains when creating or updating reverse proxy services. 
Propagate validation errors during updates and return HTTP 409 for duplicate domain claims.

Add regression tests for domain validation, ownership, and service creation and updates.
2026-09-10 11:57:14 +02:00
Brad Ison 27991aab98 [management] Let embedding binaries extend the command tree (#7483)
The management binary is embedded by downstream builds that override
server construction via SetNewServer, but the cobra command tree itself
was closed: rootCmd is unexported and fully assembled in init, with no
way to attach additional subcommands. Customize hands the built root
command to a caller-supplied function before Execute, so an embedding
binary can add its own commands next to — or under — the built-in ones,
such as extra administrative helpers beneath the existing admin group.
2026-09-09 15:36:37 +02:00
Pascal Fischer 269cbadfeb [management] expire and disconnect peers while including offline peers (#7467) 2026-09-09 13:44:19 +02:00
Pascal Fischer 7a62d63a36 [management] fix delete of owner user (#7456) 2026-09-08 13:47:32 +02:00
dmitri-netbird 00003814f3 [management] update network_router_test to verify empty and nil peer_groups (#7425)
* update network_router_test to verify empty and nil peer_groups

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* fix an issue with deserialization of nil json array in user.go

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* order zones by id

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

---------

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
2026-09-04 15:29:51 +02:00
Zoltan Papp 825389818c [client] Gather fresh system info on every management sync stream connect (#7409)
* Gather fresh system info on every management sync stream connect

The engine collected the peer meta once at start and reused the same
Info for every Sync stream reconnect, so a mobile network switch that
redials management kept reporting the old local network addresses.
The peer network range posture check was then evaluated against stale
data until the client restarted.

Sync now takes a gatherer that runs at each stream connect. The
gatherer is cheap: GetInfo plus the cached posture check file results,
kept in the new system.InfoSource, which the engine refreshes whenever
the checks list changes. No process enumeration runs on the reconnect
path.

Also fix the management mock server calling itself instead of SyncFunc.

* Evaluate the login response posture checks before the first sync connect

The engine starts with the checks the login response carried, and the
first sync stream request used to send their evaluated file results.
After moving the gather into InfoSource, the stream opened with an empty
cache and the first sync response did not refill it, because its checks
equal the ones the engine already holds. Desktop peers therefore never
reported process or file posture results.

Seed the cache once before the first connect, where the old gather ran,
so a timed out evaluation still falls through to the address-only info.

* Harden the sync info source against nil callbacks and shared slices

A nil getInfo opens the stream without metadata, as a nil sysInfo did
before. The cached posture results are a copy, so the Info returned by
Refresh cannot alias the snapshot later Current calls report. The
exclusion test asserts the remaining address count so it cannot pass
vacuously on a single-address host.

* Retry a posture check refresh that timed out or failed to sync

The checks list was recorded before the gather ran, so once the gather
timed out or SyncMeta failed, the next sync response carrying the same
list matched the recorded one and nothing retried. The peer kept
reporting the previous posture results until the list changed again.

Record the checks only after the meta reached management, so a failed
cycle is repeated on the next sync response.

* Log the skipped posture refresh, let the mock Sync return errors and deflake the reconnect test

* Drop the nil guard around the sync info callback

* Send the refreshed info on the first sync connect instead of gathering it twice
2026-09-04 15:07:01 +02:00
Pascal Fischer 0bdfa4277e [management] blocking sync requests for user peers sharing the same wireguard key (#7427) 2026-09-04 13:10:51 +02:00
Bethuel Mmbaga 066af82c3e [management] Keep embedded IdP deployments on a single account (#7380) 2026-09-04 11:03:54 +03:00
Bethuel Mmbaga 13ab50b901 [management] Add SetNX and GetDel cache store operations (#7084) 2026-09-04 11:03:28 +03:00
dmitri-netbird c455a4ac31 [management] disallow weird ip addresses for direct upstream hosts (#7400)
* disallow weird ip addresses for direct upstream hosts

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* handle bracketed ipv6 addresses

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* extend the check to subnet service targets

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* fix spelling

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* reject ipv6 addresses with zones

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* catch host:port hostnames in services with subnet targets

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* make linter happy

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

---------

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
2026-09-04 10:00:35 +02:00
Maycon Santos 8dc4272519 [management] Serve networks with peer-based routers from the SQLite network map (#7418)
The SQLite network-map query expanded a router's groups with from network_routers, json_each(peer_groups). That comma is an inner join, so a router row survives only when json_each returns at least one row. A router targeting an individual peer carries no groups — the write path stores NULL for a nil slice and '[]' for an empty one — and json_each yields nothing for either, so the join erased the router before it could be keyed by its peer. Postgres reads the same rows through a correlated subquery and was never affected.

The fix expands the groups with a left join, so the router survives with a NULL group_peers.peer_id and the existing scan loop keys it by router.Peer. Group routers still fan out one row per member.
2026-09-04 08:14:02 +02:00
Viktor Liu c2b5d211d9 [management] Enforce reverse proxy group access before minting and when honouring a session cookie (#7240) 2026-09-04 07:15:20 +02:00
mlsmaycon ba3bc8c56f [management] Keep the ingress address contract and the ownership fallback
Two follow-ups to canonicalizing the proxy address.

netip accepts a zoned literal where net.ParseIP did not, so "fe80::1%eth0"
started passing validation and would have been stored as a cluster key. An
address scoped to one host's interface cannot name a cluster others reach,
so zones are rejected, as before.

Making the ownership query exact also left the bootstrap check without a
fallback for rows written before canonicalization: a foreign cluster
stored as "BYOP.Account2.Example.com" no longer matches the normalized
address, reads as never declared, and the pin it should refuse goes
through. Split the two callers instead of choosing between them.
IsClusterAddressConflicting stays exact for the per-connect path that
needs the index; HasProxyOutsideAccountAtHost folds case for the
bootstrap, which runs once per account and is the only thing standing
between it and pinning its immutable endpoint to somebody else's cluster.
The settings delete guard already made that trade for the same reason.

Restores the foreign-casing case that went with the exact query, and adds
the zone cases to the ingress test.
2026-09-03 12:11:05 +00:00
mlsmaycon 61e1742885 [management] Canonicalize the proxy cluster address where it is stored
The proxy-connect path already computes the canonical form of the address
a proxy declares — ValidateDomains returns lowercase punycode — and then
throws it away, storing the string as declared. cluster_address is the key
every capability, ownership and routing lookup matches on, so one host
could sit in that column under two spellings, and the previous commit
compensated with LOWER() in the ownership query, which gives up the
cluster_address index on a query that runs for every account-scoped proxy
connect.

Keep the canonical form instead. Connect is the only writer of the column
(heartbeats touch last_seen and status), and proxy rows are session state
rebuilt on every connect rather than durable config, so the column
converges without a migration and the lookups can stay exact and indexed.

Folding happens before punycode conversion, not after: idna lowercases the
ASCII it produces but does not case-fold the unicode it consumes, so
PRÖXY.example.com and pröxy.example.com would otherwise encode to two
different labels for one host.

The agent network check keeps comparing normalised forms in memory, which
costs nothing there — it is a pass over the account's cluster list, not a
query — and covers rows written before this landed.
2026-09-03 10:19:04 +00:00
mlsmaycon d911649158 [management] Compare agent network cluster addresses case-insensitively
Proxies store their cluster address as they declared it, while
proxy_address is normalised lowercase before validation. The capability
and ownership lookups match cluster_address exactly, so feeding them the
normalised form asked about a spelling the store may never have seen: a
private cluster declared with capitals came back unproven and was
refused, and — worse — another account's cluster declared with capitals
came back as "never declared" and let the pin through.

Compare identity on the normalised form but keep the stored spellings,
and read the capability under each of them, any-true, the same way it
aggregates over a cluster's proxies. Ownership gets the same treatment at
the source: hostnames are case-insensitive, so two spellings of one host
are one cluster and must conflict rather than being claimable side by
side, which also closes the same gap in the proxy-registration
availability check that shares the query.

The e2e's wait for a stopped cluster to leave the active list now allows
for the active window rather than 90s: a proxy that dies without closing
its stream is only dropped once its last heartbeat ages past
proxyActiveThreshold, so the old budget could fail the test on the slow
path alone.
2026-09-03 07:21:44 +00:00
Maycon Santos 6aaeed744e [management] Check a provider's url and credential before saving it (#7301)
A bad upstream or key saved cleanly and surfaced minutes later as a failed
request or an empty model picker, with nothing pointing back at the record.

CreateProvider now spends the credential once against the vendor's model
listing. UpdateProvider does the same when the upstream, the key, the catalog
provider or the skip-TLS flag changed — only then, so renames and price edits
neither wait on a vendor nor fail because one is down. Both run before the store
write, so a rejected rotation leaves the working key where it was.

What cannot be checked still saves: no listing endpoint, no derivable Bedrock
control-plane host, a private upstream, a record skipping TLS verification.
Everything else blocks, outages included — 5xx, 429 and timeouts leave the
record unverified just as a refusal does. Refusals return 422 and carry no
status code or echoed URL.

Discovery now reads as a partial edit, so a retyped URL can be listed against
without also rotating the credential. Entries with their own listing host
(Bedrock) get their configured upstream resolved separately, since a successful
listing said nothing about it.
2026-09-02 21:46:47 +02:00
Maycon Santos 26e5495e5d [management,proxy] Serve guardrail allowlists of declared model ids (#7389)
After #7221, guardrail allowlists built from a path-style provider's declared
model ids (Bedrock, Vertex) stopped working: the raw region/version form was
compared against the parser's canonical id, so the agent config advertised an
empty model list and requests for the allowlisted model were refused.

Make every allowlist compare provider-aware, keyed on the destination
provider's catalog id: the agent config, the policy gate, and the synthesized
proxy allowlists match an entry on both its verbatim and canonical form —
Bedrock's strip only under bedrock_api, Vertex's only under vertex_ai_api,
verbatim everywhere else, so a plain provider's suffixed entries never widen.
The router's claim compare learns the Vertex @version strip.

New e2e, realstore, and unit tests reproduce both regressions and pin the fix.
2026-09-02 18:57:02 +02:00
Pascal Fischer 8a5e940c84 [management] remove old math rand lib (#6836) 2026-09-02 17:52:51 +02:00
Zoltan Papp ecbeba8e67 [management] Fix geolocation panics (#7382)
* [management] Return errors instead of panicking on malformed geolocation inputs

* [management] Reject empty date suffix in geolocation database filename
2026-09-02 12:36:03 +02:00
mlsmaycon 0a33ad8979 [management] Validate the proxy cluster an agent network bootstraps onto
The synthesised agent network gateway service is unconditionally private:
agents reach it over the WireGuard tunnel and are authorised by
ValidateTunnelPeer against the enabled policies' source groups, and its
only target is the cluster itself with DirectUpstream. Only a proxy
running embedded in a netbird client can serve that, which management
already reports per cluster as the `private` capability.

CreateSettings accepted any hostname as proxy_address, so a labeled
bootstrap could pin an account to a cluster that cannot serve its
gateway — another account's BYOP cluster, or one whose proxies are all
centralised. The endpoint assigned at bootstrap is immutable, so the
account is then stuck with a dead gateway until someone deletes and
re-bootstraps the settings row.

Validate the cluster before allocating an endpoint beneath it. Whether
management knows a cluster is decided on its proxy rows, never on how
fresh their heartbeats are: the rows outlive their proxies' liveness, so
a known cluster stays judged as one and has to prove with a live embedded
proxy that it can serve the gateway. Deciding on liveness instead would
let the same centralised cluster pass or fail depending on whether its
proxies had heartbeated in the last couple of minutes, turning "wait for
the proxy to go quiet" into a way to pin the endpoint to a cluster that
can never serve it. Ownership comes from the same time-independent
source, so a foreign cluster stays refused while it is offline.

Only a cluster no proxy has ever declared is still pinnable — that is the
address-first order the dedicated (self-addressed) path documents, and
the one self-hosted setups follow when they configure before deploying.
2026-09-02 09:59:29 +00:00
Pascal Fischer e3d6c3d0eb [management] fix private services calc on new db path (#7383) 2026-09-01 20:20:30 +02:00
Maycon Santos ebc259e30b [management,client] Gate remote jobs behind an admin opt-in with MDM support (#7153)
This introduces a disabled-by-default allow-remote-jobs setting that
controls whether the management server may run jobs (such as debug
bundles) on a peer. The flag propagates end to end: through client
configuration, the daemon SetConfig and Login requests, authentication,
and system info, up to management, where it is stored on the peer and
exposed on the peers API as remote_jobs_allowed. The client refuses any
management-requested job unless the peer has opted in. Because enabling
remote jobs crosses the user-to-root boundary, turning it on requires
privilege, mirroring the SSH-server gate. Administrators can enforce the
setting through MDM policy on both macOS and Windows, and MDM can also
override the debug-bundle upload URL. The change ships policy
documentation and generated profile templates, and adds configuration,
conflict, and enforcement tests covering the opt-in, privilege, and MDM
paths.
2026-09-01 17:53:41 +02:00
Maycon Santos 3027130f0f [management] Add Agent Network access roles and self-service endpoints (#7221)
Delegating Agent Network today means handing out full account admin, and
regular users cannot see their own usage or how to connect a local tool.

Add two roles on top of the existing agent_network permission
submodules. agent_network_admin owns the whole area (providers,
policies, guardrails, budgets, usage, logs, settings) with read-only
users, groups, peers, and account info needed to build policies, and
nothing else in the account. usage_viewer is the regular User baseline
plus read on the aggregated usage and cost overview: no provider
configuration, no policies, no request-level logs, which can contain
captured prompts. billing_admin gets a proper permission-map entry with
the User baseline so role resolution stops failing with role-not-found;
its plan and invoice permissions stay enforced cloud-side.

Add the self-service endpoints behind the "My Agent Network" view,
available to every authenticated user because both answers are scoped
strictly to the caller. GET /api/agent-network/me/setup returns the
account endpoint plus the providers and models the caller's own groups
authorize, computed with the same rules the proxy enforces: policy
filtering as in policy selection, model allowlist union intersected
with declared models, orphan and disabled providers omitted. Not set up
and no access are deliberately indistinguishable, and the response
carries display metadata only. GET /api/agent-network/me/consumption
returns the caller's own user-dimension counters.
2026-09-01 14:35:38 +02:00
Daneyon Hansen 7a9582db16 [management,proxy] Add agentgateway integration (#7274)
* [management] Add agentgateway provider catalog entry

Allow Agent Network providers to target an operator-supplied agentgateway proxy while stamping trusted NetBird identity headers.

Signed-off-by: Daneyon Hansen <daneyon.hansen@solo.io>

* [proxy] Allow trusted Agent Network identity headers

Permit only the built-in identity injector to replace the two reserved agentgateway attribution headers while keeping them blocked for every other middleware.

Signed-off-by: Daneyon Hansen <daneyon.hansen@solo.io>

* [management,proxy] Add multi-vendor gateway routing

Let one Agent Network route declare multiple parser surfaces while preserving the existing singular vendor wire field.

Signed-off-by: Daneyon Hansen <daneyon.hansen@solo.io>

* [management] Update router test for model policies

Signed-off-by: Daneyon Hansen <daneyon.hansen@solo.io>

* [proxy] Cover reserved header policy

Signed-off-by: Daneyon Hansen <daneyon.hansen@solo.io>

* [management] Add agentgateway model discovery

Use agentgateway's OpenAI-compatible models endpoint and omit wildcard patterns until NetBird can authorize and price them consistently.

Signed-off-by: Daneyon Hansen <daneyon.hansen@solo.io>

---------

Signed-off-by: Daneyon Hansen <daneyon.hansen@solo.io>
2026-09-01 13:03:16 +02:00
Maycon Santos 1081ca006d [management,client] Add anonymize level and upload URL to remote debug bundle jobs (#7147)
This extends the management-requested remote debug-bundle job with two
new, optional parameters. anonymize_level selects how aggressively the
bundle is scrubbed: "default" keeps internal (private) IP ranges
readable, while "strict" also anonymizes private, CGNAT and link-local
addresses; the value is trimmed and lowercased, and an unknown level is
rejected at creation. upload_url lets an operator point the peer at a
specific upload service instead of the default one; it must be a
well-formed https URL with a host, and an empty value falls back to the
default upload server. Both fields flow through the job workload API and
are surfaced in the create-debug-job modal on the dashboard. Validation
is shared so the client executor and the management boundary agree on
what a valid upload URL is, preventing drift between the two checks.
2026-09-01 11:45:20 +02:00
Theodor Midtlien 12e8874517 [client, relay, management] Bump go version to 1.26 and go-quic to v0.62.0 (#7359)
* Bump go version to 1.26 and go-quic to v0.62.0
* Replace deprecated ecdsa public key assembly and add tests for jwt
* Update goversioninfo
* Pin go toolchain to 1.26.7
2026-08-31 18:01:14 +02:00
Pascal Fischer 353251d886 [management] fix posture check evaluation for direct peers in policy definition (#7348) 2026-08-28 16:46:42 +02:00
Pascal Fischer 611a9291cd [management] fix posture check flip evaluation for affected peers calc (#7347) 2026-08-28 15:39:48 +02:00
Pascal FischerandDmitri Dolguikh e06c17cf59 [management] network map from nmap data type (#6919)
Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
Co-authored-by: Dmitri Dolguikh <dmitri.external@netbird.io>
2026-08-27 11:28:05 +02:00
Viktor Liu 51095cb986 [client, management] Support per-peer lazy connection state and default proxy peers to lazy (#6762)
* Support per-peer lazy connection state and default proxy peers to lazy

* Classify forward targets from incoming config in lazy exclusion

* Set IsUserspaceBind mock so lazy manager starts in engine test

* Skip lazy exclude reconciliation when the set is unchanged

* Keep cached lazy flag when a sync carries no peer config
2026-08-26 09:33:51 +02:00
dmitri-netbird c512bf25aa [management] handle nil ptr in sendInitialSync() when the peer is deleted (#7315)
* fix a nil-ptr error occuring in sendInitialSync when the peer being synced is deleted

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* handle a nil ptr in GetPeerNetworkMapComponents

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

---------

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
2026-08-25 16:14:17 +02:00
Pascal Fischer 7d83a3902d [proxy] validate header auth on proxy (#7263) 2026-08-25 13:46:26 +02:00
Maycon Santos f03853867b [proxy,management] Serve Bedrock model discovery from the control plane (#7250)
[proxy,management] Serve Bedrock model discovery from the control plane

A Bedrock provider could never answer a model-discovery request. The router
sent GET /inference-profiles to the record's upstream, which has to be
bedrock-runtime.<region> for InvokeModel to work, and that host does not
implement the operation. ListInferenceProfiles is a control-plane operation on
bedrock.<region>.amazonaws.com, and one provider record carries one upstream,
so the two hosts genuinely differ.

The route now carries a discovery host, taken from the catalog's declaration
with the region read back out of the configured upstream, and the listing — and
only the listing — goes there. Inference is untouched. A proxied or self-hosted
Bedrock endpoint gets no discovery host at all rather than a guessed one, since
inventing a host would send the operator's credential somewhere they never
configured.

Two things had to follow for the listing to be usable once it arrives. The
response filter only understood OpenAI's {"data":[{"id":…}]}, so a Bedrock
listing fell through it untouched, offering every profile in the account
whatever the policy said. And discoverableModels intersected by exact string,
so a record registering the raw profile id while a guardrail names the catalog
key intersected to nothing — bounding a working provider's listing down to
empty.

Normalisation is the third. The geography in front of a cross-region profile
was matched against a hardcoded list of four, so every profile issued under jp,
au, ca, sa or us-gov carried its prefix into the pricing key, matched no
catalog entry and metered at zero. It is now recognised by either the geography
or the vendor that follows it, so an id has to be new on both axes at once to
slip through — a live eu-central-1 listing returned "global.xai.grok-4.6" days
after the vendor list was first written.
2026-08-23 20:29:10 +02:00
Maycon Santos 5e88d3f87a [management] Offer a provider's live model list in the config form (#7246)
[management] Offer a provider's live model list in the config form

Adds POST /api/agent-network/catalog/providers/models, which asks a vendor
which models an operator's own credential can actually reach, so the provider
form can offer a live list instead of only the compiled-in catalog. The catalog
goes stale, and it cannot see an account: which OpenAI models an org is
entitled to, which Bedrock inference profiles an account and region hold, which
Vertex models a project has enabled.

The endpoints, auth headers and response shapes come from probing the live APIs
(#7244); each vendor invented its own envelope and none can be guessed from the
request. Bedrock shaped the design: its listing lives on the control plane
while inference must go to the runtime host, so Discovery carries its own host
rather than reusing the record's upstream, and profile ids are taken verbatim
because the region prefix is what AWS requires at invoke time.

A caller supplies either the key they are typing or the id of a saved record
whose stored credential is reused — never both, since accepting both would run
an arbitrary credential under the identity of a record the caller may only be
permitted to read. Gated on Create rather than Read, because this spends the
operator's credential against a third party.

Management has not made outbound calls on an operator's behalf before and it
holds a credential for every provider, so every resolved address must be public
— covering loopback, RFC1918, the cloud metadata address and NetBird's own
100.64/10 range — and redirects are not followed, since a redirect moves the
request to a host the check never saw.

The vendor is authoritative for the id; the catalog stays authoritative for
pricing. A discovered model the shipped table cannot price returns
pricing_known: false so the operator must set rates rather than being
registered at a silent zero.
2026-08-23 20:21:25 +02:00
Maycon Santos 7be45c2dd8 [proxy,management] Bound model discovery to the caller's own policies (#7239)
[proxy,management] Bound model discovery to the caller's own policies

GET /v1/models was bounded by the provider record's enumerated models, which is
the right bound only while a single policy reaches a provider. Where two teams
share one provider under different allowlists, every caller was offered the
union — each model outside their own policy being a request the guardrail
refuses a moment later. A gateway record enumerating nothing was worse: it
offered the upstream's entire catalogue however narrow the policy was.

Each route now carries one rule per authorising policy — its source groups and
the models it permits — instead of a single flattened list. At request time the
router keeps the rules whose groups intersect the caller's, unions their
models, and intersects that with what the provider serves.

nil and [] stay distinct end to end: a policy setting no allowlist reaches the
router as nil and lifts the restriction for the groups it binds, while an
enabled allowlist with no models arrives as [] and permits nothing. Collapsing
them would let a listing that should offer nothing fall open to everything.

The guardrail's own per-provider allowlist is untouched. It is a fail-closed
backstop that cannot tell who is asking, so discovery is now narrower than the
backstop rather than wider.
2026-08-23 20:13:35 +02:00