Commit Graph
1081 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 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
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
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
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
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
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
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
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
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
dmitri-netbird a144e8c144 [client, management] switch to go.uber.org/mock (#7253)
* switch to go.uber.org/mock/gomock

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

* updated go:generate commands + regenerated mocks

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

* update go:generate mockgen commands

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

* removed duplicate import

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

* fix go:generate

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

---------

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
2026-08-20 11:53:19 +02:00
Maycon Santos d5b283dca8 [management] Refuse a usage limit a one-off setup key cannot honour (#7220)
refuse creating one-off keys without limits set to 1
2026-08-18 18:36:42 +02:00
Maycon Santos f805c149d9 [management] Record reverse proxy usage for activity accounting (#7116)
People who only ever reach private services through the reverse proxy were
invisible to activity accounting. Active users are counted from user.LastLogin
or from the LastSeen of a peer they own, and neither column was written on the
proxy paths — so a person signing in via SSO to a proxied service, or a peer
serving one over the mesh, never showed up in the 24 hour numbers.
Both writes now happen where the proxy already authenticates:
- GenerateSessionToken stamps LastLogin after the session token is signed,
  the same column and the same way the dashboard and device login paths do.
- ValidateTunnelPeer stamps the calling peer's LastSeen, the column its owner
  activates through.
The policy lives in a new reverseproxy/activity manager rather than in the gRPC
service, matching the module layout the other reverse proxy domains use. It
skips what can never count — service users, embedded proxy peers and WASM
clients — and throttles peer writes to once an hour, well inside the window
accounting asks about and far above the proxy's five minute tunnel cache.
The peer write is a single indexed UPDATE that touches only
peer_status_last_seen. Connected and SessionStartedAt are left alone so the
session-ownership fencing MarkPeerConnectedIfNewerSession relies on is never
disturbed, and the timestamp comes from the database clock rather than the
caller, for the same reason the other status writers take it from there. The
caller's cutoff travels into the statement's WHERE, so concurrent requests for
one peer collapse into a single write instead of each acting on its own stale
read, and a peer that was never seen — NULL last seen, since Status is an
embedded pointer — still records its first activity.
Nothing outside the reverse proxy changes behaviour: the only addition
elsewhere is the RefreshPeerLastSeen store method the manager calls.
2026-08-11 15:54:39 +02:00
Brad Ison ebfdf7d7b8 [management] Rework Agent Network endpoint identity and settings bootstrap (#7085)
Store the per-account gateway endpoint as {domain, proxy_address} with a
global unique index on the full hostname; dedicated = (domain ==
proxy_address). Bootstrap becomes an explicit POST carrying exactly one
of proxy_address (server allocates an adjective-noun label beneath it)
or endpoint (claimed verbatim, address-first); provider create loses its
bootstrap side effect. PUT is a full replace with every field required —
the immutable identity fields must be echoed unchanged and a mismatch is
rejected with 422. A guarded DELETE releases the endpoint: refused with
412 while providers exist or a proxy is actively serving the endpoint
hostname (matched case-insensitively); re-creating bootstraps fresh. A
self-addressed pin excludes its address from the account's cluster allow
list, and the live mapping update path now addresses the serving proxy
from the synthesized service. Existing rows are migrated on all three
store engines.
2026-08-10 19:06:55 +02:00
Maycon Santos f65f7b347e [management] Deny reverse proxy access to pending and blocked users (#7105)
A user in the Pending Approval state could complete SSO and reach any
SSO-protected reverse proxy service distributed to a group they belong
to, including the All Users group. The reverse proxy authorization path
checked the session token signature, that the user exists, that the
user's account matches the service's account, and group membership —
never the user's account status. The REST API (`permissions/manager.go`)
and peer registration both gate on that state, but the proxy gRPC
service does not go through the permissions manager, so neither gate
applied. A pending user is persisted as blocked and pending approval, so
blocked users reached those services the same way.

`ValidateSession` now denies on account status, reporting
`pending_approval` or `user_blocked` so the proxy access log and the
denied page carry the cause rather than a generic refusal.
`GenerateSessionToken` refuses to mint a token for such a user at all,
so the browser never receives a session cookie and the OIDC callback can
tell the user why instead of showing "Service configuration error".
`ValidateUserGroupAccess` and `ValidateTunnelPeer` close the same gap;
for the tunnel path this covers a user blocked after their peer was
registered, since peer group membership alone kept mesh-origin access
open.

A single helper produces both the denied reason for the RPC responses
and the sentinel error for the error-returning callers, so the four
entry points cannot drift apart. A user the store cannot resolve is
denied rather than passed through.

One thing deliberately left out: session cookies are validated locally
by the proxy against the service public key with no management
round-trip, so a cookie issued before a user is blocked stays valid
until it expires (24h by default). That is a revocation-propagation
problem rather than this authorization gap, and every option for it
(per-request validation with a cache, short-lived tokens with refresh,
push-based revocation) changes the proxy hot path or the
proxy/management protocol. Worth its own ticket.
2026-08-08 20:48:34 +09:00
Pascal Fischer 2ee21d2b5c [management] Affected peers for user updates (#7099) 2026-08-07 18:07:53 +02:00
Pascal Fischer 524b8b9718 [management] prewarm a posture check cache on network map generation (#7093) 2026-08-07 15:03:40 +02:00
Maycon Santos 6526fc2bec [management] Prevent deleting groups referenced by reverse proxy services (#7062)
## Describe your changes

A group could be deleted while a reverse proxy service still referenced
it, silently breaking the service's access control: private services
list groups in `access_groups` as the peer allowlist, and SSO bearer
auth distributes tokens to `distribution_groups`.

Group deletion now runs through the same linkage validation as routes,
policies, and agent network policies: deleting a group that backs a
private service allowlist or an enabled bearer-auth distribution list
fails with a `GroupLinkError` naming the service domain. Disabled bearer
configs and stale `access_groups` on non-private services are inert and
do not block deletion.

Tests cover both linked cases in single and bulk deletion, and pin the
non-blocking cases. The test account seeds decoy services ahead of the
linked ones so the check is proven to scan the full service list.
2026-08-05 03:24:20 +09:00
Misha Bragin 2afa69b622 [management] prevent dangling group refs in agent-network ACLs. (#7060)
Block deleting a group referenced as a source group by an agent network
   policy, and drop unresolvable groups from synthesised private-service
ACLs. A deleted group survived in agent_network_policies.source_groups
   and was carried into the injected in-memory policy, where network-map
   assembly resolved it to a nil group and panicked on every proxy peer
   sync.
2026-08-04 18:04:22 +02:00
Maycon Santos bc7a15ab71 [management] Align agent-network API contracts for API clients (#7026)
## Describe your changes

Work on the Terraform provider (terraform-provider-netbird #177–#183)
surfaced places where the agent-network API broke its own contracts or
deviated from the conventions the rest of the management API follows,
forcing client-side workarounds.

Settings reads now follow the settings-endpoint convention: GET always
answers with a JSON object. Before bootstrap it returns the defaults
with an empty cluster/subdomain/endpoint (previously 200 with a JSON
`null` body, while the spec said 404). The settings PUT can bootstrap
the account by carrying a `cluster` — previously the row could only come
into existence through the first provider create, and a settings-first
setup was impossible; a differing cluster on a bootstrapped account is
rejected instead of silently ignored. PUT remains full-state.

The provider PUT schema promised omit-preserves semantics for several
operator-editable fields that the handler never delivered (it builds the
row from the request, like every other update handler). The schema
wording now matches the shipped full-state behavior; only the api_key
(secret) and session keys stay preserved by the manager. Identity
headers are always present in provider responses so an explicitly
cleared value round-trips as an empty string.

The Go REST client gains the full agent-network surface (catalog,
providers, policies, guardrails, budget rules, settings), including a
shim translating the legacy 200+`null` settings body from older servers
into an `IsNotFound` error.

Note for reviewers: the dashboard special-cased the `null` settings
body; it needs a small follow-up for the new defaults response (in
progress).
2026-08-04 01:59:09 +02:00
Maycon Santos 2bfd9fcffe [management] Resolve agent network permissions per submodule (#7030)
## Describe your changes

Agent Network gates providers, policies, guardrails, budgets, usage,
access logs, and settings behind the single `agent_network` permission
module, so access is all-or-nothing: a future delegated role cannot be
scoped to a subset of the area (for example usage-only visibility).

This introduces dotted submodules (`agent_network.providers`,
`.policies`, `.guardrails`, `.budgets`, `.usage`, `.logs`, `.settings`)
and resolves grants with a cascade: exact module first, then its parent,
then the role's `AutoAllowNew` default. The agent network manager now
validates each operation against its matching submodule. `usage`
(aggregated counters, overview) is deliberately separate from `logs`
(request-level entries, which can contain captured prompts).

No role definitions change. No built-in role carries an explicit
`agent_network` entry, so every role resolves the submodules exactly as
it resolved the parent module before — pinned by a test that compares
each built-in role's answer on every submodule against its answer on
`agent_network`. Role additions that use these submodules come
separately.
2026-08-03 12:45:17 +02:00
Brad Ison 7639655883 [management] Generic gRPC extension seam for external modules (#6894)
## Describe your changes

This adds an extension point to the management server for registering
additional gRPC services. We already have a generic integrations system
and dependency injection for server components. This closes the gap on
being able to also extend the gRPC API cleanly.

## Issue ticket number and link

N/A

## Stack

<!-- branch-stack -->

### Checklist
- [ ] Is it a bug fix
- [ ] Is a typo/documentation fix
- [x] Is a feature enhancement
- [ ] It is a refactor
- [ ] Created tests that fail without the change (if possible)
- [ ] This change does **not** modify the public API, gRPC protocols,
functionality behavior, CLI / service flags, or introduce a new feature
— **OR** I have discussed it with the NetBird team beforehand (link the
issue / Slack thread in the description). See
[CONTRIBUTING.md](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTING.md#discuss-changes-with-the-netbird-team-first).

> By submitting this pull request, you confirm that you have read and
agree to the terms of the [Contributor License
Agreement](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT.md).

## Documentation
Select exactly one:

- [ ] I added/updated documentation for this change
- [x] Documentation is **not needed** for this change (explain why)

No docs needed. This is strictly a small internal plumbing enhancement /
refactor.

<!-- codesmith:footer -->
---
<a
href="https://app.blacksmith.sh/netbirdio/codesmith/netbird/pr/6894"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-light-v2.svg"><img
alt="View with [code]smith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"></picture></a>
<a
href="https://backend.blacksmith.sh/track/enable-autofix?expires=1787582002&installation_model_id=427504&pr_number=6894&repository=netbirdio%2Fnetbird&return_to=https%3A%2F%2Fgithub.com%2Fnetbirdio%2Fnetbird%2Fpull%2F6894&signature=3288061677db243031830964fec8f0f34c82f7fc63a39298cd0b4e3490551060"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-light.svg"><img
alt="Autofix with [code]smith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"></picture></a>
<sup>Need help on this PR? Tag <code>@codesmith-bot</code> with what you
need. Autofix is disabled.</sup>

<!-- codesmith:autofix:disabled -->
<!-- /codesmith:footer -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a gRPC extension mechanism to contribute additional services and
automatically chain extra unary and stream interceptors.
  * Extension shutdown hooks now run as part of server stop.
* Added exported proxy token generation via `GenerateProxyToken()` for
external integrations.
* **Tests**
* Added coverage for extension interceptor/service wiring, extension
shutdown execution, and proxy token generation validation (including
hash consistency and prefix).
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-03 12:26:38 +02:00
Pascal Fischer dd2bdc0de3 [management] explicit accountID check when deleting a user (#6944) 2026-07-28 18:34:55 +02:00
Viktor Liu 9269b56386 [management] Read reverse-proxy service and target columns in Postgres path (#6886) 2026-07-28 14:46:19 +02:00
Viktor Liu b3f9b82442 [management] Force routing-peer DNS resolution for reverse-proxy domain targets (#6872) 2026-07-28 14:45:57 +02:00
Maycon Santosandbraginini 4f6247b5c3 [management, proxy] Add prompt-cache token and cost accounting to agent network usage (#6900)
Co-authored-by: braginini <bangvalo@gmail.com>
2026-07-26 21:42:41 +02:00