Repository navigation
[client] add MDM configuration profile support (Windows registry + macOS plist) - #6374
Merged
Merged
Conversation
This can be useful for troubleshooting and checking config now that its resolution is not trivial defaults > config > env cars > CLI/UI > MDM
This also provide fixup for UI no aligning to changed config when coming from cli up with config flags.
UI sends full config snapshot with all values. It doesn't make sense to block it if the values are aligned with the values constrained by the MDM policy. It's just simplier to allow values that are compliant. (this goes for the CLI as well at this point)
…to service This commit tries to settle things with an old PR-4237 which had relaxed the case where the SetConfig returned an `Unavailable` code error. Under this circumnstance the PR allowed the upFunc to just emit a warning and progress further with the login gRPC. Since the login call is consuming the --management-url coming from the `up` command, it might be possible to abuse the "Unavailable" code to inject a management URL that is different from the configured one even though the --disable-update-settings is set to true (?)
pappz
requested changes
Jun 9, 2026
…r whatever reason We want to just signal this through the giveUpChan, we will manage the signal from the waiter side and in case set it to false there. THis way we avoid locking, which should allow the MDM down+wait_for_term_chan_signal_+up procedure clientRunning is used to signal two different conditions here: 1. the initialization procedure is over (we have an engine) 2. the connection being up (or being attempted) Probably these two functionalities should not alias, and the failure of the second condition (because of any error) should just drive a reconnection (currently it's not happening, and we silently go idle). OR, mor probably, the two things are the SAME and there should not exist a case where we did the "Up" initialization and connection attempt but we are not still attempting it.
pappz
requested changes
Jun 10, 2026
…her instance We instead juts SKIP updating the policy, so 1. the MDM ticker will kick in 1 minute time, 2. find the policy misaligned, 3. enter the onMDMPolicyChange, 4. find the s.clientRunning == true (because it is set to false only in server cleanupConnection, and not by s.actCancel()) 5. call s.actCancel() again if not nil 6. immediately return from <-s.clientGiveUpChan 7. finally call s.restartEngineForMDMLocked()
If the config was cancelled midflight, connect will abort later on
DisableAutoConnect should just avoid the connection attempts *when the service starts*. If we are started and we are up and running, DisableAutoConnect should not kick in. Another PR will follow about this topic
DisableAutoConnect should just avoid the connection attempts *when the service starts*. If we are started and we are up and running, DisableAutoConnect should not kick in
2 of 7 tasks
pappz
requested changes
Jun 11, 2026
This was initially used to drive the UI but approach changed to reload config/features upon notifications which makes this data redundant.
PoliciesEqual → policiesEqual AllKeys → allKeys
|
pappz
approved these changes
Jun 12, 2026
2 of 7 tasks
Merged
4 of 8 tasks
3 of 8 tasks
4 of 8 tasks
2 of 7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Describe your changes
First MDM integration milestone for NetBird. Adds a
client/mdm/package thatreads OS-native managed-configuration sources (Windows registry under
HKLM\Software\Policies\NetBirdand macOS plist at/Library/Managed Preferences/io.netbird.client.plist) and applies them tothe daemon's resolved configuration as the highest-priority layer. Scope of
this PR is Windows + macOS only; iOS and Android coverage is deferred to
follow-up PRs.
Highlights:
New package
client/mdm/with per-OS loaders behind build tags. SharedPolicystruct with typed accessors (GetString/GetBool/GetInt/GetStringSlice), case-insensitivecanonicalKeylookup so admins canuse either PascalCase ADMX-style names or camelCase plist-style names,
1-minute reload ticker with
reflect.DeepEqualdiff + change callback,and a
SecretKeysredaction set for log safety. 16 keys are surfaced:managementURL,disableUpdateSettings,disableProfiles,disableNetworks,disableClientRoutes,disableServerRoutes,blockInbound,disableMetricsCollection,allowServerSSH,disableAutoConnect,preSharedKey,rosenpassEnabled,rosenpassPermissive,wireguardPort,splitTunnelMode,splitTunnelApps.Daemon integration in
client/internal/profilemanager/config.go:Config.apply()now runsapplyMDMPolicy(LoadPolicy())as the lastlayer (defaults → on-disk JSON → env → CLI/UI → MDM). MDM-derived
values are never persisted back to
default.json/<profile>.json,so un-enrollment takes effect on the next reload with no stale policy
on disk.
Server.Startarms the MDM ticker; any diff triggersrestartEngineForMDMwhich re-resolves config + re-spawnsconnectWithRetryRuns+ emitsSystemEvent{type=config_changed, source=mdm}so the GUI tray / CLI subscribers can refresh withoutpolling. Same event is now also emitted from
Server.Start(sourcestartup) andServer.Up(sourceup_rpc).Per-field MDM gate on
SetConfig(client/server/server.go) and onLogin(same file) — value-aware: a request that sets a field to thesame value the policy already enforces is a no-op echo, not a
conflict. Conflicts return
codes.FailedPreconditionwith the newMDMManagedFieldsViolation { repeated string fields }proto detaillisting the offending key names. The
Logingate is required becausethe CLI
netbird up --flag=...path falls through toLoginwhenSetConfigis rejected (legacy backward-compat behaviour); withoutthe matching
Logingate, MDM policy would be bypassable from theCLI.
checkProfilesDisabled/checkUpdateSettingsDisabled/checkNetworksDisablednow resolve MDM-first: when the MDM key ispresent in the policy, that value wins (true OR false), and the CLI
--disable-*flag is the fallback only when the MDM source is silenton the key. This honours the "MDM is authoritative" semantic: an
admin pushing
disableNetworks=falsevia MDM re-enables the featureeven on a host installed with
--disable-networks.gRPC surface:
GetConfigResponse.mDMManagedFields[]lists thecurrently MDM-enforced key names so the UI can grey out the
corresponding inputs. New
MDMManagedFieldsViolationmessage used asgRPC error detail (see point 4).
Legacy Fyne tray UI (
client/ui/client_ui.go): submenu checkboxes(
Allow SSH,Connect on Startup,Enable Quantum-Resistance,Block Inbound Connections) get a(MDM)title suffix andDisable()when MDM-managed; the value itself is sourced from thedaemon-resolved config so the checkbox always reflects ground truth.
Advanced Settings form:
iMngURLandiInterfacePortget an inline(MDM)suffix in the text; password entryiPreSharedKeyuses anempty Text + placeholder (
MDM-managed/configured) so the eyetoggle never leaks the redaction sentinel.
mProfile.setEnabled(false)now disables parent + children (Manage Profiles / Deregister /
per-profile sub-items) instead of
Hide()so the user sees agreyed-out menu rather than a missing menu.
UI 2-second GetFeatures poll replaced with event-driven refresh
hooked to
config_changed. One initial sync at boot; the periodicloop continues to handle status/connection updates (no event for
those yet). Transition caches (
profilesEnabled,networksMenuEnabled) avoid systrayClickedChsubscription dropfrom repeated Enable/Disable calls.
New
netbird debug configsubcommand(
client/cmd/debug.go) dumps the daemon'sGetConfigResponseasJSON via protojson with
EmitUnpopulated:true— used for verifyingMDM application end-to-end (the
mDMManagedFieldsarray is thesingle source of truth for "which keys is the daemon actually
enforcing right now").
Admin manifests + deployment helpers (
docs/):netbird.admx+netbird.adml— Group Policy / Intune ADMXingestion. All 16 keys.
netbird-macos.mobileconfig— Apple Configuration Profile templatewith
com.apple.ManagedClient.preferencespayload targeting bundleio.netbird.client.netbird-policy.reg— Windows.regsample (full set of 16 keys)for
reg importor sidecar attachment.netbird-policy.reg.ps1— JumpCloud Windows command companion thatimports an attached
netbird-policy.regafter wiping the existingpolicy key (single source of truth).
macos-netbird-mdm.sh— JumpCloud macOS command counterpart;inline-variable script that emits the plist.
Issue ticket number and link
Internal scoping task. Anchor for reviewers: the previous
community attempt at MDM support is at
#5986 — this PR is a from-scratch
rewrite with the gaps identified in that PR's review fixed
(daemon-side enforcement, gRPC
mDMManagedFieldssurface, per-fieldFailedPrecondition, nomdm.jsonintermediate file, 1-minute reloadticker).
Stack
Checklist
Documentation
Select exactly one:
This PR ships the admin-facing manifests inline under
docs/:io.netbird.client.plistnetbird-macos.mobileconfignetbird-macos.shnetbird-policy.regnetbird-policy.reg.ps1netbird.admlnetbird.admxAdditionally the PR is paired by a
netbirdio/docsPR for the end-user-facing adminguide (linked below). The in-tree manifests target tooling that
expects them at known paths (ADMX/ADML in
PolicyDefinitions,.mobileconfigforprofiles install); thenetbirdio/docsPRcovers the narrative ("how do I push the NetBird policy from Intune /
Jamf / JumpCloud") that doesn't fit inside a manifest.
Docs PR URL (required if "docs added" is checked)
netbirdio/docs#783
Summary by CodeRabbit
New Features
UI
Tests
Documentation