Adds doc for Windows/macOS MDM integration (#783)

* Adds doc for Windows/macOS MDM integration

* Fixes coderabbit suggestions

* Update public/docs-static/files/netbird-policy.reg.ps1

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Check port and allow/disallow in admitted values for macos script

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
Riccardo Manfrin
2026-06-19 22:47:02 +02:00
committed by GitHub
parent 15fdd20777
commit 59405c5e73
9 changed files with 1291 additions and 0 deletions

View File

@@ -0,0 +1,126 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<!--
NetBird MDM preferences (macOS) — bare plist for MDM platforms that
accept a managed-preferences plist tied to a bundle identifier
(e.g. JumpCloud "Mac Application Custom Settings", Mosyle "Custom
Settings", Jamf "Application & Custom Settings" → External
Application).
Bundle identifier (preference domain): io.netbird.client
The MDM provider will wrap this plist into a Configuration Profile
payload of type com.apple.ManagedClient.preferences and push it to
target devices via the Apple MDM protocol. The OS materializes the
final file at:
/Library/Managed Preferences/io.netbird.client.plist
which is what the NetBird daemon's client/mdm/policy_darwin.go
loader reads on every 1-minute MDM reload tick.
For MDM platforms that expect a full Configuration Profile instead
of a bare plist (Custom Configuration Profile / .mobileconfig upload),
use docs/netbird-macos.mobileconfig — same keys, additional Payload*
envelope.
Editing this file:
- Remove or comment out any key you do NOT want to enforce. The
daemon treats an absent key as "no enforcement" for that field.
- Keep the document well-formed XML. Validate locally with:
plutil -lint docs/io.netbird.client.plist
- Keys are camelCase; values are typed (<string>, <true/>, <false/>,
<integer>). See docs/src/pages/client/mdm-integration.mdx (the
public docs page) for the full reference.
Persistence caveat:
macOS wipes /Library/Managed Preferences/ at every boot on
devices that are NOT MDM-enrolled. This plist only sticks across
reboots when delivered through a real MDM channel. For local
testing on an un-enrolled host, write the file manually as root
and accept it will not survive the next boot.
-->
<plist version="1.0">
<dict>
<!-- ===== Identity / auth ===== -->
<key>managementURL</key>
<string>https://api.netbird.io:443</string>
<!--
Pre-shared key: secret. Remove the entry entirely when not used;
do NOT leave an empty <string></string>, which the daemon would
otherwise treat as a deliberate empty-PSK enforcement.
-->
<!--
<key>preSharedKey</key>
<string>REPLACE_ME</string>
-->
<!-- ===== Engine / runtime behavior =====
Each key is optional. Remove or comment out to leave the
field unmanaged on the client. -->
<key>allowServerSSH</key>
<true/>
<!--
<key>disableAutoConnect</key>
<false/>
<key>disableClientRoutes</key>
<false/>
<key>disableServerRoutes</key>
<false/>
<key>blockInbound</key>
<false/>
<key>rosenpassEnabled</key>
<true/>
<key>rosenpassPermissive</key>
<false/>
-->
<!-- ===== WireGuard UDP port =====
Range 1-65535. Omit to keep the daemon default. -->
<!--
<key>wireguardPort</key>
<integer>51820</integer>
-->
<!-- ===== UI / lockdown kill switches =====
disableUpdateSettings : block every config change from UI and CLI
on this device (Settings view stays
readable but read-only).
disableProfiles : hide the profile menu, reject profile CRUD.
disableNetworks : hide the Networks / Exit Node menus,
reject the related RPCs.
disableMetricsCollection: opt out of anonymous usage telemetry. -->
<!--
<key>disableUpdateSettings</key>
<true/>
<key>disableProfiles</key>
<true/>
<key>disableNetworks</key>
<true/>
<key>disableMetricsCollection</key>
<false/>
-->
<!-- ===== Split tunnel =====
Android-only at the client level. Safe to ship on macOS for
mixed-platform fleets; the macOS daemon parses and ignores. -->
<!--
<key>splitTunnelMode</key>
<string>allow</string>
<key>splitTunnelApps</key>
<string>com.acme.app1,com.acme.app2</string>
-->
</dict>
</plist>

View File

@@ -0,0 +1,159 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<!--
NetBird MDM configuration profile (macOS).
Wraps a `com.apple.ManagedClient.preferences` payload that pushes the
NetBird MDM policy into:
/Library/Managed Preferences/io.netbird.client.plist
Read at runtime by the netbird daemon's macOS loader
(client/mdm/policy_darwin.go — Phase 2). Key names match the canonical
lowerCamelCase form used in docs/netbird.admx and the mdm.Key*
constants in client/mdm/policy.go.
Bundle identifier: io.netbird.client
(confirm against the signed pkg before fleet roll-out)
Distribution:
- sign with `productsign --sign "Developer ID Installer: ..." ...`
before fleet roll-out (Apple-Configurator-2 won't install an
unsigned profile on Sonoma+ without user override).
- For local dev install: `sudo profiles install -path netbird-macos.mobileconfig`.
- For MDM (Jamf/Kandji/Mosyle/Intune): upload as a Custom Profile.
Editing:
- Replace UUID placeholders below with fresh UUIDs (`uuidgen` on
macOS) when forking this template for a real fleet — each
deployment should have unique UUIDs so the OS treats it as a
distinct profile.
- Tune the PayloadContent values to the policy you want to enforce.
- Remove any key you do NOT want to enforce (the daemon treats an
absent key as "no enforcement" for that field).
iOS note:
This file is macOS-specific. iOS uses managed app config via
UserDefaults[com.apple.configuration.managed] under a different
payload type (com.apple.app.configuration.managed); the wrapper
structure is the same but the inner payload dictionary differs.
See docs/netbird-ios.mobileconfig (Phase 5) when shipped.
-->
<plist version="1.0">
<dict>
<!-- Outer profile envelope -->
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>PayloadIdentifier</key>
<string>io.netbird.client.mdm</string>
<key>PayloadUUID</key>
<string>11111111-1111-1111-1111-111111111111</string>
<key>PayloadDisplayName</key>
<string>NetBird MDM Policy</string>
<key>PayloadDescription</key>
<string>Enforces NetBird client configuration. Values written here override any local user / CLI / on-disk setting and are re-applied at every daemon boot and on every 1-minute MDM reload tick.</string>
<key>PayloadOrganization</key>
<string>NetBird</string>
<key>PayloadScope</key>
<string>System</string>
<key>PayloadRemovalDisallowed</key>
<false/>
<key>PayloadContent</key>
<array>
<dict>
<!-- Managed preferences payload: writes /Library/Managed Preferences/io.netbird.client.plist -->
<key>PayloadType</key>
<string>com.apple.ManagedClient.preferences</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>PayloadIdentifier</key>
<string>io.netbird.client.mdm.preferences</string>
<key>PayloadUUID</key>
<string>22222222-2222-2222-2222-222222222222</string>
<key>PayloadDisplayName</key>
<string>NetBird Managed Preferences</string>
<key>PayloadEnabled</key>
<true/>
<key>PayloadContent</key>
<dict>
<key>io.netbird.client</key>
<dict>
<key>Forced</key>
<array>
<dict>
<key>mcx_preference_settings</key>
<dict>
<!-- ===== Identity / auth (strings) ===== -->
<key>managementURL</key>
<string>https://api.netbird.io:443</string>
<!-- Pre-shared key: secret. Remove the entry entirely
when not used; do NOT leave an empty string. -->
<!--
<key>preSharedKey</key>
<string>REPLACE_ME</string>
-->
<!-- ===== Engine / runtime behavior (bool) =====
Remove any key to leave the field unmanaged. -->
<!--
<key>disableAutoConnect</key>
<false/>
<key>disableClientRoutes</key>
<false/>
<key>disableServerRoutes</key>
<false/>
<key>blockInbound</key>
<false/>
-->
<key>allowServerSSH</key>
<true/>
<!--
<key>rosenpassEnabled</key>
<true/>
<key>rosenpassPermissive</key>
<false/>
-->
<!-- ===== WireGuard UDP port (int) =====
Range 1-65535. Omit to keep the default. -->
<!--
<key>wireguardPort</key>
<integer>51820</integer>
-->
<!-- ===== Split tunnel (Android-only at the daemon level)
Pushed harmlessly on macOS for fleets with mixed
desktop+mobile devices; the macOS daemon ignores it. -->
<!--
<key>splitTunnelMode</key>
<string>allow</string>
<key>splitTunnelApps</key>
<string>com.acme.app1,com.acme.app2</string>
-->
<!-- ===== UI / kill switches (bool) ===== -->
<!--
<key>disableUpdateSettings</key>
<true/>
<key>disableProfiles</key>
<true/>
<key>disableNetworks</key>
<true/>
<key>disableMetricsCollection</key>
<false/>
-->
</dict>
</dict>
</array>
</dict>
</dict>
</dict>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,202 @@
#!/bin/bash
#
# SYNOPSIS
# Push the NetBird MDM policy to a macOS device via JumpCloud Commands.
#
# DESCRIPTION
# This is the macOS counterpart of docs/netbird-policy.reg.ps1.
# It writes the values declared in the "POLICY VALUES" block below to
# the managed-preferences plist that the NetBird daemon's
# client/mdm/policy_darwin.go loader reads on every 1-minute MDM
# reload tick:
#
# /Library/Managed Preferences/io.netbird.client.plist
#
# Once the plist lands, the daemon picks up the new values without
# restart (the ticker calls Config.apply() → applyMDMPolicy() and
# restarts the engine on diff).
#
# DEPLOYMENT (JumpCloud)
# 1. Admin Console -> Device Management -> Commands -> +.
# 2. Type: Mac, Shell, Run as: root.
# 3. Paste this file verbatim into the command body.
# 4. Bind to the target system group, save, run.
#
# IMPORTANT: PERSISTENCE
# macOS wipes /Library/Managed Preferences/ at every boot on devices
# that are NOT MDM-enrolled. For a persistent fleet rollout, push the
# companion docs/netbird-macos.mobileconfig as a Custom Configuration
# Profile (Admin Console -> MDM -> Mac Custom Configuration Profiles)
# instead of this script. Use this script when:
# - the device is MDM-enrolled (file survives reboots), or
# - you need a one-shot test push before reboot, or
# - you orchestrate via JumpCloud Commands and want the same
# variable-driven workflow as the Windows .ps1 sibling.
#
# IDEMPOTENCY: re-running with the same values is a no-op from the
# daemon's point of view (the 1-minute reload ticker diff returns empty).
#
# SECURITY: PreSharedKey is redacted in this script's log output.
set -euo pipefail
### POLICY VALUES — EDIT THIS BLOCK ###########################################
#
# Set each variable below to the desired value. Set to empty string ""
# or to NULL to omit a key entirely (the daemon treats an absent key
# as "no enforcement" for that field). Booleans use "true"/"false"
# (lowercase). Integers as decimal.
#
# Reference for key names + accepted values:
# client/mdm/policy.go (Key* constants)
# docs/netbird-macos.mobileconfig (sample profile)
# docs/netbird.admx + .adml (Windows ADMX schema)
#
NULL='__UNSET__'
managementURL='https://api.netbird.io:443'
preSharedKey="$NULL" # secret; redacted in log
allowServerSSH='true'
blockInbound="$NULL"
disableAutoConnect="$NULL"
disableClientRoutes="$NULL"
disableServerRoutes="$NULL"
disableMetricsCollection="$NULL"
disableUpdateSettings="$NULL"
disableProfiles="$NULL"
disableNetworks="$NULL"
rosenpassEnabled="$NULL"
rosenpassPermissive="$NULL"
wireguardPort='51820'
splitTunnelMode="$NULL" # "allow" or "disallow", Android-only at the daemon level
splitTunnelApps="$NULL" # comma-separated app IDs, Android-only
##############################################################################
readonly PLIST_DIR='/Library/Managed Preferences'
readonly PLIST_PATH="$PLIST_DIR/io.netbird.client.plist"
readonly LOG_TAG='netbird-mdm'
# log sends a message to the system logger using the configured tag and echoes the message to stdout prefixed by an ISO 8601 UTC timestamp and the tag.
log() {
/usr/bin/logger -t "$LOG_TAG" "$*"
printf '%s [%s] %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$LOG_TAG" "$*"
}
# is_set returns success if the provided value is non-empty and is not equal to the special NULL marker.
is_set() {
local value="$1"
[[ -n "$value" && "$value" != "$NULL" ]]
}
# start_plist creates the temporary plist file at "$PLIST_PATH.tmp" containing the XML plist header and opening `<dict>` for the policy plist.
start_plist() {
cat > "$PLIST_PATH.tmp" <<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
EOF
}
# end_plist appends the closing `</dict>` and `</plist>` tags to the temporary plist file.
end_plist() {
cat >> "$PLIST_PATH.tmp" <<'EOF'
</dict>
</plist>
EOF
}
# emit_string appends a plist `<key>`/`<string>` entry for the given key and value to "$PLIST_PATH.tmp", XML-escaping `&`, `<`, and `>`, and logs the assignment (masking the logged value as `********** (secret)` when the key is `preSharedKey`).
emit_string() {
local key="$1" value="$2" log_value="$2"
# Escape XML entities in the value
local escaped
escaped="$(printf '%s' "$value" | sed -e 's/&/\&amp;/g' -e 's/</\&lt;/g' -e 's/>/\&gt;/g')"
printf ' <key>%s</key>\n <string>%s</string>\n' "$key" "$escaped" >> "$PLIST_PATH.tmp"
if [[ "$key" == "preSharedKey" ]]; then
log_value='********** (secret)'
fi
log "set $key = $log_value"
}
# emit_bool writes a boolean plist entry for a given key into the temporary plist file.
# emit_bool writes a boolean plist entry for a key when the provided value matches an accepted boolean token; logs an error and skips the key on invalid input.
emit_bool() {
local key="$1" value="$2"
local xml_bool
case "$value" in
true|True|TRUE|1|yes) xml_bool='<true/>' ; value='true' ;;
false|False|FALSE|0|no) xml_bool='<false/>' ; value='false' ;;
*) log "invalid boolean for $key: $value (must be true/false); skipping"; return ;;
esac
printf ' <key>%s</key>\n %s\n' "$key" "$xml_bool" >> "$PLIST_PATH.tmp"
log "set $key = $value"
}
# emit_int validates that VALUE contains only decimal digits and, if valid, appends an `<integer>` plist entry for KEY to the temporary plist (`$PLIST_PATH.tmp`) and logs the assignment; on invalid input it logs a skip and does not emit the key.
emit_int() {
local key="$1" value="$2"
if ! [[ "$value" =~ ^[0-9]+$ ]]; then
log "invalid integer for $key: $value (must be decimal); skipping"
return
fi
if [[ "$key" == "wireguardPort" ]] && (( value < 1 || value > 65535 )); then
log "invalid integer for $key: $value (must be 1-65535); skipping"
return
fi
printf ' <key>%s</key>\n <integer>%s</integer>\n' "$key" "$value" >> "$PLIST_PATH.tmp"
log "set $key = $value"
}
# emit_split_tunnel_mode validates that VALUE is "allow" or "disallow" and, if valid, appends a plist string entry for splitTunnelMode; on invalid input it logs a skip and does not emit the key.
emit_split_tunnel_mode() {
local value="$1"
case "$value" in
allow|disallow) emit_string splitTunnelMode "$value" ;;
*) log "invalid splitTunnelMode: $value (must be allow/disallow); skipping" ;;
esac
}
# main builds the NetBird MDM plist from configured policy variables, validates and installs it to /Library/Managed Preferences/io.netbird.client.plist (root:wheel, 644) and optionally triggers the NetBird daemon to reload.
main() {
log "applying NetBird MDM policy to $PLIST_PATH"
/bin/mkdir -p "$PLIST_DIR"
start_plist
is_set "$managementURL" && emit_string managementURL "$managementURL"
is_set "$preSharedKey" && emit_string preSharedKey "$preSharedKey"
is_set "$allowServerSSH" && emit_bool allowServerSSH "$allowServerSSH"
is_set "$blockInbound" && emit_bool blockInbound "$blockInbound"
is_set "$disableAutoConnect" && emit_bool disableAutoConnect "$disableAutoConnect"
is_set "$disableClientRoutes" && emit_bool disableClientRoutes "$disableClientRoutes"
is_set "$disableServerRoutes" && emit_bool disableServerRoutes "$disableServerRoutes"
is_set "$disableMetricsCollection" && emit_bool disableMetricsCollection "$disableMetricsCollection"
is_set "$disableUpdateSettings" && emit_bool disableUpdateSettings "$disableUpdateSettings"
is_set "$disableProfiles" && emit_bool disableProfiles "$disableProfiles"
is_set "$disableNetworks" && emit_bool disableNetworks "$disableNetworks"
is_set "$rosenpassEnabled" && emit_bool rosenpassEnabled "$rosenpassEnabled"
is_set "$rosenpassPermissive" && emit_bool rosenpassPermissive "$rosenpassPermissive"
is_set "$wireguardPort" && emit_int wireguardPort "$wireguardPort"
is_set "$splitTunnelMode" && emit_split_tunnel_mode "$splitTunnelMode"
is_set "$splitTunnelApps" && emit_string splitTunnelApps "$splitTunnelApps"
end_plist
if ! /usr/bin/plutil -lint "$PLIST_PATH.tmp" >/dev/null 2>&1; then
log "ERROR: generated plist failed plutil lint; not installing"
/usr/bin/plutil -lint "$PLIST_PATH.tmp" >&2 || true
/bin/rm -f "$PLIST_PATH.tmp"
exit 1
fi
/bin/mv -f "$PLIST_PATH.tmp" "$PLIST_PATH"
/usr/sbin/chown root:wheel "$PLIST_PATH"
/bin/chmod 644 "$PLIST_PATH"
log "policy installed; NetBird daemon will pick it up within the next 1-minute reload tick"
# Optional: kick the daemon for an immediate apply. Safe — does
# nothing on a host where NetBird is not yet installed.
/bin/launchctl kickstart -k system/io.netbird.client 2>/dev/null || true
}
main "$@"

Binary file not shown.

View File

@@ -0,0 +1,98 @@
#requires -Version 5.1
<#
.SYNOPSIS
Push the NetBird MDM policy to a Windows device via JumpCloud Commands
by importing a sidecar netbird-policy.reg file.
.DESCRIPTION
Windows counterpart of docs/netbird-macos.sh. Outcome:
HKLM\Software\Policies\NetBird populated from the attached
netbird-policy.reg file, daemon picks up the change via the
1-minute MDM reload ticker.
Deployment:
1. Admin Console -> Device Management -> Commands -> +.
2. Type: Windows PowerShell. Run as: SYSTEM.
3. Paste this file verbatim into the command body.
4. In the same command, attach `netbird-policy.reg` as a file.
JumpCloud copies attached files into the command's working
directory before invoking the script, so `$PSScriptRoot` or
Get-Location resolves to where the .reg lives.
5. Bind to the target system group, save, run.
Producing the .reg file:
On a reference machine, after configuring the policy values either
via gpedit (GPO) or manual `reg add`, export with:
reg export "HKLM\Software\Policies\NetBird" netbird-policy.reg /y
Then attach the resulting file to the JumpCloud command.
Semantics:
- The script nukes the existing HKLM\Software\Policies\NetBird key
before importing the .reg, so the .reg is the SINGLE SOURCE OF
TRUTH. Any value present in the registry but absent from the .reg
is removed. This is what an MDM admin almost always wants.
- Setting the .reg to an empty (header-only) file effectively unsets
the policy.
Idempotency: re-running the script with the same .reg is a no-op from
the daemon's perspective (values identical → 1-min ticker sees no
diff → engine not restarted).
Exit codes: 0 = success; 1 = .reg missing or reg.exe error.
#>
$ErrorActionPreference = "Stop"
$RegFileName = "netbird-policy.reg"
$RegKey = "HKLM\Software\Policies\NetBird"
# Resolve the attached .reg file: JumpCloud copies command attachments
# into C:\Windows\Temp\ before invoking the script. Cwd / $PSScriptRoot
# fallbacks cover the local-dev case where you might dot-source this
# from elsewhere.
$candidates = @(
(Join-Path "$env:WINDIR\Temp" $RegFileName)
(Join-Path (Get-Location) $RegFileName)
(Join-Path $PSScriptRoot $RegFileName)
) | Where-Object { Test-Path $_ }
if ($candidates.Count -eq 0) {
Write-Error "[netbird-mdm] $RegFileName not found in working directory or `$PSScriptRoot. Attach the file to the JumpCloud command."
exit 1
}
$regFile = $candidates[0]
Write-Host "[netbird-mdm] using $regFile"
# Wipe the existing policy key so the .reg is authoritative.
$existed = Test-Path "Registry::HKEY_LOCAL_MACHINE\Software\Policies\NetBird"
if ($existed) {
& reg.exe delete $RegKey /f | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Error "[netbird-mdm] failed to clear $RegKey before import (exit $LASTEXITCODE)"
exit 1
}
Write-Host "[netbird-mdm] cleared previous values under $RegKey"
}
# Import. reg.exe writes both data and (re-)creates the key if needed.
& reg.exe import $regFile
if ($LASTEXITCODE -ne 0) {
Write-Error "[netbird-mdm] reg import failed (exit $LASTEXITCODE)"
exit 1
}
# Audit dump so the JumpCloud per-execution log captures the applied state.
Write-Host "[netbird-mdm] final policy state under $RegKey :"
if (Test-Path "Registry::$RegKey") {
& reg.exe query $RegKey /s
} else {
Write-Host "[netbird-mdm] no policy values present under $RegKey"
}
# Daemon's 1-min reload ticker picks up the change automatically.
# Uncomment to force immediate convergence (skips the ticker wait):
# Restart-Service netbird -Force -ErrorAction SilentlyContinue
exit 0

View File

@@ -0,0 +1,95 @@
<?xml version="1.0" encoding="utf-8"?>
<policyDefinitionResources xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
revision="1.0"
schemaVersion="1.0"
xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions">
<displayName>NetBird Client Policies</displayName>
<description>Group Policy template for NetBird client MDM-managed settings. Values are written under HKLM\Software\Policies\NetBird and consumed by the netbird daemon at startup and every 1-minute reload tick.</description>
<resources>
<stringTable>
<!-- Categories -->
<string id="NetBird_Category">NetBird</string>
<string id="SUPPORTED_NetBird_All">NetBird Client 0.40+</string>
<!-- Identity / auth -->
<string id="ManagementURL_Name">Management URL</string>
<string id="ManagementURL_Help">URL of the NetBird management server. Format: https://host[:port]. When set, users cannot override this value via UI or CLI.</string>
<string id="PreSharedKey_Name">Pre-shared key</string>
<string id="PreSharedKey_Help">WireGuard pre-shared key used as an additional symmetric secret on every peer-to-peer tunnel. Secret value.</string>
<!-- Settings: engine / runtime behavior -->
<string id="DisableAutoConnect_Name">Disable auto-connect</string>
<string id="DisableAutoConnect_Help">When enabled, the NetBird tunnel does not auto-connect at daemon startup. Equivalent to --disable-auto-connect.</string>
<string id="DisableClientRoutes_Name">Disable client routes</string>
<string id="DisableClientRoutes_Help">When enabled, this client will not consume routes advertised by routing peers. Equivalent to --disable-client-routes.</string>
<string id="DisableServerRoutes_Name">Disable server routes</string>
<string id="DisableServerRoutes_Help">When enabled, this client will not act as a routing peer for other clients. Equivalent to --disable-server-routes.</string>
<string id="BlockInbound_Name">Block inbound</string>
<string id="BlockInbound_Help">When enabled, the client firewall blocks all inbound peer traffic on the WireGuard interface. Equivalent to --block-inbound.</string>
<string id="AllowServerSSH_Name">Allow server SSH</string>
<string id="AllowServerSSH_Help">When enabled, this client accepts incoming SSH sessions via NetBird SSH. Equivalent to --allow-server-ssh.</string>
<string id="RosenpassEnabled_Name">Enable Rosenpass</string>
<string id="RosenpassEnabled_Help">Enables Rosenpass post-quantum key exchange on WireGuard tunnels. Both peers must support it.</string>
<string id="RosenpassPermissive_Name">Rosenpass permissive</string>
<string id="RosenpassPermissive_Help">When enabled, the client falls back to plain WireGuard if a peer does not support Rosenpass; otherwise it refuses the connection.</string>
<string id="WireguardPort_Name">WireGuard port</string>
<string id="WireguardPort_Help">UDP port used by the local WireGuard interface. Allowed range: 1-65535.</string>
<string id="SplitTunnel_Name">Split tunnel</string>
<string id="SplitTunnel_Help">Restrict the NetBird tunnel to or from a chosen list of application package names. Choose either the allow mode (only the listed apps route through NetBird) or the disallow mode (the listed apps bypass NetBird; everything else routes through). The mode is mutually exclusive — only one can be active at a time. Android-only at the daemon level; Windows/macOS/iOS clients ignore this policy.</string>
<string id="SplitTunnel_Allow">Allow only listed apps (everything else bypasses)</string>
<string id="SplitTunnel_Disallow">Disallow listed apps (everything else routes)</string>
<!-- UI -->
<string id="DisableUpdateSettings_Name">Disable update settings</string>
<string id="DisableUpdateSettings_Help">When enabled, blocks every configuration change from the client UI and from the CLI (netbird up / login / setconfig). The Settings view stays viewable but read-only. Equivalent to --disable-update-settings.</string>
<string id="DisableProfiles_Name">Disable profiles</string>
<string id="DisableProfiles_Help">When enabled, the client UI/CLI cannot list, create, switch or remove NetBird connection profiles. Equivalent to --disable-profiles.</string>
<string id="DisableNetworks_Name">Disable networks</string>
<string id="DisableNetworks_Help">When enabled, the client UI/CLI cannot list, select or deselect NetBird networks (the corresponding daemon RPCs return Unavailable). Equivalent to --disable-networks.</string>
<string id="DisableMetricsCollection_Name">Disable metrics collection</string>
<string id="DisableMetricsCollection_Help">When enabled, the client does not collect or report local usage metrics.</string>
</stringTable>
<presentationTable>
<presentation id="ManagementURL_Pres">
<textBox refId="ManagementURL_Text">
<label>Management URL:</label>
<defaultValue>https://api.netbird.io:443</defaultValue>
</textBox>
</presentation>
<presentation id="PreSharedKey_Pres">
<textBox refId="PreSharedKey_Text">
<label>Pre-shared key:</label>
</textBox>
</presentation>
<presentation id="WireguardPort_Pres">
<decimalTextBox refId="WireguardPort_Decimal" defaultValue="51820">WireGuard UDP port:</decimalTextBox>
</presentation>
<presentation id="SplitTunnel_Pres">
<dropdownList refId="SplitTunnel_Mode" defaultItem="0">Mode:</dropdownList>
<textBox refId="SplitTunnel_Apps">
<label>Package names (comma-separated):</label>
</textBox>
</presentation>
</presentationTable>
</resources>
</policyDefinitionResources>

View File

@@ -0,0 +1,223 @@
<?xml version="1.0" encoding="utf-8"?>
<policyDefinitions xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
revision="1.0"
schemaVersion="1.0"
xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions">
<policyNamespaces>
<target prefix="netbird" namespace="NetBird.Policies.Client" />
</policyNamespaces>
<resources minRequiredRevision="1.0" />
<supportedOn>
<definitions>
<definition name="SUPPORTED_NetBird_All" displayName="$(string.SUPPORTED_NetBird_All)" />
</definitions>
</supportedOn>
<categories>
<category name="NetBird" displayName="$(string.NetBird_Category)" />
</categories>
<policies>
<!-- ============================================================ -->
<!-- TOP-LEVEL: foundational identity / authentication -->
<!-- ============================================================ -->
<policy name="ManagementURL"
class="Machine"
displayName="$(string.ManagementURL_Name)"
explainText="$(string.ManagementURL_Help)"
key="Software\Policies\NetBird"
presentation="$(presentation.ManagementURL_Pres)">
<parentCategory ref="NetBird" />
<supportedOn ref="SUPPORTED_NetBird_All" />
<elements>
<text id="ManagementURL_Text" valueName="ManagementURL" required="true" />
</elements>
</policy>
<policy name="PreSharedKey"
class="Machine"
displayName="$(string.PreSharedKey_Name)"
explainText="$(string.PreSharedKey_Help)"
key="Software\Policies\NetBird"
presentation="$(presentation.PreSharedKey_Pres)">
<parentCategory ref="NetBird" />
<supportedOn ref="SUPPORTED_NetBird_All" />
<elements>
<text id="PreSharedKey_Text" valueName="PreSharedKey" />
</elements>
</policy>
<!-- ============================================================ -->
<!-- SETTINGS: engine / runtime / connection behavior -->
<!-- ============================================================ -->
<policy name="DisableAutoConnect"
class="Machine"
displayName="$(string.DisableAutoConnect_Name)"
explainText="$(string.DisableAutoConnect_Help)"
key="Software\Policies\NetBird"
valueName="DisableAutoConnect">
<parentCategory ref="NetBird" />
<supportedOn ref="SUPPORTED_NetBird_All" />
<enabledValue><decimal value="1" /></enabledValue>
<disabledValue><decimal value="0" /></disabledValue>
</policy>
<policy name="DisableClientRoutes"
class="Machine"
displayName="$(string.DisableClientRoutes_Name)"
explainText="$(string.DisableClientRoutes_Help)"
key="Software\Policies\NetBird"
valueName="DisableClientRoutes">
<parentCategory ref="NetBird" />
<supportedOn ref="SUPPORTED_NetBird_All" />
<enabledValue><decimal value="1" /></enabledValue>
<disabledValue><decimal value="0" /></disabledValue>
</policy>
<policy name="DisableServerRoutes"
class="Machine"
displayName="$(string.DisableServerRoutes_Name)"
explainText="$(string.DisableServerRoutes_Help)"
key="Software\Policies\NetBird"
valueName="DisableServerRoutes">
<parentCategory ref="NetBird" />
<supportedOn ref="SUPPORTED_NetBird_All" />
<enabledValue><decimal value="1" /></enabledValue>
<disabledValue><decimal value="0" /></disabledValue>
</policy>
<policy name="BlockInbound"
class="Machine"
displayName="$(string.BlockInbound_Name)"
explainText="$(string.BlockInbound_Help)"
key="Software\Policies\NetBird"
valueName="BlockInbound">
<parentCategory ref="NetBird" />
<supportedOn ref="SUPPORTED_NetBird_All" />
<enabledValue><decimal value="1" /></enabledValue>
<disabledValue><decimal value="0" /></disabledValue>
</policy>
<policy name="AllowServerSSH"
class="Machine"
displayName="$(string.AllowServerSSH_Name)"
explainText="$(string.AllowServerSSH_Help)"
key="Software\Policies\NetBird"
valueName="AllowServerSSH">
<parentCategory ref="NetBird" />
<supportedOn ref="SUPPORTED_NetBird_All" />
<enabledValue><decimal value="1" /></enabledValue>
<disabledValue><decimal value="0" /></disabledValue>
</policy>
<policy name="RosenpassEnabled"
class="Machine"
displayName="$(string.RosenpassEnabled_Name)"
explainText="$(string.RosenpassEnabled_Help)"
key="Software\Policies\NetBird"
valueName="RosenpassEnabled">
<parentCategory ref="NetBird" />
<supportedOn ref="SUPPORTED_NetBird_All" />
<enabledValue><decimal value="1" /></enabledValue>
<disabledValue><decimal value="0" /></disabledValue>
</policy>
<policy name="RosenpassPermissive"
class="Machine"
displayName="$(string.RosenpassPermissive_Name)"
explainText="$(string.RosenpassPermissive_Help)"
key="Software\Policies\NetBird"
valueName="RosenpassPermissive">
<parentCategory ref="NetBird" />
<supportedOn ref="SUPPORTED_NetBird_All" />
<enabledValue><decimal value="1" /></enabledValue>
<disabledValue><decimal value="0" /></disabledValue>
</policy>
<policy name="WireguardPort"
class="Machine"
displayName="$(string.WireguardPort_Name)"
explainText="$(string.WireguardPort_Help)"
key="Software\Policies\NetBird"
presentation="$(presentation.WireguardPort_Pres)">
<parentCategory ref="NetBird" />
<supportedOn ref="SUPPORTED_NetBird_All" />
<elements>
<decimal id="WireguardPort_Decimal" valueName="WireguardPort"
minValue="1" maxValue="65535" required="true" />
</elements>
</policy>
<policy name="SplitTunnel"
class="Machine"
displayName="$(string.SplitTunnel_Name)"
explainText="$(string.SplitTunnel_Help)"
key="Software\Policies\NetBird"
presentation="$(presentation.SplitTunnel_Pres)">
<parentCategory ref="NetBird" />
<supportedOn ref="SUPPORTED_NetBird_All" />
<elements>
<enum id="SplitTunnel_Mode" valueName="SplitTunnelMode" required="true">
<item displayName="$(string.SplitTunnel_Allow)"><value><string>allow</string></value></item>
<item displayName="$(string.SplitTunnel_Disallow)"><value><string>disallow</string></value></item>
</enum>
<text id="SplitTunnel_Apps" valueName="SplitTunnelApps" required="true" />
</elements>
</policy>
<!-- ============================================================ -->
<!-- UI: visibility / UX kill switches -->
<!-- ============================================================ -->
<policy name="DisableUpdateSettings"
class="Machine"
displayName="$(string.DisableUpdateSettings_Name)"
explainText="$(string.DisableUpdateSettings_Help)"
key="Software\Policies\NetBird"
valueName="DisableUpdateSettings">
<parentCategory ref="NetBird" />
<supportedOn ref="SUPPORTED_NetBird_All" />
<enabledValue><decimal value="1" /></enabledValue>
<disabledValue><decimal value="0" /></disabledValue>
</policy>
<policy name="DisableProfiles"
class="Machine"
displayName="$(string.DisableProfiles_Name)"
explainText="$(string.DisableProfiles_Help)"
key="Software\Policies\NetBird"
valueName="DisableProfiles">
<parentCategory ref="NetBird" />
<supportedOn ref="SUPPORTED_NetBird_All" />
<enabledValue><decimal value="1" /></enabledValue>
<disabledValue><decimal value="0" /></disabledValue>
</policy>
<policy name="DisableNetworks"
class="Machine"
displayName="$(string.DisableNetworks_Name)"
explainText="$(string.DisableNetworks_Help)"
key="Software\Policies\NetBird"
valueName="DisableNetworks">
<parentCategory ref="NetBird" />
<supportedOn ref="SUPPORTED_NetBird_All" />
<enabledValue><decimal value="1" /></enabledValue>
<disabledValue><decimal value="0" /></disabledValue>
</policy>
<policy name="DisableMetricsCollection"
class="Machine"
displayName="$(string.DisableMetricsCollection_Name)"
explainText="$(string.DisableMetricsCollection_Help)"
key="Software\Policies\NetBird"
valueName="DisableMetricsCollection">
<parentCategory ref="NetBird" />
<supportedOn ref="SUPPORTED_NetBird_All" />
<enabledValue><decimal value="1" /></enabledValue>
<disabledValue><decimal value="0" /></disabledValue>
</policy>
</policies>
</policyDefinitions>

View File

@@ -766,6 +766,7 @@ export const docsNavigation = [
links: [
{ title: 'Profiles', href: '/client/profiles' },
{ title: 'Environment Variables', href: '/client/environment-variables' },
{ title: 'MDM Integration', href: '/client/mdm-integration' },
{
title: 'Settings',
isOpen: false,

View File

@@ -0,0 +1,387 @@
import { Note, Warning } from "@/components/mdx";
export const description =
"Enforce NetBird client configuration through your MDM channel (Intune, Jamf, Kandji, Mosyle, Workspace ONE, JumpCloud, Group Policy). Push management URL, kill switches, and other settings to a fleet of Windows and macOS devices.";
# MDM Integration
NetBird's client honors policies pushed by your Mobile Device Management
(MDM) channel, so an administrator can enforce configuration across a
fleet of devices instead of touching each machine. On every supported
platform the daemon reads from the **OS-native managed-configuration
store** that your MDM already writes to. No agent of ours sits between
you and the MDM provider; whatever you can push to that store (manually,
via Group Policy, via a Configuration Profile, via your MDM console)
becomes effective NetBird policy.
This page covers Windows and macOS. iOS and Android support is on the
roadmap.
## At a glance
| Platform | Where NetBird reads policy | How an admin writes it |
| --- | --- | --- |
| Windows | `HKLM\Software\Policies\NetBird` (registry) | Group Policy (ADMX) · Intune ADMX ingestion or OMA-URI · `reg import` · MDM-vendor scripts |
| macOS | `/Library/Managed Preferences/io.netbird.client.plist` | Configuration Profile (`.mobileconfig`) pushed by your MDM, targeting bundle id `io.netbird.client` |
Both backends are the de-facto convention for desktop apps (the same
shape Chrome, Edge, Firefox, Zoom, Tailscale, Citrix Workspace, and
others use). Any MDM that supports the platform also supports NetBird —
there is no NetBird-specific integration to build.
## How enforcement works
When NetBird starts, and every minute while it runs, the daemon:
1. Reads the platform-native managed-configuration store.
2. Merges the values **on top of every other configuration layer**
(defaults → on-disk profile → environment variables → CLI/UI input →
**MDM**). MDM always wins.
3. Locks any field that came from the MDM source. Attempts to change
that field from the GUI, the CLI (`netbird up --flag=...`, `netbird
login --flag=...`) or via direct gRPC are rejected with a clear
error listing the locked fields. The client UI greys these fields
out and tags them with **(MDM)** so the user knows they cannot
change them.
4. If the MDM payload changes (admin pushes new values), the change
takes effect within ~1 minute on the device — no client restart
needed.
<Note>
MDM is **authoritative**. When a key is present in the MDM payload, the
MDM value wins regardless of whether it is `true` or `false`. An admin
pushing `disableNetworks=false` via MDM re-enables the feature even on a
host that was installed with `--disable-networks`. The MDM payload is
the source of truth as long as the policy is in place. MDM-supplied
values are also never written back to the on-disk profile, so removing
the policy at the MDM side takes effect on the next reload with no
stale residue on the device.
</Note>
## Policy keys reference
The same 16 keys apply on every platform. Names are camelCase in the
managed-configuration payload; the Windows ADMX template renders the
PascalCase variant in the Group Policy Editor — both are recognized.
| Key | Type | Description |
| --- | --- | --- |
| `managementURL` | string | Override the management server URL (e.g. `https://api.netbird.io:443` or a self-hosted URL). |
| `preSharedKey` | string | WireGuard pre-shared key. Treated as secret and redacted in logs. |
| `wireguardPort` | integer | UDP port the local WireGuard interface binds to. Range `165535`. |
| `allowServerSSH` | boolean | Allow the embedded NetBird SSH server on this peer. |
| `disableAutoConnect` | boolean | Skip auto-connecting on startup; require an explicit `netbird up`. |
| `rosenpassEnabled` | boolean | Turn on the post-quantum Rosenpass key exchange. |
| `rosenpassPermissive` | boolean | Permissive mode for Rosenpass (interop with non-Rosenpass peers). |
| `blockInbound` | boolean | Drop all inbound traffic except established/related — kill-switch style. |
| `disableClientRoutes` | boolean | This peer does not route traffic to other peers. |
| `disableServerRoutes` | boolean | This peer is not a router for others. |
| `disableMetricsCollection` | boolean | Disable anonymous usage telemetry. |
| `disableUpdateSettings` | boolean | Block every configuration change from UI or CLI on this device (read-only mode). |
| `disableProfiles` | boolean | Hide the profile menu in the GUI and reject profile CRUD via CLI. |
| `disableNetworks` | boolean | Hide the Networks / Exit Node menus in the GUI and reject the related RPCs. |
| `splitTunnelMode` | string | `allow` or `disallow` — split-tunnel policy mode (Android only at the client level; harmless on desktop). |
| `splitTunnelApps` | string | Comma-separated list of package names that the split-tunnel mode applies to (Android only). |
### Notes on a few keys
- `disableUpdateSettings` and `disableProfiles` overlap with the
service-install CLI flags `--disable-update-settings` and
`--disable-profiles`. Either source can disable the feature; the MDM
value wins when present.
- `disableUpdateSettings` keeps the Settings view in the GUI visible
(so users can inspect current values) but rejects every attempt to
save changes. Use it for read-only fleets.
- `splitTunnelMode` and `splitTunnelApps` are wired into Android's
`VpnService.Builder.addAllowedApplication()` flow; on Windows and
macOS the daemon parses the keys but ignores them. They are safe to
ship in a cross-platform payload.
- The `disableMetricsCollection` key is reserved for an upcoming
metrics integration; the client recognizes it today but no metrics
pipeline is shipped yet.
## Windows
The NetBird daemon reads policies from
`HKLM\Software\Policies\NetBird`. Anything that ends up under that
registry key — through whichever delivery channel — becomes policy.
The shapes are:
| Value name | Registry type | Example |
| --- | --- | --- |
| `ManagementURL`, `PreSharedKey`, `SplitTunnelMode`, `SplitTunnelApps` | `REG_SZ` | `"https://api.netbird.io:443"` |
| All `Disable*` flags, `AllowServerSSH`, `RosenpassEnabled`, `RosenpassPermissive` | `REG_DWORD` (0 / 1) | `0x00000001` |
| `WireguardPort` | `REG_DWORD` | `0x0000ca6c` (51820 decimal) |
Choose one of the delivery channels below. All four converge on the
same registry key.
### Group Policy (on-prem AD / local gpedit)
1. Copy the ADMX/ADML files into the system Policy Definitions store:
- Place `netbird.admx` in `C:\Windows\PolicyDefinitions\`.
- Place `netbird.adml` in `C:\Windows\PolicyDefinitions\en-US\`.
2. Open `gpedit.msc` (or the AD Group Policy Management Editor).
3. Navigate to **Computer Configuration → Administrative Templates →
NetBird**.
4. Edit any policy (e.g. **Management URL**), set it to **Enabled**
with the desired value, and click **OK**.
5. Run `gpupdate /force` on each target device (or wait for the
periodic refresh).
6. Verify with `reg query HKLM\Software\Policies\NetBird` — the values
you set should appear there.
Download the templates: <a href="/docs-static/files/netbird.admx" download>netbird.admx</a> / <a href="/docs-static/files/netbird.adml" download>netbird.adml</a>.
### Microsoft Intune (ADMX ingestion)
Recommended for cloud-managed Windows fleets.
1. In the Intune admin center, go to **Devices → Configuration → Import
ADMX**, upload `netbird.admx` together with `netbird.adml`. Wait for
the **Available** status.
2. Create a new **Configuration Profile → Templates → Imported
Administrative templates → NetBird**.
3. Configure the policies you want to enforce.
4. Assign the profile to your device group(s) and save.
Devices pick up the policy on the next Intune sync (typically within
8 hours, sooner if you trigger a manual sync from the device). The
values end up in `HKLM\Software\Policies\NetBird`.
### Microsoft Intune (custom OMA-URI)
If you cannot ingest the ADMX template, you can push individual values
via OMA-URI under
`./Device/Vendor/MSFT/Policy/ConfigOperations/ADMXInstall/...` or via
the Registry CSP at
`./Device/Vendor/MSFT/Registry/HKEY_LOCAL_MACHINE/Software/Policies/NetBird/<ValueName>`.
ADMX ingestion is simpler and gives admins the same UI as on-prem GPO,
so prefer that.
### `.reg` import (single source of truth)
For fleets without an MDM, or as a quick-test path, you can carry the
whole policy in a single `.reg` file:
1. Configure the policy values on a reference machine (via `gpedit` or
`reg add`).
2. Export the key:
```
reg export "HKLM\Software\Policies\NetBird" netbird-policy.reg /y
```
3. Distribute the resulting file and apply with:
```
reg import netbird-policy.reg
```
Download a sample: <a href="/docs-static/files/netbird-policy.reg" download>netbird-policy.reg</a>.
### JumpCloud
NetBird ships a JumpCloud companion script: <a href="/docs-static/files/netbird-policy.reg.ps1" download>netbird-policy.reg.ps1</a>. To use it:
1. In the JumpCloud admin console, go to **Device Management →
Commands → +**.
2. Type: **Windows PowerShell**. Run as: **SYSTEM**.
3. Paste `netbird-policy.reg.ps1` verbatim into the command body.
4. In the same command, attach the `netbird-policy.reg` file you
produced above. JumpCloud copies attached files into the command's
working directory before invoking the script.
5. Bind the command to the target system group and run it.
The script wipes the existing `HKLM\Software\Policies\NetBird` key
before importing the `.reg`, so the `.reg` is the **single source of
truth** for that device. To unset all policy, attach an empty (header-
only) `.reg`; the daemon will pick up the absence on the next reload.
## macOS
The NetBird daemon reads policy from
`/Library/Managed Preferences/io.netbird.client.plist`. macOS writes
that file when an MDM provider pushes a Configuration Profile whose
`com.apple.ManagedClient.preferences` payload targets the bundle id
`io.netbird.client`.
<Warning>
macOS wipes the contents of `/Library/Managed Preferences/` on every
boot if the device is not MDM-enrolled. Manual `defaults write` works
for a quick test but does not survive a reboot on an un-enrolled Mac.
Use a real MDM channel for production rollouts.
</Warning>
### Custom Configuration Profile (recommended)
This is the canonical macOS path and works with every MDM
(Jamf, Kandji, Mosyle, Microsoft Intune for Mac, Workspace ONE,
JumpCloud, Apple Configurator 2, etc.).
1. Start from the template <a href="/docs-static/files/netbird-macos.mobileconfig" download>netbird-macos.mobileconfig</a>. Open it in your editor (or in
[iMazing Profile Editor](https://imazing.com/profile-editor) /
[ProfileCreator](https://github.com/ProfileCreator/ProfileCreator)).
2. Inside the `mcx_preference_settings` dictionary, set the keys you
want to enforce. Keep the bundle id `io.netbird.client` as the
preference domain.
3. Replace the placeholder `PayloadUUID` values with freshly generated
UUIDs (`uuidgen` on macOS) so each deployment has unique ids.
4. (Optional, recommended for production) sign the profile with your
organization's Developer ID Installer certificate using
`productsign` — unsigned profiles on Sonoma/Sequoia/Tahoe require
an extra user confirmation on install.
5. Upload the resulting `.mobileconfig` to your MDM as a **Custom
Configuration Profile** and scope it to the target device group.
Verify on a target device with:
```bash
sudo defaults read "/Library/Managed Preferences/io.netbird.client"
```
The output should match the keys you set in the profile.
### MDM-specific notes
- **Jamf Pro**: upload as **Computers → Configuration Profiles → New →
Application & Custom Settings → External Applications → Upload File
(Plist file)** for the preference domain `io.netbird.client`.
- **Kandji**: use the **Custom Profile** assignment library item.
- **Mosyle**: **Profiles → Add new profile → Custom Settings** with
domain `io.netbird.client`.
- **Microsoft Intune (for Mac)**: **Devices → Configuration → Create
profile → macOS → Templates → Custom**, upload the `.mobileconfig`.
- **Apple Configurator 2** (no MDM, ideal for testing on a tethered
device): drag the `.mobileconfig` onto the device in Configurator and
push.
### JumpCloud
JumpCloud supports two delivery channels for the NetBird policy on
macOS. Pick whichever fits how your fleet is enrolled.
#### MDM Custom Configuration Profile (recommended for MDM-enrolled fleets)
If your Macs are MDM-enrolled with JumpCloud, push the policy as a
managed-preferences plist:
1. In the JumpCloud admin console, open **Policy Management →
Policies → +** and choose the **Mac** platform.
2. Pick the **MDM Custom Configuration Profile** policy template.
3. Upload <a href="/docs-static/files/io.netbird.client.plist" download>io.netbird.client.plist</a>
as the plist payload. Edit the file before upload to enable just
the keys you want to enforce — leave the rest commented out.
4. Bind the policy to the target Device Group and save.
Notes:
- JumpCloud's **MDM Custom Configuration Profile** accepts a bare
managed-preferences plist (the inner Apple managed-prefs dictionary)
— **not** a full `.mobileconfig` envelope. Uploading
`netbird-macos.mobileconfig` will be rejected. Use the bare
`io.netbird.client.plist` for this code path; reserve
`netbird-macos.mobileconfig` for other MDMs that expect the full
Configuration Profile shape.
- Keep the filename as `io.netbird.client.plist`. The Apple
convention for managed-preferences plists is
`<bundle.id>.plist` (this is how macOS materializes the file at
`/Library/Managed Preferences/<bundle.id>.plist`), and JumpCloud's
policy form does not currently expose a separate bundle-identifier
field — keeping the canonical filename is the safest path. If your
JumpCloud console version surfaces a bundle-id / preference-domain
field elsewhere in the policy wizard, set it to `io.netbird.client`
too.
JumpCloud wraps the plist into an Apple Configuration Profile and
pushes it via the MDM channel. The OS materializes the file at
`/Library/Managed Preferences/io.netbird.client.plist`, where the
NetBird daemon picks it up within the next 1-minute reload tick.
Removing the policy from JumpCloud removes the file on the next sync,
which un-locks the corresponding fields on the client.
#### Shell Command (no MDM enrollment required)
If your fleet is JumpCloud-managed but not MDM-enrolled, NetBird ships
a companion script: <a href="/docs-static/files/netbird-macos.sh" download>netbird-macos.sh</a>. It is the macOS
counterpart of the Windows `.reg.ps1` script — same fleet, different
backend:
1. Edit the `### POLICY VALUES ###` block at the top of the script;
set the variables for the keys you want to enforce and leave the
rest at `$NULL`.
2. In the JumpCloud admin console, go to **Device Management →
Commands → +**. Type: **Mac, Shell**. Run as: **root**.
3. Paste the edited script verbatim into the command body.
4. Bind to the target system group and run.
The script writes
`/Library/Managed Preferences/io.netbird.client.plist`, sets ownership
to `root:wheel` with mode `644`, and kicks the NetBird daemon so the
change applies immediately. On MDM-enrolled devices the file survives
reboots; on un-enrolled devices the file is wiped at the next reboot
(macOS-imposed). Prefer the Custom Mac Application Settings policy
above when the fleet is enrolled.
## Verifying enforcement
On any platform, the cleanest verification is the daemon's own debug
dump:
```bash
netbird debug config
```
The response includes a `mDMManagedFields` array that lists every key
the daemon is currently honoring from the MDM source. If a key you
expected to be locked is missing from that array, the MDM payload did
not reach the device (or used a value name the daemon does not
recognize).
The client UI mirrors the same state: any submenu item, settings field,
or kill switch driven by MDM appears greyed out with a **(MDM)** tag
next to its label.
Daemon logs (`/var/log/netbird/client.log` on Linux/macOS,
`%ProgramData%\Netbird\` on Windows) contain a one-line
`MDM enrolled with N managed key(s): [...]` entry on every reload, plus
one `MDM override <key> = <value>` line per applied key. Secrets are
redacted.
## Troubleshooting
**The policy did not apply at all.**
Check that the daemon can see the source.
- Windows: `reg query HKLM\Software\Policies\NetBird` — if empty, the
delivery channel did not write the values. Check `gpresult /h` for
GPO failures or the Intune sync status in **Settings → Accounts →
Access work or school → Info → Sync**.
- macOS:
`sudo defaults read /Library/Managed\ Preferences/io.netbird.client`
— if the file is missing, the MDM payload was not pushed or the
bundle id in the profile does not match `io.netbird.client`.
**The key shows up in the registry / plist but not in `netbird debug
config` `mDMManagedFields`.**
The value name is misspelled. Names are case-insensitive but must match
one of the keys in the reference table above. The daemon log emits an
`MDM ignoring unknown <key>` warning when this happens.
**The user can still change the field from the GUI / CLI.**
The change is being rejected by the daemon but the UI may not have
caught up yet. The UI refreshes within a couple of seconds after a
config change; try closing and reopening the Settings window. If the
change actually sticks, double-check that the MDM payload is still
present on the device — it may have been removed by another policy.
**On macOS, the file disappears after a reboot.**
The device is not MDM-enrolled. macOS protects
`/Library/Managed Preferences/` by wiping it at boot if no MDM
controls the directory. Enroll the device with a real MDM provider for
persistent rollouts.
**My MDM provider is not in the list above.**
Any MDM that can push a Configuration Profile on macOS or write a
registry value on Windows works. The mechanism is OS-native, not
NetBird-specific. If you hit a quirk specific to your provider, please
open an issue at
https://github.com/netbirdio/netbird/issues with the provider name and
what you observed.