diff --git a/public/docs-static/files/io.netbird.client.plist b/public/docs-static/files/io.netbird.client.plist
index ed87c71d..c9d6c56b 100644
--- a/public/docs-static/files/io.netbird.client.plist
+++ b/public/docs-static/files/io.netbird.client.plist
@@ -3,9 +3,8 @@
+
+
+
+
+
+
+
+
+
+ disableAdvancedView : hide the advanced section of the UI.
+ UI-only: nothing is rejected on its
+ account.
+ disableMetricsCollection: reserved; recognized but no effect yet. -->
diff --git a/public/docs-static/files/netbird-ios-appconfig.plist b/public/docs-static/files/netbird-ios-appconfig.plist
new file mode 100644
index 00000000..b74785f1
--- /dev/null
+++ b/public/docs-static/files/netbird-ios-appconfig.plist
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+
+ managementURL
+ https://api.netbird.io:443
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/docs-static/files/netbird-macos.mobileconfig b/public/docs-static/files/netbird-macos.mobileconfig
index d5b54e73..71bd82cc 100644
--- a/public/docs-static/files/netbird-macos.mobileconfig
+++ b/public/docs-static/files/netbird-macos.mobileconfig
@@ -16,11 +16,22 @@
(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.
+ - delivered through an MDM, the profile is signed by the MDM
+ channel itself; nothing to sign by hand,
+ - handed out any other way (Apple Configurator, double-click), it
+ installs unsigned and asks the user to confirm on recent macOS.
+ To avoid that, sign it as a CMS message with an identity the
+ target Macs trust:
+ security cms -S -N "Your Signing Identity" \
+ -i netbird-macos.mobileconfig -o signed.mobileconfig
+ Configuration profiles are not signed with productsign, which
+ handles installer packages.
+ - For a local test: double-click the file, then install it from
+ System Settings > General > Device Management. macOS 11 and later
+ cannot install configuration profiles with the `profiles` command.
+ - For MDM (Kandji/Mosyle/Intune/Workspace ONE/JumpCloud): upload as
+ a custom profile. Jamf Pro: prefer io.netbird.client.plist in
+ Application & Custom Settings, or sign this profile before upload.
Editing:
- Replace UUID placeholders below with fresh UUIDs (`uuidgen` on
@@ -121,6 +132,15 @@
-->
+
+
+
+
+
+
+
+
+
@@ -138,7 +179,9 @@
com.acme.app1,com.acme.app2
-->
-
+
diff --git a/public/docs-static/files/netbird-macos.sh b/public/docs-static/files/netbird-macos.sh
index d22ea45f..aca8d51b 100644
--- a/public/docs-static/files/netbird-macos.sh
+++ b/public/docs-static/files/netbird-macos.sh
@@ -53,9 +53,9 @@ set -euo pipefail
# docs/netbird.admx + .adml (Windows ADMX schema)
#
NULL='__UNSET__'
-managementURL='https://api.netbird.io:443'
+managementURL="$NULL"
preSharedKey="$NULL" # secret; redacted in log
-allowServerSSH='true'
+allowServerSSH="$NULL"
blockInbound="$NULL"
disableAutoConnect="$NULL"
disableAutostart="$NULL"
@@ -65,9 +65,15 @@ disableMetricsCollection="$NULL"
disableUpdateSettings="$NULL"
disableProfiles="$NULL"
disableNetworks="$NULL"
+disableAdvancedView="$NULL" # UI-only: hides the advanced section, rejects nothing
rosenpassEnabled="$NULL"
rosenpassPermissive="$NULL"
-wireguardPort='51820'
+lazyConnection="$NULL" # "true"/"false"; absent defers to the Management setting
+wireguardPort="$NULL"
+allowRemoteJobs="$NULL" # present at any value locks the client toggle
+debugBundleUploadURL="$NULL" # https URL with a host
+enableLocalMetrics="$NULL" # local Prometheus endpoint; unrelated to disableMetricsCollection
+localMetricsAddress="$NULL" # default 127.0.0.1:9191
splitTunnelMode="$NULL" # "allow" or "disallow", Android-only at the daemon level
splitTunnelApps="$NULL" # comma-separated app IDs, Android-only
##############################################################################
@@ -175,9 +181,15 @@ main() {
is_set "$disableUpdateSettings" && emit_bool disableUpdateSettings "$disableUpdateSettings"
is_set "$disableProfiles" && emit_bool disableProfiles "$disableProfiles"
is_set "$disableNetworks" && emit_bool disableNetworks "$disableNetworks"
+ is_set "$disableAdvancedView" && emit_bool disableAdvancedView "$disableAdvancedView"
is_set "$rosenpassEnabled" && emit_bool rosenpassEnabled "$rosenpassEnabled"
is_set "$rosenpassPermissive" && emit_bool rosenpassPermissive "$rosenpassPermissive"
+ is_set "$lazyConnection" && emit_bool lazyConnection "$lazyConnection"
is_set "$wireguardPort" && emit_int wireguardPort "$wireguardPort"
+ is_set "$allowRemoteJobs" && emit_bool allowRemoteJobs "$allowRemoteJobs"
+ is_set "$debugBundleUploadURL" && emit_string debugBundleUploadURL "$debugBundleUploadURL"
+ is_set "$enableLocalMetrics" && emit_bool enableLocalMetrics "$enableLocalMetrics"
+ is_set "$localMetricsAddress" && emit_string localMetricsAddress "$localMetricsAddress"
is_set "$splitTunnelMode" && emit_split_tunnel_mode "$splitTunnelMode"
is_set "$splitTunnelApps" && emit_string splitTunnelApps "$splitTunnelApps"
diff --git a/public/docs-static/files/netbird-policy.reg b/public/docs-static/files/netbird-policy.reg
index 074e42d4..bd8bd52c 100644
Binary files a/public/docs-static/files/netbird-policy.reg and b/public/docs-static/files/netbird-policy.reg differ
diff --git a/public/docs-static/files/netbird.adml b/public/docs-static/files/netbird.adml
index 8a62ff04..dd9a85ad 100644
--- a/public/docs-static/files/netbird.adml
+++ b/public/docs-static/files/netbird.adml
@@ -64,7 +64,25 @@
When enabled, the client UI/CLI cannot list, select or deselect NetBird networks (the corresponding daemon RPCs return Unavailable). Equivalent to --disable-networks.Disable metrics collection
- When enabled, the client does not collect or report local usage metrics.
+ Reserved for a future client telemetry feature. The client recognizes this setting but it has no effect in current releases.
+
+ Disable advanced view
+ When enabled, the client hides the advanced section of its interface. This policy only changes what the interface offers: it is not part of the daemon configuration and never causes a configuration change to be rejected. Setting it to Disabled explicitly re-enables the section.
+
+ Lazy connections
+ Local override for lazy connections. Enabled forces lazy connections on, Disabled forces them off, and leaving the policy Not Configured defers to the Management setting. The NB_LAZY_CONN environment variable takes precedence over this policy.
+
+ Allow remote jobs
+ Allows management-requested remote jobs, such as debug bundle requests, to run on this peer. Off by default; equivalent to --allow-remote-jobs. Configuring this policy at either value locks the corresponding client toggle, so Disabled pins remote jobs off.
+
+ Debug bundle upload URL
+ Overrides the upload service used for debug bundles produced by remote jobs, taking precedence over the value requested by Management. Must be an https URL including a host.
+
+ Enable local metrics endpoint
+ Exposes the client's local Prometheus /metrics endpoint. Unrelated to "Disable metrics collection".
+
+ Local metrics listen address
+ Listen address of the local Prometheus /metrics endpoint. Defaults to 127.0.0.1:9191 when the endpoint is enabled and this policy is Not Configured.
@@ -93,6 +111,19 @@
+
+
+
+
+
+
+
+
+
+ 127.0.0.1:9191
+
+
+
diff --git a/public/docs-static/files/netbird.admx b/public/docs-static/files/netbird.admx
index ddeacc17..71c9ec1e 100644
--- a/public/docs-static/files/netbird.admx
+++ b/public/docs-static/files/netbird.admx
@@ -231,5 +231,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/NavigationDocs.jsx b/src/components/NavigationDocs.jsx
index aabb2bdb..81a8330c 100644
--- a/src/components/NavigationDocs.jsx
+++ b/src/components/NavigationDocs.jsx
@@ -110,6 +110,22 @@ export const docsNavigation = [
title: 'MDM Deployment',
isOpen: false,
links: [
+ {
+ title: 'MDM Integration',
+ href: '/client/mdm-integration',
+ },
+ {
+ title: 'Enforce Settings on Windows',
+ href: '/manage/peers/mdm-deployment/windows-mdm-policy',
+ },
+ {
+ title: 'Enforce Settings on macOS',
+ href: '/manage/peers/mdm-deployment/macos-mdm-policy',
+ },
+ {
+ title: 'Enforce Settings on iOS',
+ href: '/manage/peers/mdm-deployment/ios-mdm-policy',
+ },
{
title: 'Deploy with Group Policy (GPO)',
href: '/manage/peers/mdm-deployment/windows-gpo-deployment',
diff --git a/src/pages/client/mdm-integration.mdx b/src/pages/client/mdm-integration.mdx
index 21500615..6ee6aa93 100644
--- a/src/pages/client/mdm-integration.mdx
+++ b/src/pages/client/mdm-integration.mdx
@@ -1,411 +1,338 @@
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.";
+ "Enforce NetBird client settings on a fleet of Windows, macOS, and iOS devices through your MDM (Intune, Jamf, Kandji, Mosyle, Workspace ONE, JumpCloud, Group Policy): how a policy is applied and locked, and what every policy key does, grouped by the job it solves.";
# 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 extra NetBird software 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.
+Installing NetBird on a few hundred laptops is the easy half of a rollout. The hard half is everything that happens afterwards: a user points the client at a different server, creates a second profile, turns on an SSH server, or switches off a setting your security posture depends on. Fixing that one machine at a time does not scale, and asking users nicely is not a control.
-This page covers Windows and macOS. iOS and Android support is on the
-roadmap.
+The NetBird client solves this by honoring **MDM policy**: settings your device management tool writes to the operating system's own managed-configuration store. The client reads that store, applies what it finds on top of every other setting, and locks those settings so nobody on the device can change them. NetBird adds no agent or connector of its own. If your MDM can write to the store, it can manage NetBird.
-## At a glance
+This page is in four parts:
-| Platform | Where NetBird reads policy | How an admin writes it |
+1. [What you can achieve](#what-you-can-achieve): the goals you can reach with a policy, and the setting for each.
+2. [Recommended policies](#recommended-policies): a starting policy for user devices, shared devices, and routing peers.
+3. [How the client applies a policy](#how-the-client-applies-a-policy): the mental model, and what a user sees on a managed device.
+4. [Policy keys reference](#policy-keys-reference), then [verifying enforcement](#verifying-enforcement) and [troubleshooting](#troubleshooting).
+
+How to deliver a policy is specific to each operating system, so it has its own page:
+
+| Platform | Where NetBird reads policy | How to deliver 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` |
+| Windows | Registry, `HKLM\Software\Policies\NetBird` | [Enforce settings on Windows](/manage/peers/mdm-deployment/windows-mdm-policy): Intune, Group Policy, `.reg` import, JumpCloud |
+| macOS | `/Library/Managed Preferences/io.netbird.client.plist` | [Enforce settings on macOS](/manage/peers/mdm-deployment/macos-mdm-policy): a configuration profile for bundle id `io.netbird.client` |
+| iOS and tvOS | Managed App Configuration for bundle id `io.netbird.app` (tvOS: `io.netbird.app.tv`) | [Enforce settings on iOS](/manage/peers/mdm-deployment/ios-mdm-policy): an app configuration sent with the managed app |
-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.
+Linux and Android have no MDM channel today. See [When not to use MDM policy](#when-not-to-use-mdm-policy).
-## How enforcement works
+## What you can achieve
-When NetBird starts, and every minute while it runs, the daemon:
+Find what you want to do, set the key, and deliver it with your MDM (see the platform pages above). Each key links to its full entry in the [Policy keys reference](#policy-keys-reference).
-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.
+Include only the keys you mean to pin. A key you set is locked, even when you set it to `false`; a key you leave out stays the user's to change.
+
+### Set the server and startup
+
+| You want to | Set | What to know |
+| --- | --- | --- |
+| Make every device use your management server | [`managementURL`](#managementURL) to your server's URL | Pins the server, not the account: users can still sign out and in to another account on that server. |
+| Make devices connect automatically at boot | [`disableAutoConnect`](#disableAutoConnect) to `false` | Users can still disconnect by hand. |
+| Make users connect by hand | [`disableAutoConnect`](#disableAutoConnect) to `true` | The client starts disconnected; the user clicks **Connect** or runs `netbird up`. |
+| Stop the NetBird app opening at login | [`disableAutostart`](#disableAutostart) to `true` | The background service still runs. A user can switch launch-at-login back on, but the app turns it off again the next time it starts. |
+
+### Lock the client app
+
+| You want to | Set | What users can still do |
+| --- | --- | --- |
+| Stop users changing NetBird's settings | [`disableUpdateSettings`](#disableUpdateSettings) to `true` | Connect, disconnect, and sign in. Every settings change, in the app or on the command line, is refused. |
+| Stop users adding a second profile, for example to reach a personal NetBird account | [`disableProfiles`](#disableProfiles) to `true` | Use their current profile as normal. They can still sign out and sign in to a different account; see the [`managementURL`](#managementURL) note. |
+| Make sure users keep the networks and exit node you assign | [`disableNetworks`](#disableNetworks) to `true` | Use the assigned networks and exit node. They cannot turn them off. To send all internet traffic through an exit node, also enable **Auto Apply** on it in the dashboard. |
+| Give non-technical users a simpler app | [`disableAdvancedView`](#disableAdvancedView) to `true` | Everything except the **Peers** and **Resources** lists. |
+| Lock the app down completely, for example on shared or kiosk devices | all four to `true` | Connect and disconnect. |
+
+None of these settings stops a user from disconnecting NetBird. See [What a policy does not do](#what-a-policy-does-not-do).
+
+### Control what the device exposes
+
+| You want to | Set | What to know |
+| --- | --- | --- |
+| Stop other peers opening connections to a device | [`blockInbound`](#blockInbound) to `true` | Outbound connections still work. It also stops the device's SSH server and its routing. Never use it on a routing peer. |
+| Make sure no NetBird SSH server runs on a device | [`allowServerSSH`](#allowServerSSH) to `false` | Wins even when SSH is enabled for the peer in the dashboard. |
+| Allow the NetBird SSH server on a device | [`allowServerSSH`](#allowServerSSH) to `true` | SSH must also be enabled for the peer in the dashboard, and `blockInbound` must be off. |
+| Stop a laptop from carrying other peers' traffic | [`disableServerRoutes`](#disableServerRoutes) to `true` | Protects against a laptop wrongly assigned as a routing peer. Never use it on a real routing peer. |
+| Stop a device using routed networks and exit nodes | [`disableClientRoutes`](#disableClientRoutes) to `true` | Cuts the device off from every network resource and exit node, so it is rarely what you want on a laptop. |
+
+### Encrypt the tunnel
+
+| You want to | Set | What to know |
+| --- | --- | --- |
+| Add a shared secret to every connection | [`preSharedKey`](#preSharedKey) | Every peer, including routing peers and servers, needs the same key, or they cannot connect to each other. Local users can read the key. |
+| Add post-quantum protection | [`rosenpassEnabled`](#rosenpassEnabled) and [`rosenpassPermissive`](#rosenpassPermissive) to `true` | Permissive mode lets peers without Rosenpass still connect. Remove it once every peer runs Rosenpass. |
+
+### Support and monitoring
+
+| You want to | Set | What to know |
+| --- | --- | --- |
+| Let support collect debug bundles remotely | [`allowRemoteJobs`](#allowRemoteJobs) to `true`, and [`debugBundleUploadURL`](#debugBundleUploadURL) to your upload server | Bundles hold peer names, IP addresses, and logs; the upload URL keeps them on infrastructure you control. |
+| Make sure remote jobs never run on a device | [`allowRemoteJobs`](#allowRemoteJobs) to `false` | Remote jobs are off by default; pinning `false` stops anyone turning them on. |
+| Scrape connection state with Prometheus | [`enableLocalMetrics`](#enableLocalMetrics) to `true` | Listens on `127.0.0.1:9191` by default. Keep `localMetricsAddress` on loopback: the endpoint has no authentication. |
+
+### Tune the connection
+
+| You want to | Set | What to know |
+| --- | --- | --- |
+| Keep every connection open on a device | [`lazyConnection`](#lazyConnection) to `false` | Overrides the account setting for that device. `NB_LAZY_CONN` on the device beats this key. |
+| Force lazy connections on a device | [`lazyConnection`](#lazyConnection) to `true` | Same precedence as above. |
+| Use a fixed WireGuard port | [`wireguardPort`](#wireguardPort) | Only when another program needs the default port or your firewall rules expect a specific one. |
+
+## Recommended policies
+
+Start from the column that matches the device group, then add or remove keys for your own posture. Send each policy to one device group, and keep routing peers out of every group that gets `blockInbound` or `disableServerRoutes`.
+
+| Key | User devices | Shared or kiosk devices | Routing peers and servers |
+| --- | --- | --- | --- |
+| `managementURL` | ✓ | ✓ | ✓ |
+| `disableUpdateSettings` | ✓ | ✓ | ✓ |
+| `disableProfiles` | ✓ | ✓ | ✓ |
+| `disableServerRoutes` | ✓ | ✓ | never |
+| `blockInbound` | optional | ✓ | never |
+| `allowRemoteJobs` and `debugBundleUploadURL` | optional | optional | optional |
+| `disableNetworks` | optional | ✓ | |
+| `disableAdvancedView` | optional | ✓ | |
+| `disableAutoConnect: false` (always connect at boot) | | ✓ | ✓ |
+
+Why each column looks the way it does:
+
+- **User devices**, such as laptops and desktops, consume resources and never serve them, so `disableServerRoutes` stops one that is wrongly assigned as a routing peer from carrying other peers' traffic. Add `blockInbound` if nothing ever needs to reach these devices. Leave it out if IT reaches them over [NetBird SSH](/manage/peers/ssh), and control inbound access with access policies instead. Add `disableNetworks` to stop users choosing networks and exit nodes (the ones the dashboard assigns keep working), and `disableAdvancedView` to hide the **Peers** and **Resources** lists.
+- **Shared or kiosk devices** should leave the user as little as possible to change: no network or exit node choice, no advanced view, and a connection at every boot.
+- **Routing peers and servers** must keep forwarding traffic and accepting connections, so they never get `blockInbound` or `disableServerRoutes`. This column only applies to Windows and macOS machines; Linux has no MDM channel, so configure Linux routing peers with [CLI flags](#when-not-to-use-mdm-policy).
+- **Remote jobs** are optional in every column. Turn on `allowRemoteJobs` wherever your support team collects [debug bundles](/manage/peers/remote-jobs) remotely, and always pin `debugBundleUploadURL` with it, so bundles go to an upload server you control. Routing peers are often the first place to look when a resource is unreachable or relayed.
+
+Leave these keys out unless you have a specific reason:
+
+- `preSharedKey`, `rosenpassEnabled`, `rosenpassPermissive`: each has to match on every peer, or devices lose their connections. See [Encryption keys](#encryption-keys).
+- `disableClientRoutes`: cuts a device off from every network resource and exit node.
+- `wireguardPort`: the default suits almost every fleet.
+- `lazyConnection`: leave lazy connections to the account setting, so one dashboard switch controls the whole fleet. Push `false` only for devices that must hold every connection open.
+- `enableLocalMetrics`, `localMetricsAddress`: only for devices you scrape with Prometheus.
+- `splitTunnelMode`, `splitTunnelApps`, `disableMetricsCollection`: no effect yet.
+
+### The user device baseline
+
+The three keys every column above shares make the usual starting policy, and the platform pages use it as their example. With a self-hosted management server:
+
+| Key | Value | What it does |
+| --- | --- | --- |
+| `managementURL` | `https://netbird.example.com:443` | Every device talks to your server and nothing else |
+| `disableUpdateSettings` | `true` | Users can connect and disconnect, but cannot change settings |
+| `disableProfiles` | `true` | Users cannot add a second profile |
+
+On NetBird Cloud, use `https://api.netbird.io:443`. Send the policy only to the device group that holds your user devices, so routing peers and servers stay out of it.
+
+## How the client applies a policy
+
+Think of a policy as **a set of pinned switches**. Every key in the payload pins one setting to the value you chose and takes it out of the user's reach. Every key you leave out stays an ordinary setting that the user, the CLI, or the dashboard controls as usual. **A key that is absent is not pinned, and a key that is present is pinned, whatever its value.**
-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.
+**Pushing `false` is not the same as leaving a key out.** `allowServerSSH: false` pins the SSH server off: no user, CLI flag, or dashboard setting can turn it on while the policy stands. Leaving `allowServerSSH` out of the payload leaves the SSH server to whoever controls the device. Only include the keys you mean to pin.
+On Windows and macOS, the client daemon reads the policy at startup and again every minute. Each time:
+
+1. **It applies the policy on top of every other configuration layer.** Defaults, the on-disk profile, environment variables, and CLI or app input all apply first; the policy is applied last and wins. A policy also overrides the service-install flags `--disable-update-settings`, `--disable-profiles`, and `--disable-networks`: pushing `disableNetworks: false` re-enables network selection even on a device installed with `--disable-networks`. One exception: the `NB_LAZY_CONN` environment variable takes precedence over the `lazyConnection` key.
+2. **It locks the pinned settings.** A request that tries to change a pinned setting to a different value, from the desktop app, the CLI (`netbird login --flag`, or `netbird up --flag` on a disconnected client) or the daemon API, is rejected as a whole with `fields managed by MDM cannot be modified: []`. None of its other fields are applied either. A request that repeats the pinned value is accepted, because it changes nothing. These locks read the policy file on every request, so they apply as soon as the file changes; the `disableUpdateSettings`, `disableProfiles`, and `disableNetworks` gates apply at the next reload. Two keys only change what the app shows or does, and lock nothing: [`disableAutostart`](#disableAutostart) and [`disableAdvancedView`](#disableAdvancedView).
+3. **It never writes policy values to disk.** The on-disk profile keeps the device's own settings. When you remove a key from the policy, the setting falls back to that on-disk value on the next reload and becomes editable again, with nothing left behind.
+
+When a reload finds that the policy has changed, the client restarts its connection to apply it. Expect a brief interruption on every policy change (typically a second or two), and plan larger changes outside working hours. The desktop app then shows a notification: **NetBird settings updated**, "Your NetBird configuration was updated by your IT policy."
+
+On iOS the timing is different: the app passes the policy to the tunnel only while the app is running. See [Enforce settings on iOS](/manage/peers/mdm-deployment/ios-mdm-policy#when-a-policy-takes-effect).
+
+### What a user sees
+
+Since NetBird v0.75.0, the desktop app **hides** the controls for pinned settings rather than greying them out. With the user device baseline, the server address field is gone. Read-only mode removes the **Network**, **Security**, **SSH**, and **Advanced** settings tabs, and `disableProfiles` removes the **Profiles** tab. **General**, **Troubleshooting**, and **About** remain, and the **Connect** button still works.
+
+On the CLI, changing a pinned setting fails with the error above. The [`disableUpdateSettings`](#disableUpdateSettings), [`disableProfiles`](#disableProfiles), and [`disableNetworks`](#disableNetworks) keys have errors of their own, listed under [Troubleshooting](#troubleshooting).
+
+### What a policy does not do
+
+A policy pins the client's configuration. It does not make the tunnel mandatory: a user can still disconnect with `netbird down` or the **Disconnect** button, no key blocks `netbird logout` of the active profile (which removes the peer from your account), and anyone with administrator rights on the device can stop the service, uninstall the client, or edit the registry or the managed-preferences file. NetBird has no always-on mode. Enforce that layer with the operating system: give users standard accounts, manage the NetBird service with your MDM, and write your [access policies](/manage/access-control) so that disconnecting from NetBird costs a user access to company resources rather than freeing them from restrictions.
+
## Policy keys reference
-The same 20 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.
+The client recognizes 23 keys. They are grouped below by the job they do, starting with the ones nearly every rollout needs. A key that has no meaning on a platform (for example `allowServerSSH` on iOS) is ignored there, so one payload is safe to send to every platform.
-| Key | Type | Description |
+Key names are camelCase in plist and app-configuration payloads. On Windows, the registry value names and the Group Policy templates use PascalCase (`ManagementURL`, `BlockInbound`); both forms are recognized, and names are matched without regard to case. In the Group Policy editor, each policy is named after its key in plain words, such as **Block inbound** for `blockInbound`.
+
+Value types:
+
+- **boolean**: a native boolean (`` or `` in a plist), or one of the strings `true`/`false`, `1`/`0`, `yes`/`no`, `on`/`off`. An integer (`0` is false, anything else is true) also works on Windows (`REG_DWORD`) and iOS, but **not on macOS**: there, a boolean written as `` is listed as managed and locked, but never applied, so the setting is frozen at whatever the device had.
+- **string**: a string. An empty string counts as not set.
+- **integer**: an integer, or a string of decimal digits.
+
+The registry type for each key on Windows is on [Enforce settings on Windows](/manage/peers/mdm-deployment/windows-mdm-policy#value-types).
+
+### Server and startup keys
+
+Nearly every rollout sets `managementURL`. The other two keys decide whether NetBird connects, and whether its app opens, on their own.
+
+| Key | Type | What it does |
| --- | --- | --- |
-| `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 `1–65535`. |
-| `allowServerSSH` | boolean | Allow the embedded NetBird SSH server on this peer. |
-| `allowRemoteJobs` | boolean | Allow management-requested remote jobs (e.g. debug bundles) on this peer. Off by default; equivalent to `--allow-remote-jobs`. |
-| `debugBundleUploadURL` | string | Override the upload service used for debug bundles produced by remote jobs, taking precedence over the value requested by Management. Must be an `https` URL with a host. |
-| `disableAutoConnect` | boolean | Skip auto-connecting on startup; require an explicit `netbird up`. |
-| `disableAutostart` | boolean | Prevent the GUI from registering itself as an OS autostart entry on fresh installs, and — when enabled at any later point — remove an existing registration on the next GUI launch (Windows Registry `Run` key, macOS Login Item, Linux `.desktop`). Desktop GUIs only; no-op on iOS/Android. Once the admin lifts the policy, the setting stays off until the user re-enables it in Settings. |
-| `lazyConnection` | boolean | Local override for lazy connections. `true` forces lazy connections on, `false` forces them off, and an absent key defers to the Management setting. `NB_LAZY_CONN` takes precedence when both are configured. |
-| `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. |
-| `enableLocalMetrics` | boolean | Expose the client's [local Prometheus `/metrics` endpoint](/client/local-metrics). |
-| `localMetricsAddress` | string | Listen address of the local `/metrics` endpoint (default `127.0.0.1:9191`). |
-| `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). |
+| `managementURL` | string | The management server the client connects to, for example `https://api.netbird.io:443` for NetBird Cloud or `https://netbird.example.com:443` for a self-hosted server. Must start with `http://` or `https://`; a URL without a port gets `:443` (or `:80`) added. An invalid URL is logged and ignored, and the client keeps its previous server. |
+| `disableAutoConnect` | boolean | `true`: the client does not connect when its service starts, so the user has to click **Connect** or run `netbird up`. `false`: pins [Connect on Startup](/client/connect-on-startup) on, so the client always connects at boot. |
+| `disableAutostart` | boolean | `true`: the desktop app does not open itself at login. The app removes its login entry (the Windows `Run` registry key or the macOS Login Item) every time it starts, and never creates one on a fresh install. The background service is not affected. Desktop app only. |
-### Notes on a few keys
+- **`managementURL` pins the server, not the account.** NetBird Cloud serves every account from `https://api.netbird.io:443`, so on Cloud this key stops a user from pointing the client at another server, but not from signing in to another Cloud account. No key pins the account: even with `managementURL`, `disableProfiles`, and `disableUpdateSettings` all pinned, `netbird logout` followed by `netbird up --setup-key ` moves the device to another account on the same server. A device signed in to another account has no access to your resources, because your access policies do not apply to it.
+- **`disableAutostart` only acts when the app starts.** The **Launch NetBird UI at Login** switch in the app's settings stays usable. A user who turns it back on gets the app at their next login, and the app removes the entry again as soon as it opens. Pushing `false` does not turn launch-at-login on; it only stops the app from removing the entry.
-- `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.
-- `disableNetworks` combined with an exit node that has Auto Apply
- enabled pins a device to that exit node: the exit node applies
- automatically and the daemon rejects every attempt to deselect it.
- See [Enforcing the Exit Node on Managed Devices](/use-cases/remote-access/exit-nodes#enforcing-the-exit-node-on-managed-devices)
- for the full recipe, including why the policy must be deployed
- before users touch the exit node switch.
-- `localMetricsAddress` is applied as pushed. Users are held to a
- loopback address unless they are root, because the endpoint is
- unauthenticated, but a policy already comes from an administrator and
- is not subject to that check. A non-loopback address publishes peer
- names and connectivity state to anything that can reach the port.
-- `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.
-- `disableAutostart` only affects the desktop GUI's OS autostart entry
- (Windows Registry `Run` key, macOS Login Item, Linux `.desktop`).
- iOS and Android have no equivalent user-space autostart mechanism —
- the daemon runs as a system service — so the key is parsed and
- ignored there. Safe to ship in cross-platform payloads.
-- The `disableMetricsCollection` key is reserved for an upcoming
- metrics integration; the client recognizes it today but no metrics
- pipeline is shipped yet.
+### Client app keys
-## Windows
+These keys decide how much of the app a user can touch. The user device baseline uses the first two.
-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 |
+| Key | Type | What it does |
| --- | --- | --- |
-| `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) |
+| `disableUpdateSettings` | boolean | Read-only mode. Every request that would change a setting is rejected, from the app or the CLI (`netbird login --flag`, or `netbird up --flag` on a disconnected client). Connecting, disconnecting, and signing in again still work, because they change no settings. |
+| `disableProfiles` | boolean | Hides profile management in the app and rejects creating, renaming, removing, or switching [profiles](/client/profiles). The device stays on its current profile. |
+| `disableNetworks` | boolean | Stops the user changing which networks and exit node the device uses. Networks and exit nodes **keep working**: the device still reaches the resources and exit node the dashboard assigns to it. Only the choice is taken away. The app hides the exit node switcher and the **Resources** tab, and `netbird networks list`, `select`, and `deselect` are rejected. The device keeps whatever selection it had when the policy arrived. |
+| `disableAdvancedView` | boolean | Removes **Advanced View** from the desktop app's menu, along with its **Peers** and **Resources** tabs: the list of peers this device can see, with each connection's details, and the list of network resources. A user already in that view is switched back to the simple view. On iOS it hides the Advanced section. It only changes what the app shows: no setting is locked, and no request is rejected because of it. |
-Choose one of the delivery channels below. All four converge on the
-same registry key.
+- **`disableUpdateSettings` covers settings no key names.** The DNS, firewall, interface, and SSH sub-options have no key of their own, so the only way to stop users changing them is read-only mode. On a device with `disableUpdateSettings: true`, an administrator who needs to change one of those settings has to lift the policy first.
+- **`disableNetworks` plus an exit node pins the device to that exit node.** With an exit node that has **Auto Apply** enabled, the exit node is applied automatically and the user cannot deselect it. See [Enforcing the exit node on managed devices](/use-cases/remote-access/exit-nodes#enforcing-the-exit-node-on-managed-devices) for the full recipe, including why the policy has to reach the devices before users touch the exit node switch.
-### Group Policy (on-prem AD / local gpedit)
+### Device exposure keys
-For a full end-to-end walkthrough — domain Central Store, GPO creation, silent MSI install, and troubleshooting — see [Deploying NetBird with Group Policy (GPO)](/manage/peers/mdm-deployment/windows-gpo-deployment). The steps below cover the minimal local setup.
+These keys decide what reaches the device, and what the device passes on. Each pins a client-side setting that also exists in the app and the CLI, and each one wins over what the dashboard configures: routes, routing-peer assignments, SSH, and access policies.
-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.
+| Key | Type | What it does |
+| --- | --- | --- |
+| `blockInbound` | boolean | `true`: drops every inbound connection from other peers, whatever your access policies allow. It also stops the SSH server and stops the device acting as a routing peer. Outbound connections from the device are not affected. See [Block Inbound Connections](/client/block-inbound-connections). |
+| `allowServerSSH` | boolean | `false`: the [NetBird SSH server](/manage/peers/ssh) never runs on this device, even if SSH is enabled for the peer in the dashboard. `true`: lets it run, but only once SSH is also enabled for the peer in the dashboard and `blockInbound` is off. |
+| `disableServerRoutes` | boolean | `true`: the device never acts as a [routing peer](/manage/networks/how-routing-peers-work). It does not forward traffic to the networks, resources, or exit node it is assigned to serve. |
+| `disableClientRoutes` | boolean | `true`: the device ignores the routes it receives. It does not install routes to network resources reached through a routing peer, and it cannot use an exit node. It can still reach other peers directly. |
-Download the templates: netbird.admx / netbird.adml.
+- **The two route keys are easy to mix up.** "Client" and "server" describe the device's role in a route, not the direction of the traffic. `disableClientRoutes` stops the device *using* routes, which is what an end-user laptop does. `disableServerRoutes` stops the device *providing* routes, which is what a routing peer does. On a laptop, `disableClientRoutes: true` cuts it off from every network resource and exit node, which is rarely what you want. `disableServerRoutes: true` is the safe choice for laptops, because it stops a laptop that has been wrongly assigned as a routing peer from carrying other peers' traffic.
+- **Do not send `blockInbound` or `disableServerRoutes` to routing peers.** A routing peer with either key set stops forwarding. If it is the only routing peer for a network, that network becomes unreachable for everyone. Scope these keys to a device group that excludes routing peers and servers.
+- **`blockInbound` is not a kill switch.** It blocks connections *to* the device, not traffic *from* it, and it does not keep the tunnel up. Use it for devices that only ever consume resources, such as most end-user laptops, and never for a device other peers need to reach.
-### Microsoft Intune (ADMX ingestion)
+### Encryption keys
-Recommended for cloud-managed Windows fleets.
+These keys change how connections are encrypted. A pre-shared key, and Rosenpass outside permissive mode, have to match on both ends of a connection, so a key sent to only part of a fleet can cut that part off from the rest.
-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.
+| Key | Type | What it does |
+| --- | --- | --- |
+| `preSharedKey` | string | A WireGuard pre-shared key mixed into every connection this device makes. Only peers configured with the **same** key can connect to each other. Redacted in logs, and never returned by the client API. |
+| `rosenpassEnabled` | boolean | Turns on [Rosenpass](/client/post-quantum-cryptography), a post-quantum key exchange that rotates a fresh pre-shared key every two minutes. Experimental. A device with Rosenpass on cannot connect to a peer without it unless `rosenpassPermissive` is also on. |
+| `rosenpassPermissive` | boolean | With Rosenpass on, lets this device fall back to a standard WireGuard connection with a peer that does not run Rosenpass, and keeps using Rosenpass with the peers that do. |
-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`.
+- **A pre-shared key splits your network in two.** Devices that have the key and devices that do not cannot connect to each other. If you pushed a `preSharedKey` to your laptops only, they would lose every routing peer and server that does not carry the same key. Only use it when every peer, including routing peers and servers, gets the same key.
+- **The pre-shared key is not hidden from local users.** By default, standard users can read the NetBird policy key on Windows, and the macOS managed-preferences file is readable by every user. Treat the key as an extra layer of protection, never as a credential that only administrators know.
+- **To roll out Rosenpass, start in permissive mode.** Push `rosenpassEnabled: true` with `rosenpassPermissive: true`, so devices that do not have Rosenpass yet can still connect. Remove permissive mode once every peer runs Rosenpass.
-### Microsoft Intune (custom OMA-URI)
+### Support and monitoring keys
-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/`.
-ADMX ingestion is simpler and gives admins the same UI as on-prem GPO,
-so prefer that.
+These keys turn on remote diagnostics and local metrics. Both features expose information about the device, so the safe default is to leave them off and pin them on only where you need them.
-### `.reg` import (single source of truth)
+| Key | Type | What it does |
+| --- | --- | --- |
+| `allowRemoteJobs` | boolean | `true`: lets the management server run [remote jobs](/manage/peers/remote-jobs) on this device, such as collecting a debug bundle. Off by default. `false`: pins remote jobs off, so a user cannot turn them on. |
+| `debugBundleUploadURL` | string | Where debug bundles produced by remote jobs are uploaded, overriding the location the management server asks for. Must be an `https` URL with a host; any other value is ignored. Treated as a secret and never logged, because the URL can carry credentials or a signed token. |
+| `enableLocalMetrics` | boolean | Turns on the client's [local Prometheus `/metrics` endpoint](/client/local-metrics), which reports peer connection state, latency, and whether each connection is direct or relayed. |
+| `localMetricsAddress` | string | The address the `/metrics` endpoint listens on. Default `127.0.0.1:9191`. |
-For fleets without an MDM, or as a quick-test path, you can carry the
-whole policy in a single `.reg` file:
+- **Pin the upload URL wherever you allow remote jobs.** A debug bundle holds peer names, IP addresses, and logs. Setting `debugBundleUploadURL` to an upload server you run keeps bundles on infrastructure you control.
+- **Keep the metrics endpoint on loopback.** The endpoint has no authentication. A user without administrator rights can only bind it to a loopback address, but a policy value is applied exactly as written. A non-loopback `localMetricsAddress` publishes peer names and connection state to anything that can reach the port.
-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
- ```
+### Connection tuning keys
-Download a sample: netbird-policy.reg.
+Most fleets never need these keys.
-### JumpCloud
+| Key | Type | What it does |
+| --- | --- | --- |
+| `lazyConnection` | boolean | Overrides what the management server decides about [lazy connections](/manage/peers/lazy-connection) for this device. `true` forces them on and `false` forces them off, for every peer the device connects to. Leaving the key out follows the account's **Lazy connections** setting, which is on by default for accounts created by management v0.74.0 or later; older accounts keep the setting they had. The `NB_LAZY_CONN` environment variable, if set on the device, takes precedence over this key. |
+| `wireguardPort` | integer | The local UDP port the WireGuard interface listens on. Default `51820`. Values outside `1` to `65535` are ignored, and the client keeps its previous port. |
-NetBird ships a JumpCloud companion script: netbird-policy.reg.ps1. To use it:
+Set `wireguardPort` only when another program on the device needs the default port, or your firewall rules expect a specific one.
-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.
+### Keys with no effect yet
-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.
+The client recognizes these keys, so they do no harm in a payload, but none of them changes anything today.
-## 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`.
-
-
-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.
-
-
-### 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 netbird-macos.mobileconfig. 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 io.netbird.client.plist
- 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
- `.plist` (this is how macOS materializes the file at
- `/Library/Managed Preferences/.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: netbird-macos.sh. 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.
+| Key | Type | Status |
+| --- | --- | --- |
+| `splitTunnelMode` | string | `allow` or `disallow`. Intended for per-app split tunneling on Android, which has no MDM channel yet. Ignored on every other platform. |
+| `splitTunnelApps` | string | A comma-separated list of Android package names for `splitTunnelMode`. Same status. |
+| `disableMetricsCollection` | boolean | Reserved for a future client telemetry feature. Unrelated to `enableLocalMetrics`. |
## Verifying enforcement
-On any platform, the cleanest verification is the daemon's own debug
-dump:
+On Windows and macOS, ask the daemon what it is enforcing:
```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).
+This prints the resolved configuration as JSON, after every layer including the policy. Its `mDMManagedFields` array lists every key in the policy the client reads. For the user device baseline it reads:
-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.
+```json
+"mDMManagedFields": [
+ "disableProfiles",
+ "disableUpdateSettings",
+ "managementURL"
+]
+```
-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 = ` line per applied key. Secrets are
-redacted.
+If a key you sent is missing from the array, the policy did not reach the device, or it used a name the client does not recognize.
+
+`netbird debug config` reads the policy file at the moment you run it, so it shows a change straight away. The daemon applies the change at its next reload, within a minute, and logs it: `MDM policy changed: added=[...] removed=[...] changed=[...]`, followed, while the client is connected, by `MDM policy changed; restarting engine to apply new configuration`. Those lines are the confirmation that the policy is enforced, not just delivered. The client log records the same information on every reload: one `MDM enrolled with N managed key(s): [...]` line, and one `MDM override = ` line for each setting it overrode, with secrets redacted. The log is `/var/log/netbird/client.log` on macOS and `C:\ProgramData\Netbird\client.log` on Windows.
+
+On iOS there is no CLI. See [Verifying on a device](/manage/peers/mdm-deployment/ios-mdm-policy#verifying-on-a-device).
## Troubleshooting
-**The policy did not apply at all.**
-Check that the daemon can see the source.
+Problems specific to one delivery channel, such as a Group Policy that never reaches the registry or a macOS profile that disappears after a reboot, are covered on the [Windows](/manage/peers/mdm-deployment/windows-mdm-policy#intune-troubleshooting), [macOS](/manage/peers/mdm-deployment/macos-mdm-policy#troubleshooting), and [iOS](/manage/peers/mdm-deployment/ios-mdm-policy#troubleshooting) pages. The problems below look the same on every platform.
-- 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`.
+**`mDMManagedFields` is empty or missing keys.**
+Start at the source. Confirm the values are in the registry or the managed-preferences file, using the commands on the platform page. If they are, look for `MDM ignoring unknown` in the client log: the value name does not match any key in the [reference](#policy-keys-reference). Names are matched without regard to case, but must otherwise be exact. `WireGuardPort`, for example, matches; `WgPort` does not.
-**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 ` warning when this happens.
+**A key is listed in `mDMManagedFields` but the setting did not change.**
+The value is the wrong type or out of range. The key still counts as managed, so users cannot change the setting, but the client keeps its previous value. The client log has a warning such as `MDM management URL "..." invalid` or `MDM wireguard port ... out of range`. On Windows, check that the value has the [registry type](/manage/peers/mdm-deployment/windows-mdm-policy#value-types) the key expects: a `managementURL` stored as a `REG_DWORD`, for example, is ignored. On macOS, check that booleans are `` or ``, not ``; the client logs no warning for those.
-**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.
+**A user gets `fields managed by MDM cannot be modified: [...]`.**
+The policy is working: the user, or a script, tried to change a pinned setting. The keys in the brackets are the pinned settings involved. Change those settings through your MDM instead.
-**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.
+**`netbird up --flag` prints `Already connected` and changes nothing.**
+On a connected client, `netbird up` ignores its flags, whether or not a policy is in place. Run `netbird down` first. On a managed device, the change is then rejected if the policy pins that setting.
-**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.
+**A user gets `update settings are disabled, you cannot use this feature without update settings enabled`.**
+`disableUpdateSettings` is on, or the service was installed with `--disable-update-settings`. Every settings change is rejected, including changes to settings no key names. Lift the policy to make the change.
+
+**A user gets `profiles are disabled, you cannot use this feature without profiles enabled`.**
+`disableProfiles` is on, or the service was installed with `--disable-profiles`. The device stays on its current profile.
+
+**A user gets `network selection is disabled by the administrator`.**
+`disableNetworks` is on, or the service was installed with `--disable-networks`.
+
+**Lazy connections do not follow the policy.**
+The `NB_LAZY_CONN` environment variable is set on the device, and it takes precedence over `lazyConnection`. Remove it from the service environment.
+
+**Devices lost access to network resources after a policy change.**
+Check for `disableClientRoutes: true` or a `preSharedKey` in the policy. The first stops the device using routes; the second stops it connecting to any peer that does not have the same key. See [Device exposure keys](#device-exposure-keys) and [Encryption keys](#encryption-keys).
+
+**A network became unreachable for everyone after a policy change.**
+A routing peer received `blockInbound: true` or `disableServerRoutes: true`, and stopped forwarding. Take routing peers out of the policy's scope.
+
+**My MDM is not mentioned on these pages.**
+Any MDM that can write a registry value on Windows, push a configuration profile on macOS, or send an app configuration on iOS works, because the mechanism belongs to the operating system, not to NetBird.
+
+## When not to use MDM policy
+
+- **Linux and Android devices.** Neither has an MDM channel in NetBird today. On Linux, set the same options with `netbird up` flags or the service-install flags (`netbird service install --disable-update-settings --disable-profiles --disable-networks`), or [bootstrap the client from a config file](/manage/peers/bootstrap-via-config-file).
+- **One device, one change.** To change a setting on a single machine you manage by hand, use the app or the CLI. A policy is for settings that must hold across a fleet and must not drift.
+- **Access control.** A policy configures the client on one device. Which peers can reach which resources belongs in [access policies](/manage/access-control), which apply to every device and do not depend on an MDM.
+
+## TL;DR
+
+- **Find the goal, set the key.** [What you can achieve](#what-you-can-achieve) maps each goal to a setting, and [the user device baseline](#the-user-device-baseline) (`managementURL`, `disableUpdateSettings`, `disableProfiles`) is the usual start.
+- **Present means pinned, even as `false`.** Every key you leave out stays the user's. The client applies the policy last, locks what it pins, re-reads it every minute, and restarts the connection briefly when it changes.
+- **Keep routing peers out of the wrong policy.** Never send them `blockInbound` or `disableServerRoutes`, and send a `preSharedKey` to every peer or none.
+- **Verify with `netbird debug config`, then the log.** `mDMManagedFields` shows the policy the client reads; the `MDM policy changed` log line confirms it is enforced.
diff --git a/src/pages/manage/peers/mdm-deployment/intune-netbird-integration.mdx b/src/pages/manage/peers/mdm-deployment/intune-netbird-integration.mdx
index 89d8b6a0..57fedbda 100644
--- a/src/pages/manage/peers/mdm-deployment/intune-netbird-integration.mdx
+++ b/src/pages/manage/peers/mdm-deployment/intune-netbird-integration.mdx
@@ -14,6 +14,8 @@ This division of security responsibilities creates a comprehensive zero trust im
In this hands-on tutorial, you'll learn how to deploy NetBird with Intune to grant tailored access permissions for different teams.
+This page installs the NetBird client. To enforce its settings once it is installed, such as pinning the management server or making settings read-only, follow [Enforce NetBird Settings on Windows](/manage/peers/mdm-deployment/windows-mdm-policy), which uses an Intune configuration profile.
+
## Prerequisites
Before beginning this tutorial, ensure you have the following prerequisites in place:
@@ -219,4 +221,8 @@ To verify that NetBird was added to Intune, navigate to `Home > Apps | Windows`
While each platform has slightly different configuration options, adding NetBird and assigning it to groups follows the same pattern across Intune. For more information, refer to [Intune app management](https://learn.microsoft.com/en-us/intune/intune-service/apps/app-management).
-With NetBird successfully deployed through Intune, your organization has the foundation for implementing a comprehensive zero trust access model that verifies user identity and device compliance before granting network access.
\ No newline at end of file
+With NetBird successfully deployed through Intune, your organization has the foundation for implementing a comprehensive zero trust access model that verifies user identity and device compliance before granting network access.
+
+## Next: enforce the client's settings
+
+With the client installed, use an Intune configuration profile to pin its settings, such as the management server, and to stop users changing them: see [Enforce NetBird Settings on Windows](/manage/peers/mdm-deployment/windows-mdm-policy).
diff --git a/src/pages/manage/peers/mdm-deployment/ios-mdm-policy.mdx b/src/pages/manage/peers/mdm-deployment/ios-mdm-policy.mdx
new file mode 100644
index 00000000..966f55a1
--- /dev/null
+++ b/src/pages/manage/peers/mdm-deployment/ios-mdm-policy.mdx
@@ -0,0 +1,96 @@
+import { Note, Warning } from "@/components/mdx";
+
+export const description =
+ "Deliver NetBird MDM policy to iOS, iPadOS, and tvOS devices with Managed App Configuration: the payload, where to put it in Intune, Jamf Pro, and other MDMs, when a change reaches the tunnel, and what each key does in the app.";
+
+# Enforce NetBird Settings on iOS
+
+On iOS there is no file or registry for policy. Apple's channel for configuring a managed app is **Managed App Configuration**: your MDM sends a dictionary along with the app, and iOS hands it to that app only. The NetBird app reads it from its own preferences, so the payload must target the NetBird app's bundle id: `io.netbird.app` on iOS and iPadOS, `io.netbird.app.tv` on tvOS.
+
+What each key does, and how the client applies and locks a policy, is on the [MDM Integration](/client/mdm-integration) page. Read its [How the client applies a policy](/client/mdm-integration#how-the-client-applies-a-policy) section first if you have not.
+
+The examples use [the user device baseline](/client/mdm-integration#the-user-device-baseline): a self-hosted server at `https://netbird.example.com:443`, read-only settings, and no extra profiles.
+
+
+**The app must be managed by the MDM that sends the configuration.** It does not have to be installed by it: an MDM can take over management of a copy the user installed from the App Store, and on an unsupervised device the user has to accept that change. Until the app is managed it receives no app configuration, and NetBird runs with no policy on that device.
+
+
+## Push the payload
+
+The payload is a plain property-list dictionary, with the same key names as on desktop. The user device baseline is:
+
+```xml
+
+ managementURL
+ https://netbird.example.com:443
+ disableUpdateSettings
+
+ disableProfiles
+
+
+```
+
+Start from netbird-ios-appconfig.plist and delete the keys you do not want to pin: a key that is present is pinned, even when its value is `false`. Then add it to the NetBird app's assignment in your MDM:
+
+- **Microsoft Intune**: **Apps → App configuration policies → Add → Managed devices**, platform **iOS/iPadOS**, pick the NetBird app, then set **Configuration settings format** to **Enter XML data** and paste the dictionary.
+- **Jamf Pro**: **Devices → Mobile Device Apps → NetBird → App Configuration**, and paste the dictionary.
+- **Kandji**, **Mosyle**, **Workspace ONE**, **JumpCloud**: the same field exists on the managed-app assignment, usually labelled *App Configuration* or *Application Configuration*.
+
+Booleans can be written as `` and ``, or as `1` and `0`. Integers and strings behave as on desktop.
+
+## When a policy takes effect
+
+Every key that applies to iOS is enforced: the keys that reshape the app, and the keys the client applies to the connection itself. When a change lands depends on one detail of how iOS works.
+
+iOS delivers the configuration to the NetBird app. The tunnel, though, runs in a Network Extension: a separate process the system starts when the tunnel comes up and stops when it goes down, and which Apple's app-configuration channel does not reach. The app therefore passes the configuration on to the extension whenever the app is running: when it becomes active, when a settings screen appears, and on a timer about every 30 seconds. When the policy has changed, the tunnel restarts on its own and the user is told their IT policy was applied.
+
+
+**A policy lands while the app is open.** With NetBird in the foreground, a change pushed from your MDM reaches the tunnel within about 30 seconds, or at once when the user switches back to the app. Nothing has to be restarted by hand.
+
+**While the app is closed or in the background, iOS suspends it**, and a policy pushed or withdrawn in that time reaches the tunnel only the next time the app is opened. This matters most with VPN On Demand or the widget, where the tunnel can come up without the app ever appearing: it then runs on the last policy the app saw. Plan rollouts accordingly, and treat "applied" as "applied once the user next opens the app".
+
+
+
+**On tvOS, only the app is enforced, not the tunnel.** The hand-off described above relies on a shared app group, which does not work between the tvOS app and its extension, so the tvOS tunnel runs without the policy. Keys that lock or hide controls behave as described below; the keys the client applies to the connection (`managementURL`, `preSharedKey`, `rosenpassEnabled`, `rosenpassPermissive`, `blockInbound`, `wireguardPort`, `lazyConnection`) do not reach the tunnel on tvOS yet.
+
+
+## What each key does in the app
+
+| Key | Effect in the iOS and tvOS app |
+| --- | --- |
+| `managementURL` | Hides **Settings → Connection** and the server step of onboarding. The profile-creation sheet shows and uses the pinned URL. |
+| `preSharedKey` | The key row reads *Configured* and is locked: no Save, no Remove. The value itself never reaches the app's interface. |
+| `rosenpassEnabled`, `rosenpassPermissive` | Locks its own switch and shows the pinned value. |
+| `disableClientRoutes` | Hides the exit-node selector. The resource list explains why, instead of listing resources. |
+| `disableAutoConnect` | VPN On Demand becomes read-only. |
+| `disableProfiles` | Hides the Profiles section: no creating, switching, or removing profiles. |
+| `disableNetworks` | Removes the Resources tab. |
+| `disableAdvancedView` | Hides Advanced (on tvOS, the whole section). |
+| `disableUpdateSettings` | Read-only mode: nothing is hidden, and every configuration control is locked. |
+
+A second group of keys has no control to lock, because the client applies them straight to the connection: `blockInbound`, `wireguardPort`, `lazyConnection`, `allowRemoteJobs`, and `debugBundleUploadURL` (when a remote job produces a bundle). On iOS they are enforced all the same, with nothing on screen to mark: a device whose policy sets `allowRemoteJobs: false`, for example, refuses remote jobs. On tvOS none of them reach the tunnel, for the reason in the warning above.
+
+The remaining keys do not apply to iOS and are ignored, so they are safe in a payload shared with desktop devices: `allowServerSSH` (there is no SSH server on iOS), `disableServerRoutes` (an iOS device does not route for other peers), `disableAutostart` (desktop app only), `disableMetricsCollection`, `enableLocalMetrics` and `localMetricsAddress` (the local endpoint is not reachable on iOS), and `splitTunnelMode` and `splitTunnelApps`.
+
+## Verifying on a device
+
+There is no CLI on iOS. A pinned setting shows a lock and a **Managed by your organization** note in the app, and a gated section disappears altogether. For the user device baseline, the Profiles section is gone and every configuration control is locked.
+
+That confirms the app received the policy. To confirm it reached the *tunnel*, change the policy while the tunnel is connected and bring NetBird to the foreground: the tunnel restarts and the device shows "NetBird configuration was updated by your IT policy". That notice comes from the extension, so seeing it means the tunnel applied the new policy.
+
+## Troubleshooting
+
+**Nothing is locked in the app.**
+The app did not receive the configuration. Check that NetBird is a managed app on the device (installed by your MDM, or taken over by it), that the app-configuration policy is assigned to the same device group as the app, and that it targets the bundle id `io.netbird.app` (`io.netbird.app.tv` on tvOS).
+
+**The app shows the policy, but the tunnel still behaves as before.**
+The app passes the policy to the tunnel only while it is running. Open NetBird and leave it in the foreground for half a minute; the tunnel restarts and reports that the policy was applied. On tvOS the tunnel does not receive the policy at all; see [When a policy takes effect](#when-a-policy-takes-effect).
+
+For problems that look the same on every platform, see [Troubleshooting](/client/mdm-integration#troubleshooting) on the MDM Integration page.
+
+## Recap
+
+- iOS delivers the policy as Managed App Configuration to the managed NetBird app, bundle id `io.netbird.app` (tvOS: `io.netbird.app.tv`).
+- The user device baseline is the same three keys as on desktop: `managementURL`, `disableUpdateSettings`, and `disableProfiles`.
+- A change reaches the tunnel while the app is open, within about 30 seconds; on tvOS it reaches the app only.
+- Confirm in the app: pinned settings show **Managed by your organization**.
diff --git a/src/pages/manage/peers/mdm-deployment/macos-mdm-policy.mdx b/src/pages/manage/peers/mdm-deployment/macos-mdm-policy.mdx
new file mode 100644
index 00000000..07b5784e
--- /dev/null
+++ b/src/pages/manage/peers/mdm-deployment/macos-mdm-policy.mdx
@@ -0,0 +1,127 @@
+import { Note, Warning } from "@/components/mdx";
+
+export const description =
+ "Deliver NetBird MDM policy to macOS devices: a configuration profile for the io.netbird.client preference domain, with steps for Jamf Pro, Kandji, Mosyle, Intune, Workspace ONE, and JumpCloud, and how to confirm it arrived.";
+
+# Enforce NetBird Settings on macOS
+
+On macOS, the NetBird client reads its MDM policy from `/Library/Managed Preferences/io.netbird.client.plist`. Normally you do not write that file yourself: macOS writes it when your MDM installs a configuration profile with managed preferences for the preference domain (bundle id) `io.netbird.client`. This page shows how to build that profile, deliver it with the common MDMs, and confirm it arrived.
+
+What each key does, and how the client applies and locks a policy, is on the [MDM Integration](/client/mdm-integration) page. Read its [How the client applies a policy](/client/mdm-integration#how-the-client-applies-a-policy) section first if you have not.
+
+The examples use [the user device baseline](/client/mdm-integration#the-user-device-baseline): a self-hosted server at `https://netbird.example.com:443`, read-only settings, and no extra profiles. As managed preferences, the whole policy is:
+
+```xml
+managementURL
+https://netbird.example.com:443
+disableUpdateSettings
+
+disableProfiles
+
+```
+
+
+**macOS clears `/Library/Managed Preferences/` at every boot on a Mac that is not enrolled in an MDM.** Writing the file by hand, with `defaults write` or a script, works for a quick test, but the policy is gone after the next reboot. Use a configuration profile delivered by an MDM for anything beyond a test.
+
+
+## Templates
+
+Download the template that matches your MDM:
+
+- netbird-macos.mobileconfig: a complete configuration profile. Use it with Kandji, Mosyle, Intune, Workspace ONE, JumpCloud, and Apple Configurator.
+- io.netbird.client.plist: the bare managed-preferences dictionary, without the profile wrapper. Use it with Jamf Pro's Application & Custom Settings payload.
+- netbird-macos.sh: a script that writes the policy file directly, for JumpCloud fleets that are not MDM-enrolled.
+
+Each template lists every key. Delete the keys you do not want to pin: a key that is present is pinned, even when its value is `false`.
+
+Write booleans as `` or ``. On macOS, a boolean written as `1` is listed as managed and locked, but never applied, so the setting is frozen at whatever the Mac had.
+
+## Build the configuration profile
+
+1. Open `netbird-macos.mobileconfig` in a text editor, or in a profile editor such as [iMazing Profile Editor](https://imazing.com/profile-editor) or [ProfileCreator](https://github.com/ProfileCreator/ProfileCreator).
+2. Inside the `mcx_preference_settings` dictionary, keep only the keys you want to pin, with your values. For the user device baseline, that is the three keys shown at the top of this page. Keep `io.netbird.client` as the preference domain.
+3. Replace each placeholder `PayloadUUID` with a freshly generated UUID (run `uuidgen`), so every deployment has unique identifiers.
+
+Profiles delivered through an MDM are signed by the MDM, so there is nothing to sign by hand for a managed rollout. A profile you hand out some other way, with Apple Configurator or as a file a user double-clicks, installs unsigned, and recent macOS releases ask the user for an extra confirmation. To avoid that, sign the profile as a CMS message:
+
+```bash
+security cms -S -N "" -i netbird-macos.mobileconfig -o signed.mobileconfig
+```
+
+Use an identity the target Macs already trust. Configuration profiles are **not** signed with `productsign`, which is for installer packages, nor with an Apple Developer ID certificate.
+
+## Deliver it with your MDM
+
+- **Jamf Pro**: **Computers → Configuration Profiles → New → Application & Custom Settings → External Applications → Upload File (Plist file)**, with the preference domain `io.netbird.client`, and upload the edited `io.netbird.client.plist`. Jamf can also take the whole `.mobileconfig` through **Computers → Configuration Profiles → Upload**, but it may drop payload keys it does not recognize from an unsigned profile, so sign the profile first if you use that route.
+- **Kandji**: add a **Custom Profile** library item and upload the `.mobileconfig`.
+- **Mosyle**: **Management → Management Profiles → Certificates / Custom Profiles → Add new profile**, and upload the `.mobileconfig`.
+- **Microsoft Intune**: **Devices → Configuration → Create profile → macOS → Templates → Custom**, and upload the `.mobileconfig`.
+- **Workspace ONE** and other MDMs: upload the `.mobileconfig` as a custom configuration profile and scope it to the target device group.
+- **Apple Configurator 2**, for testing on a connected Mac without an MDM: drag the `.mobileconfig` onto the device and install it.
+
+### JumpCloud
+
+JumpCloud can deliver the policy two ways. Use the first if your Macs are MDM-enrolled with JumpCloud.
+
+#### MDM Custom Configuration Profile (MDM-enrolled Macs)
+
+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. Under **Settings**, upload your edited `netbird-macos.mobileconfig` (see [Build the configuration profile](#build-the-configuration-profile)).
+4. Bind the policy to the target device group and save.
+
+JumpCloud delivers the profile through MDM. Removing the policy in JumpCloud removes the file at the next sync, which unpins the settings on the client.
+
+#### Shell command (Macs not enrolled in MDM)
+
+For JumpCloud-managed Macs that are not MDM-enrolled, use netbird-macos.sh:
+
+1. Edit the `### POLICY VALUES ###` block at the top of the script: set the keys you want to pin, and leave the rest at `$NULL`.
+2. In the JumpCloud admin console, go to **Device Management → Commands → +**. Set **Type** to **Mac, Shell** and **Run as** to **root**.
+3. Paste the edited script into the command body.
+4. Bind the command to the target device group and run it.
+
+The script writes `/Library/Managed Preferences/io.netbird.client.plist`, owned by `root:wheel` with mode `644`, and restarts the NetBird daemon so the policy applies at once. On a Mac that is not MDM-enrolled, macOS deletes the file at the next reboot, so run the command again after each reboot, or enroll the Macs and use the configuration profile.
+
+
+**Every user on the Mac can read the policy file.** From a configuration profile, macOS writes it as a binary plist owned by `root:wheel` with mode `644`, and the shell script above uses the same owner and mode. It is readable by all users, so a `preSharedKey` or `debugBundleUploadURL` in it is visible to anyone who can sign in. Only root can change it. The client refuses to read a policy file that any user can write to, and then enforces no policy at all, so never loosen the file's permissions.
+
+
+## Verify
+
+On a target Mac, check that macOS wrote the policy file:
+
+```bash
+sudo defaults read "/Library/Managed Preferences/io.netbird.client"
+```
+
+The output should list the keys from your profile. Then check what the client is enforcing:
+
+```bash
+netbird debug config
+```
+
+The `mDMManagedFields` array in the output lists every key in the policy the client reads. For the user device baseline, it contains `disableProfiles`, `disableUpdateSettings`, and `managementURL`. The client log at `/var/log/netbird/client.log` has an `MDM enrolled with N managed key(s): [...]` line on every reload, and an `MDM policy changed: added=[...]` line once the daemon has applied a change, within a minute. See [Verifying enforcement](/client/mdm-integration#verifying-enforcement) for more.
+
+## Troubleshooting
+
+**The policy file does not exist.**
+The profile was not installed, or it targets the wrong preference domain. On the Mac, open **System Settings → General → Device Management** and confirm the profile is listed, then confirm the domain in the profile is exactly `io.netbird.client`.
+
+**The policy file disappears after a reboot.**
+The Mac is not MDM-enrolled, so macOS clears `/Library/Managed Preferences/` at boot. Enroll it and deliver the policy as a configuration profile.
+
+**The file exists, but `mDMManagedFields` is empty.**
+Check the file's permissions with `ls -l "/Library/Managed Preferences/io.netbird.client.plist"`. If any user can write to it (a `w` in the last three permission characters), the client ignores the whole policy, and with default logging it says nothing about it. Reset the permissions with `sudo chown root:wheel` and `sudo chmod 644` on the file. If the permissions are fine, look in the client log for `MDM ignoring unknown plist key`: a key name does not match any key in the [reference](/client/mdm-integration#policy-keys-reference).
+
+**A key is listed in `mDMManagedFields`, but the setting did not change.**
+Check the value's type. A boolean must be `` or ``; written as an ``, it is listed as managed and locked, but never applied. The client logs no warning for it.
+
+For problems that look the same on every platform, see [Troubleshooting](/client/mdm-integration#troubleshooting) on the MDM Integration page.
+
+## Recap
+
+- The client reads `/Library/Managed Preferences/io.netbird.client.plist`, which macOS writes from a configuration profile for `io.netbird.client`.
+- The user device baseline is three keys: `managementURL`, `disableUpdateSettings`, and `disableProfiles`.
+- Upload `netbird-macos.mobileconfig` to most MDMs, JumpCloud included; Jamf Pro's Application & Custom Settings takes the bare `io.netbird.client.plist`.
+- Confirm with `sudo defaults read`, then `netbird debug config`.
diff --git a/src/pages/manage/peers/mdm-deployment/windows-mdm-policy.mdx b/src/pages/manage/peers/mdm-deployment/windows-mdm-policy.mdx
new file mode 100644
index 00000000..ef9de1ae
--- /dev/null
+++ b/src/pages/manage/peers/mdm-deployment/windows-mdm-policy.mdx
@@ -0,0 +1,171 @@
+import { Note, Warning } from "@/components/mdx";
+
+export const description =
+ "Enforce NetBird client settings on Windows with Microsoft Intune: import the NetBird administrative templates, build and assign a configuration profile, and verify it. Plus a registry reference for Group Policy, JumpCloud, and .reg files.";
+
+# Enforce NetBird Settings on Windows
+
+On Windows, you enforce NetBird settings with Microsoft Intune: import NetBird's administrative templates once, build a configuration profile from them, and assign it to your devices. The client then applies the settings and locks them. Behind the scenes, the profile writes values to one registry key, `HKLM\Software\Policies\NetBird`, which is what the client reads. The [registry reference](#registry-reference) at the end of this page covers that key directly, for Group Policy, JumpCloud, or a `.reg` file.
+
+This page enforces settings on a client that is already installed. To install the client with Intune, follow [Deploying NetBird with Intune](/manage/peers/mdm-deployment/intune-netbird-integration) first. What each setting does, and how the client applies and locks a policy, is on the [MDM Integration](/client/mdm-integration) page.
+
+The examples use [the user device baseline](/client/mdm-integration#the-user-device-baseline): a self-hosted server at `https://netbird.example.com:443`, read-only settings, and no extra profiles. In Intune that is three policies: **Management URL**, **Disable update settings**, and **Disable profiles**.
+
+## Before you start
+
+- An Intune admin account with at least the **Policy and Profile Manager** role.
+- Windows devices enrolled in Intune, with the NetBird client installed, in a device group you can assign to.
+- The NetBird templates: netbird.admx and netbird.adml. They cover every setting, have no dependency on other templates, and stay within Intune's limits for imported templates.
+
+## Enforce settings with Intune
+
+### Step 1: Import the NetBird templates
+
+You do this once per tenant.
+
+1. In the Intune admin center, go to **Devices → Manage devices → Configuration**, open the **Import ADMX** tab, and select **Import**.
+2. Upload `netbird.admx` as the ADMX file and `netbird.adml` as the ADML file for the default language (`en-us`, the only language Intune accepts).
+3. Select **Next**, then **Create**.
+4. Select **Refresh** until the template's status is **Available**.
+
+
+**Updating the templates later.** Intune refuses to import a template whose settings are already imported. When NetBird ships updated templates, for example with new settings, delete the configuration profiles that use the NetBird template, delete the old template on the **Import ADMX** tab, and then import the new files. Note your profile settings before you delete them.
+
+
+### Step 2: Create the configuration profile
+
+1. Go to **Devices → Manage devices → Configuration → Create → New policy**.
+2. Set **Platform** to **Windows 10 and later** and **Profile type** to **Templates**, then select **Imported Administrative templates (Preview)** and **Create**.
+3. Give the profile a name you can find later, such as `NetBird: user device baseline`.
+4. Under **Configuration settings**, open **NetBird**. Each policy is named after its setting in plain words. For the user device baseline:
+ - **Management URL**: **Enabled**, value `https://netbird.example.com:443`.
+ - **Disable update settings**: **Enabled**.
+ - **Disable profiles**: **Enabled**.
+
+ Leave every other policy **Not configured**. A policy set to **Disabled** is not the same as **Not configured**: it pins the setting to `false`, out of the user's reach. See [How the client applies a policy](/client/mdm-integration#how-the-client-applies-a-policy).
+5. Under **Assignments**, assign the profile to a **device group**. NetBird's policies are machine settings, so a device group is what you want. To build the profile for each device role, use [Recommended policies](/client/mdm-integration#recommended-policies), and keep routing peers out of any group that gets **Block inbound** or **Disable server routes**.
+6. Select **Create**.
+
+### Step 3: Sync and verify
+
+Devices pick up the profile at their next Intune check-in. To speed up a test device, open **Settings → Accounts → Access work or school**, select the work account, select **Info**, and then **Sync**. The NetBird client applies the policy within a minute of it arriving, and restarts its connection to do so: expect a brief interruption.
+
+In the Intune admin center, open the profile and check its device and user check-in status. On the device itself:
+
+```
+reg query HKLM\Software\Policies\NetBird
+netbird debug config
+```
+
+`reg query` shows the values Intune wrote. In `netbird debug config`, the `mDMManagedFields` array lists every setting in the policy the client reads; for the user device baseline it contains `disableProfiles`, `disableUpdateSettings`, and `managementURL`. The client log at `C:\ProgramData\Netbird\client.log` has an `MDM policy changed: added=[...]` line once the client has applied the change. See [Verifying enforcement](/client/mdm-integration#verifying-enforcement) for more.
+
+### If you cannot import templates: custom OMA-URI
+
+Imported templates are the simpler path; prefer them. Without them, a custom OMA-URI profile (**Templates → Custom**) takes two kinds of setting, following Microsoft's [ADMX-backed policy ingestion](https://learn.microsoft.com/en-us/windows/client-management/win32-and-centennial-app-policy-configuration):
+
+1. **Ingest the template**: OMA-URI `./Device/Vendor/MSFT/Policy/ConfigOperations/ADMXInstall/NetBird/Policy/NetBirdAdmx`, data type **String**, value: the full contents of `netbird.admx`. This only makes the policies known to the device; it sets nothing.
+2. **Set each policy**: OMA-URI `./Device/Vendor/MSFT/Policy/Config/NetBird~Policy~NetBird/`, where `` is the policy's `name` in `netbird.admx`, with data type **String**. The value depends on what you want to pin:
+ - **On, or a value**: ``, plus a `` element for a policy that carries a value, with the element `id` from the same policy in `netbird.admx`.
+ - **Off**: ``. For the on/off policies the template writes `0`, which pins the setting off. `` always writes `1`, so never use it to turn a setting off.
+
+ For the user device baseline's server, the **Management URL** policy is `.../NetBird~Policy~NetBird/ManagementURL` with the value ``.
+
+ To stop pinning a setting, it has to go back to **Not configured**, not ``. Removing an OMA-URI from a custom profile is not guaranteed to clear the value on the device, so check with `reg query HKLM\Software\Policies\NetBird` afterwards.
+
+The URIs above follow Microsoft's naming rules for this template; they have not been tested in an Intune tenant.
+
+### Intune troubleshooting
+
+**The template import fails.**
+If the error says the settings already exist, an earlier version of the NetBird template is imported: see **Updating the templates later** above. Otherwise, check that you uploaded `netbird.adml` with `netbird.admx`, as the `en-us` language file.
+
+**The profile shows an error or never reaches the device.**
+Check the profile's per-device status in the admin center, and that the device is in the assigned device group. On the device, trigger a sync from **Settings → Accounts → Access work or school → Info → Sync**, then run `reg query HKLM\Software\Policies\NetBird`. If the key is empty, the profile has not been applied yet.
+
+**The values are in the registry, but the setting did not change.**
+The client applies a change at its next reload, within a minute. If `mDMManagedFields` still does not list the setting, or lists it without the setting changing, see [Registry troubleshooting](#registry-troubleshooting).
+
+For problems that look the same on every platform, see [Troubleshooting](/client/mdm-integration#troubleshooting) on the MDM Integration page.
+
+## Registry reference
+
+Whatever writes it, the client reads its policy from `HKLM\Software\Policies\NetBird`: an Intune profile, a Group Policy object, a script, or `reg import` all end in the same key. This section is for working with that key directly, and for checking what Intune wrote.
+
+### Value types
+
+Each setting needs a value of the right registry type. Value names are matched without regard to case.
+
+| Registry type | Value names |
+| --- | --- |
+| `REG_SZ` (string) | `ManagementURL`, `PreSharedKey`, `DebugBundleUploadURL`, `LocalMetricsAddress`, `SplitTunnelMode`, `SplitTunnelApps` |
+| `REG_DWORD`, `0` or `1` (boolean) | `AllowRemoteJobs`, `AllowServerSSH`, `BlockInbound`, `DisableAdvancedView`, `DisableAutoConnect`, `DisableAutostart`, `DisableClientRoutes`, `DisableMetricsCollection`, `DisableNetworks`, `DisableProfiles`, `DisableServerRoutes`, `DisableUpdateSettings`, `EnableLocalMetrics`, `LazyConnection`, `RosenpassEnabled`, `RosenpassPermissive` |
+| `REG_DWORD` (integer) | `WireguardPort`, for example `0x0000ca6c` for `51820` |
+
+A boolean can also be a `REG_SZ` holding `true`/`false`, `1`/`0`, `yes`/`no`, or `on`/`off`. A string setting stored with any type other than `REG_SZ` or `REG_EXPAND_SZ` is ignored: a `ManagementURL` stored as a `REG_DWORD`, for example, has no effect. The client reads `REG_SZ`, `REG_EXPAND_SZ`, `REG_DWORD`, `REG_QWORD`, and `REG_MULTI_SZ` values. Any other type, such as `REG_BINARY`, is skipped with a warning in its log and is not listed in `mDMManagedFields`.
+
+
+**Standard users can read these values.** By default, the NetBird policy key inherits read access for Authenticated Users, so a `PreSharedKey` or `DebugBundleUploadURL` stored there is visible to them. Only administrators can change the values. An administrator on the device can always edit or delete the key, so give users standard accounts.
+
+
+The user device baseline as raw registry values:
+
+```
+Windows Registry Editor Version 5.00
+
+[HKEY_LOCAL_MACHINE\Software\Policies\NetBird]
+"ManagementURL"="https://netbird.example.com:443"
+"DisableUpdateSettings"=dword:00000001
+"DisableProfiles"=dword:00000001
+```
+
+### Group Policy
+
+For domain-joined devices, follow [Deploying NetBird with Group Policy (GPO)](/manage/peers/mdm-deployment/windows-gpo-deployment). It walks through the Central Store, creating the policy GPO, installing the MSI silently, and scoping, end to end. The same `netbird.admx` and `netbird.adml` templates appear under **Computer Configuration → Administrative Templates → NetBird**.
+
+To test the templates on a single machine with the local Group Policy editor:
+
+1. Copy `netbird.admx` to `C:\Windows\PolicyDefinitions\` and `netbird.adml` to `C:\Windows\PolicyDefinitions\en-US\`.
+2. Open `gpedit.msc` and go to **Computer Configuration → Administrative Templates → NetBird**.
+3. Open a policy, such as **Management URL**, set it to **Enabled**, enter the value, and click **OK**.
+4. Run `gpupdate /force`.
+
+### A `.reg` file
+
+For a device without an MDM, or a quick test, carry the whole policy in one `.reg` file, such as the user device baseline above, and apply it from an elevated prompt with `reg import netbird-policy.reg`. To build one from a reference machine that already has the policy you want, export the key with `reg export "HKLM\Software\Policies\NetBird" netbird-policy.reg /y`.
+
+netbird-policy.reg is a sample that sets every setting, including `PreSharedKey` and `DisableClientRoutes`, which cut a laptop off from most of the network. Delete every line you do not mean to pin before you use it.
+
+`reg import` adds and overwrites values, but never removes them. To take a setting out of the policy, delete its value with `reg delete "HKLM\Software\Policies\NetBird" /v /f`.
+
+### JumpCloud
+
+NetBird ships a companion script for JumpCloud, netbird-policy.reg.ps1, that applies a `.reg` file as the complete policy for the device.
+
+1. In the JumpCloud admin console, go to **Device Management → Commands → +**.
+2. Set **Type** to **Windows PowerShell** and **Run as** to **SYSTEM**.
+3. Paste `netbird-policy.reg.ps1` into the command body, unchanged.
+4. Attach your `.reg` file to the same command. JumpCloud copies attached files into the command's working directory before it runs the script.
+5. Bind the command to the target device group and run it.
+
+The script deletes the existing `HKLM\Software\Policies\NetBird` key before it imports the `.reg` file, so the file is the **complete** policy for the device: any setting it does not contain is no longer pinned. To remove the whole policy, attach a `.reg` file that contains only the header line.
+
+### Registry troubleshooting
+
+**`reg query` shows no values.**
+Nothing wrote them. For Group Policy, run `gpresult /h report.html` and look for errors on the NetBird GPO. For Intune, see [Intune troubleshooting](#intune-troubleshooting).
+
+**The values are in the registry, but `mDMManagedFields` does not list them.**
+Look for `MDM ignoring unknown registry value` in the client log: the value name does not match a setting. Then look for `MDM ignoring unsupported registry value type`: the value has a registry type the client does not read.
+
+**A setting is listed in `mDMManagedFields`, but it did not change.**
+The value has the wrong type for its setting, such as a `ManagementURL` stored as a `REG_DWORD`. Check it against [Value types](#value-types).
+
+**A setting stays pinned after you removed it from a `.reg` file.**
+`reg import` does not delete values. Delete the value with `reg delete`, or use the [JumpCloud](#jump-cloud) script, which replaces the whole key.
+
+## Recap
+
+- Import `netbird.admx` and `netbird.adml` into Intune once, build a configuration profile from **Imported Administrative templates**, and assign it to a device group.
+- The user device baseline enables three policies: **Management URL**, **Disable update settings**, and **Disable profiles**. Everything else stays **Not configured**.
+- Confirm in Intune, then on the device with `reg query` and `netbird debug config`.
+- Without Intune, the same settings are registry values under `HKLM\Software\Policies\NetBird`, written by Group Policy, JumpCloud, or a `.reg` file.
diff --git a/src/pages/manage/peers/ssh.mdx b/src/pages/manage/peers/ssh.mdx
index e44a0514..8fa00d37 100644
--- a/src/pages/manage/peers/ssh.mdx
+++ b/src/pages/manage/peers/ssh.mdx
@@ -77,7 +77,7 @@ On the machine you want to access via SSH, enable the NetBird SSH server.
On a machine where you do not have those rights, which is the normal case for a
managed workstation, you cannot enable the SSH server yourself: an administrator
has to run the command. For the SSH server itself there is also
- [`allowServerSSH`](/client/mdm-integration#policy-keys-reference), which the client
+ [`allowServerSSH`](/client/mdm-integration#allowServerSSH), which the client
applies from MDM policy and so needs nothing from you; root login and SSH
authentication have no policy key and must be set on the machine. A switch already in its safe
state stays operable: you can turn the SSH server off, turn root login off, or
diff --git a/src/pages/use-cases/remote-access/exit-nodes.mdx b/src/pages/use-cases/remote-access/exit-nodes.mdx
index 7e4033a1..a41f7b92 100644
--- a/src/pages/use-cases/remote-access/exit-nodes.mdx
+++ b/src/pages/use-cases/remote-access/exit-nodes.mdx
@@ -214,7 +214,7 @@ Set up the exit node as described in the [configuration steps](#configuration-st
### 2. Lock network selection on the devices
-Deliver `disableNetworks: true` to the devices through your device management tooling: a registry policy or Intune profile on Windows, a configuration profile on macOS. See [MDM Integration](/client/mdm-integration) for the payload formats and delivery options per platform. The daemon re-reads the policy about once a minute, so it takes effect without restarting NetBird.
+Deliver `disableNetworks: true` to the devices through your device management tooling: a registry policy or Intune profile on Windows, a configuration profile on macOS. See [MDM Integration](/client/mdm-integration) for how the client applies a policy, and the [Windows](/manage/peers/mdm-deployment/windows-mdm-policy), [macOS](/manage/peers/mdm-deployment/macos-mdm-policy), and [iOS](/manage/peers/mdm-deployment/ios-mdm-policy) pages for the payload formats and delivery options. The daemon re-reads the policy about once a minute, so it takes effect without restarting NetBird.
On Linux devices and servers, where there is no MDM channel, set the equivalent flag when installing the service:
@@ -245,7 +245,7 @@ On a locked device, `netbird networks list` returns the "network selection is di
With Auto Apply and `disableNetworks` in place, every device in `remote-workers` routes its internet traffic through the exit node, and no user can select, deselect, or switch exit nodes from the device. Enforcement lives in the daemon, so the CLI and the UI are equally locked.
-It does not make the tunnel itself mandatory. A user can still disconnect NetBird entirely with `netbird down`, and a user with administrator rights on the device can stop or remove the service; NetBird has no always-on or kill-switch mode. Enforce that layer with the operating system: run devices with standard user accounts, manage the NetBird service through your MDM, and design your access policies so that disconnecting from NetBird costs the user access to company resources instead of freeing them from restrictions. The `disableUpdateSettings` and `disableProfiles` keys close the remaining side doors of reconfiguring the client or switching to an unmanaged profile; see [MDM Integration](/client/mdm-integration#policy-keys-reference).
+It does not make the tunnel itself mandatory. A user can still disconnect NetBird entirely with `netbird down`, and a user with administrator rights on the device can stop or remove the service; NetBird has no always-on or kill-switch mode. Enforce that layer with the operating system: run devices with standard user accounts, manage the NetBird service through your MDM, and design your access policies so that disconnecting from NetBird costs the user access to company resources instead of freeing them from restrictions. The `disableUpdateSettings` and `disableProfiles` keys close the remaining side doors of reconfiguring the client or switching to an unmanaged profile; see [Lock the client app](/client/mdm-integration#lock-the-client-app).
## Selective Internal Access Behind an Exit Node