docs: document the MDM keys added since the page was written, and iOS

The reference table listed 18 of the 23 keys the client recognises, and
claimed a count of 16. Six keys were missing from every downloadable
template as well, including lazyConnection, which the page already
documented:

- lazyConnection, disableAdvancedView, enableLocalMetrics,
  localMetricsAddress, allowRemoteJobs and debugBundleUploadURL are now
  in the reference table and in netbird.admx/.adml, io.netbird.client.plist,
  netbird-macos.mobileconfig, netbird-macos.sh and netbird-policy.reg.
- Notes call out the two pairs that read as related but are not:
  disableMetricsCollection (anonymous telemetry) versus
  enableLocalMetrics (the client's own Prometheus endpoint), and
  disableAdvancedView being UI-only, so unlike its neighbours it never
  causes a request to be rejected.

iOS and tvOS now have a section of their own. The channel is Apple's
Managed App Configuration against bundle id io.netbird.app
(io.netbird.app.tv on tvOS), with a new netbird-ios-appconfig.plist
template and per-provider instructions. The section states what the
policy reaches today: the app locks its interface and refuses
configuration changes, while the tunnel engine runs in a Network
Extension that does not receive the app configuration, so keys the
engine applies to the connection are not enforced there yet.

Two template defects found on the way:

- netbird-macos.mobileconfig was not well-formed XML. Its header
  comment contained a double hyphen, which XML forbids inside a
  comment, so plutil -lint and any strict parser rejected the file an
  admin downloaded.
- netbird-policy.reg had one LF-terminated line among CRLF ones;
  normalised, with the UTF-16LE encoding reg import requires preserved.

Verified with npm run build and npm run lint:mdx, plus XML well-formedness
on all five XML templates, bash -n on the shell template, and a
cross-reference check that every string and presentation the ADMX
references is defined in the ADML.
This commit is contained in:
riccardom
2026-09-17 09:17:25 +02:00
parent fed7b1e25b
commit 182b1b9e87
8 changed files with 447 additions and 17 deletions
@@ -85,6 +85,15 @@
<false/>
-->
<!-- ===== 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. -->
<!--
<key>lazyConnection</key>
<true/>
-->
<!-- ===== WireGuard UDP port =====
Range 1-65535. Omit to keep the daemon default. -->
<!--
@@ -92,6 +101,35 @@
<integer>51820</integer>
-->
<!-- ===== Remote jobs =====
allowRemoteJobs : allow management-requested remote jobs
(e.g. debug bundles) on this peer. Off by
default. Present at any value locks the
client toggle, so false pins it off.
debugBundleUploadURL: 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. -->
<!--
<key>allowRemoteJobs</key>
<false/>
<key>debugBundleUploadURL</key>
<string>https://uploads.example.com</string>
-->
<!-- ===== Local Prometheus metrics endpoint =====
Unrelated to disableMetricsCollection, which governs anonymous
usage telemetry. -->
<!--
<key>enableLocalMetrics</key>
<true/>
<key>localMetricsAddress</key>
<string>127.0.0.1:9191</string>
-->
<!-- ===== UI / lockdown kill switches =====
disableUpdateSettings : block every config change from UI and CLI
on this device (Settings view stays
@@ -99,6 +137,9 @@
disableProfiles : hide the profile menu, reject profile CRUD.
disableNetworks : hide the Networks / Exit Node menus,
reject the related RPCs.
disableAdvancedView : hide the advanced section of the UI.
UI-only: nothing is rejected on its
account.
disableMetricsCollection: opt out of anonymous usage telemetry. -->
<!--
<key>disableUpdateSettings</key>
@@ -110,6 +151,9 @@
<key>disableNetworks</key>
<true/>
<key>disableAdvancedView</key>
<true/>
<key>disableMetricsCollection</key>
<false/>
-->
@@ -0,0 +1,110 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<!--
NetBird Managed App Configuration (iOS / iPadOS / tvOS).
Bundle identifier of the managed app:
io.netbird.app (iOS, iPadOS)
io.netbird.app.tv (tvOS)
Delivery: your MDM sends this dictionary alongside the managed app
(Intune "App configuration policies" → Enter XML data; Jamf Pro
"Mobile Device Apps → App Configuration"; the equivalent App
Configuration field on Kandji, Mosyle, Workspace ONE, JumpCloud).
iOS hands it to the app, which reads it on every policy load.
The app must be installed BY the MDM. An app the user installed
from the App Store themselves cannot receive a configuration, and
NetBird then runs with no policy at all.
Editing this file:
- Delete or comment out every key you do NOT want to enforce. An
absent key means "no enforcement" for that field.
- Some MDM consoles want only the inner <dict>; paste from
<dict> to </dict> and drop the plist envelope in that case.
- Validate locally with:
plutil -lint netbird-ios-appconfig.plist
Scope on iOS: these keys lock and reshape the app's interface, and
the app refuses configuration changes accordingly. Keys the tunnel
engine applies to the connection itself are not yet enforced on
iOS, because the engine runs in a Network Extension that does not
receive the app configuration. See the iOS section of
docs/src/pages/client/mdm-integration.mdx.
Keys with no iOS surface (allowServerSSH, disableServerRoutes,
disableAutostart, disableMetricsCollection, enableLocalMetrics,
localMetricsAddress, debugBundleUploadURL, splitTunnelMode,
splitTunnelApps) are parsed and ignored here, so one payload can be
shared with desktop devices.
blockInbound, wireguardPort and lazyConnection are left out of this
template on purpose: they are applied by the tunnel engine, which
on iOS does not receive this configuration yet. They do no harm in
a shared payload, they simply have no effect on an iOS device.
-->
<plist version="1.0">
<dict>
<!-- ===== Identity / auth ===== -->
<key>managementURL</key>
<string>https://api.netbird.io:443</string>
<!--
Pre-shared key: secret. Remove the entry entirely when not
used; do NOT leave an empty <string></string>, which the
client treats as a deliberate empty-PSK enforcement.
-->
<!--
<key>preSharedKey</key>
<string>REPLACE_ME</string>
-->
<!-- ===== Interface lockdown =====
disableUpdateSettings : read-only mode — nothing is hidden,
every configuration control is locked.
disableProfiles : hide the Profiles section (no create,
switch or remove).
disableNetworks : remove the Resources tab.
disableAdvancedView : hide Advanced (on tvOS, the whole
section). -->
<!--
<key>disableUpdateSettings</key>
<true/>
<key>disableProfiles</key>
<true/>
<key>disableNetworks</key>
<true/>
<key>disableAdvancedView</key>
<true/>
-->
<!-- ===== Connection controls =====
Each of these locks its own control in the app. -->
<!--
<key>disableAutoConnect</key>
<false/>
<key>disableClientRoutes</key>
<false/>
<key>rosenpassEnabled</key>
<true/>
<key>rosenpassPermissive</key>
<false/>
-->
<!-- ===== Remote jobs =====
Present at any value locks the client toggle, so pushing
false pins remote jobs off. -->
<!--
<key>allowRemoteJobs</key>
<false/>
-->
</dict>
</plist>
@@ -16,7 +16,8 @@
(confirm against the signed pkg before fleet roll-out)
Distribution:
- sign with `productsign --sign "Developer ID Installer: ..." ...`
- sign with `productsign`, passing your "Developer ID Installer"
certificate to its sign flag,
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`.
@@ -121,6 +122,15 @@
<false/>
-->
<!-- ===== Lazy connections (bool) =====
true forces them on, false forces them off, an
absent key defers to the Management setting.
NB_LAZY_CONN wins over both. -->
<!--
<key>lazyConnection</key>
<true/>
-->
<!-- ===== WireGuard UDP port (int) =====
Range 1-65535. Omit to keep the default. -->
<!--
@@ -128,6 +138,29 @@
<integer>51820</integer>
-->
<!-- ===== Remote jobs =====
allowRemoteJobs is off by default; present at any
value it locks the client toggle, so false pins
remote jobs off. debugBundleUploadURL overrides the
upload service for bundles produced by remote jobs
and must be an https URL with a host. -->
<!--
<key>allowRemoteJobs</key>
<false/>
<key>debugBundleUploadURL</key>
<string>https://uploads.example.com</string>
-->
<!-- ===== Local Prometheus metrics endpoint =====
Unrelated to disableMetricsCollection below, which
governs anonymous usage telemetry. -->
<!--
<key>enableLocalMetrics</key>
<true/>
<key>localMetricsAddress</key>
<string>127.0.0.1:9191</string>
-->
<!-- ===== Split tunnel (Android-only at the daemon level)
Pushed harmlessly on macOS for fleets with mixed
desktop+mobile devices; the macOS daemon ignores it. -->
@@ -138,7 +171,9 @@
<string>com.acme.app1,com.acme.app2</string>
-->
<!-- ===== UI / kill switches (bool) ===== -->
<!-- ===== UI / kill switches (bool) =====
disableAdvancedView is UI-only: it hides the
advanced section and rejects nothing. -->
<!--
<key>disableUpdateSettings</key>
<true/>
@@ -146,6 +181,8 @@
<true/>
<key>disableNetworks</key>
<true/>
<key>disableAdvancedView</key>
<true/>
<key>disableMetricsCollection</key>
<false/>
-->
+12
View File
@@ -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"
lazyConnection="$NULL" # "true"/"false"; absent defers to the Management setting
wireguardPort='51820'
allowRemoteJobs="$NULL" # present at any value locks the client toggle
debugBundleUploadURL="$NULL" # https URL with a host
enableLocalMetrics="$NULL" # local Prometheus endpoint; not the usage telemetry switch
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"
Binary file not shown.
+31
View File
@@ -66,6 +66,24 @@
<string id="DisableMetricsCollection_Name">Disable metrics collection</string>
<string id="DisableMetricsCollection_Help">When enabled, the client does not collect or report local usage metrics.</string>
<string id="DisableAdvancedView_Name">Disable advanced view</string>
<string id="DisableAdvancedView_Help">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.</string>
<string id="LazyConnection_Name">Lazy connections</string>
<string id="LazyConnection_Help">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.</string>
<string id="AllowRemoteJobs_Name">Allow remote jobs</string>
<string id="AllowRemoteJobs_Help">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.</string>
<string id="DebugBundleUploadURL_Name">Debug bundle upload URL</string>
<string id="DebugBundleUploadURL_Help">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.</string>
<string id="EnableLocalMetrics_Name">Enable local metrics endpoint</string>
<string id="EnableLocalMetrics_Help">Exposes the client's local Prometheus /metrics endpoint. Unrelated to "Disable metrics collection", which governs anonymous usage telemetry.</string>
<string id="LocalMetricsAddress_Name">Local metrics listen address</string>
<string id="LocalMetricsAddress_Help">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.</string>
</stringTable>
<presentationTable>
@@ -93,6 +111,19 @@
</textBox>
</presentation>
<presentation id="DebugBundleUploadURL_Pres">
<textBox refId="DebugBundleUploadURL_Text">
<label>Debug bundle upload URL:</label>
</textBox>
</presentation>
<presentation id="LocalMetricsAddress_Pres">
<textBox refId="LocalMetricsAddress_Text">
<label>Listen address:</label>
<defaultValue>127.0.0.1:9191</defaultValue>
</textBox>
</presentation>
</presentationTable>
</resources>
</policyDefinitionResources>
+74
View File
@@ -231,5 +231,79 @@
<disabledValue><decimal value="0" /></disabledValue>
</policy>
<policy name="DisableAdvancedView"
class="Machine"
displayName="$(string.DisableAdvancedView_Name)"
explainText="$(string.DisableAdvancedView_Help)"
key="Software\Policies\NetBird"
valueName="DisableAdvancedView">
<parentCategory ref="NetBird" />
<supportedOn ref="SUPPORTED_NetBird_All" />
<enabledValue><decimal value="1" /></enabledValue>
<disabledValue><decimal value="0" /></disabledValue>
</policy>
<policy name="LazyConnection"
class="Machine"
displayName="$(string.LazyConnection_Name)"
explainText="$(string.LazyConnection_Help)"
key="Software\Policies\NetBird"
valueName="LazyConnection">
<parentCategory ref="NetBird" />
<supportedOn ref="SUPPORTED_NetBird_All" />
<enabledValue><decimal value="1" /></enabledValue>
<disabledValue><decimal value="0" /></disabledValue>
</policy>
<policy name="AllowRemoteJobs"
class="Machine"
displayName="$(string.AllowRemoteJobs_Name)"
explainText="$(string.AllowRemoteJobs_Help)"
key="Software\Policies\NetBird"
valueName="AllowRemoteJobs">
<parentCategory ref="NetBird" />
<supportedOn ref="SUPPORTED_NetBird_All" />
<enabledValue><decimal value="1" /></enabledValue>
<disabledValue><decimal value="0" /></disabledValue>
</policy>
<policy name="DebugBundleUploadURL"
class="Machine"
displayName="$(string.DebugBundleUploadURL_Name)"
explainText="$(string.DebugBundleUploadURL_Help)"
key="Software\Policies\NetBird"
presentation="$(presentation.DebugBundleUploadURL_Pres)">
<parentCategory ref="NetBird" />
<supportedOn ref="SUPPORTED_NetBird_All" />
<elements>
<text id="DebugBundleUploadURL_Text" valueName="DebugBundleUploadURL" required="true" />
</elements>
</policy>
<policy name="EnableLocalMetrics"
class="Machine"
displayName="$(string.EnableLocalMetrics_Name)"
explainText="$(string.EnableLocalMetrics_Help)"
key="Software\Policies\NetBird"
valueName="EnableLocalMetrics">
<parentCategory ref="NetBird" />
<supportedOn ref="SUPPORTED_NetBird_All" />
<enabledValue><decimal value="1" /></enabledValue>
<disabledValue><decimal value="0" /></disabledValue>
</policy>
<policy name="LocalMetricsAddress"
class="Machine"
displayName="$(string.LocalMetricsAddress_Name)"
explainText="$(string.LocalMetricsAddress_Help)"
key="Software\Policies\NetBird"
presentation="$(presentation.LocalMetricsAddress_Pres)">
<parentCategory ref="NetBird" />
<supportedOn ref="SUPPORTED_NetBird_All" />
<elements>
<text id="LocalMetricsAddress_Text" valueName="LocalMetricsAddress" required="true" />
</elements>
</policy>
</policies>
</policyDefinitions>
+137 -15
View File
@@ -1,7 +1,7 @@
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 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, macOS, and iOS devices.";
# MDM Integration
@@ -14,8 +14,8 @@ you and the MDM provider; whatever you can push to that store (manually,
via Group Policy, via a Configuration Profile, via your MDM console)
becomes effective NetBird policy.
This page covers Windows and macOS. iOS and Android support is on the
roadmap.
This page covers Windows, macOS, and iOS (including tvOS). Android
support is on the roadmap.
## At a glance
@@ -23,11 +23,14 @@ roadmap.
| --- | --- | --- |
| 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` |
| iOS / tvOS | Managed App Configuration delivered to the app, bundle id `io.netbird.app` (tvOS: `io.netbird.app.tv`) | App configuration payload pushed by your MDM alongside the managed app — see [iOS](#i-os) for the current scope |
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.
Each backend is the de-facto convention for its platform (the registry
and managed-preferences shapes are what Chrome, Edge, Firefox, Zoom,
Tailscale, and Citrix Workspace use; Managed App Configuration is
Apple's standard channel for managed apps). Any MDM that supports the
platform also supports NetBird — there is no NetBird-specific
integration to build.
## How enforcement works
@@ -60,9 +63,12 @@ stale residue on the device.
## Policy keys reference
The same 16 keys apply on every platform. Names are camelCase in the
managed-configuration payload; the Windows ADMX template renders the
PascalCase variant in the Group Policy Editor — both are recognized.
The 23 keys below are recognized on every platform. A few of them have
no surface on some platforms; those are parsed and ignored there, so a
single cross-platform payload is always safe to ship. Names are
camelCase in the managed-configuration payload; the Windows ADMX
template renders the PascalCase variant in the Group Policy Editor —
both are recognized.
| Key | Type | Description |
| --- | --- | --- |
@@ -79,9 +85,14 @@ PascalCase variant in the Group Policy Editor — both are recognized.
| `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. |
| `enableLocalMetrics` | boolean | Expose the client's local Prometheus `/metrics` endpoint. |
| `localMetricsAddress` | string | Listen address of the local `/metrics` endpoint (default `127.0.0.1:9191`). |
| `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. |
| `disableAdvancedView` | boolean | Hide the advanced section of the client UI. UI-only: it is not part of the daemon configuration, so nothing is rejected on its account — the client just stops offering the section. |
| `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. |
| `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). |
@@ -111,7 +122,16 @@ PascalCase variant in the Group Policy Editor — both are recognized.
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.
pipeline is shipped yet. It is unrelated to `enableLocalMetrics`,
which exposes the client's own Prometheus endpoint.
- `disableAdvancedView` is UI-only: unlike the keys above it is not
stored in the daemon configuration and never causes a request to be
rejected. The daemon reports it to the client, which hides the
advanced section. Pushing it as `false` explicitly re-enables the
section on a client that hid it for another reason.
- `allowRemoteJobs` locks the corresponding client toggle whenever the
key is present, including when its value is `false` — so an admin can
pin remote jobs off and keep users from turning them on.
## Windows
@@ -335,10 +355,101 @@ 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.
## iOS
On iOS and tvOS there is no filesystem location for policy. Apple's
channel for a managed app is **Managed App Configuration**: the MDM
sends a dictionary together with the managed app, and iOS hands it to
that app only. NetBird reads it from the app's 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.
The app must be installed **as a managed app** by the same MDM that
sends the configuration. A copy the user installed from the App Store
themselves cannot receive an app configuration, and NetBird will run
with no policy on such a device.
### Pushing the payload
The payload is a plain property-list dictionary with the same key names
used on desktop:
```xml
<dict>
<key>managementURL</key>
<string>https://api.netbird.io:443</string>
<key>disableProfiles</key>
<true/>
<key>disableUpdateSettings</key>
<true/>
</dict>
```
Start from <a href="/docs-static/files/netbird-ios-appconfig.plist" download>netbird-ios-appconfig.plist</a> and delete the keys you do not want to
enforce. Where to put it, by provider:
- **Microsoft Intune**: **Apps → App configuration policies → Add →
Managed devices**, platform **iOS/iPadOS**, pick the NetBird app, then
**Configuration settings format → Enter XML data** and paste the
dictionary.
- **Jamf Pro**: **Devices → Mobile Device Apps → NetBird → App
Configuration**, 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 may be written as `<true/>`/`<false/>` or as `<integer>1</integer>`/`<integer>0</integer>`;
both are accepted. Integers and strings behave as on desktop.
### What each key does on iOS
<Warning>
The policy reaches the NetBird **app**, not the tunnel engine. On iOS
the engine runs in a Network Extension, a separate process with its own
preferences domain, which Apple's app-configuration channel does not
deliver to. So the keys below lock and reshape the interface, and the
app refuses configuration changes accordingly — but keys whose effect is
applied by the engine on the connection itself (`managementURL`,
`preSharedKey`, `rosenpassEnabled`, `rosenpassPermissive`,
`blockInbound`, `wireguardPort`, `lazyConnection`) are **not yet
enforced on the tunnel**. Treat iOS policy as interface lockdown today,
not as connection compliance. Mirroring the dictionary into the shared
app group so the extension can read it is tracked work.
</Warning>
| Key | Effect in the iOS / tvOS app |
| --- | --- |
| `managementURL` | Hides Settings → Connection and the onboarding server step; the profile-creation sheet shows and submits the enforced URL |
| `preSharedKey` | The key row reads *Configured* and is locked — no Save, no Remove. The value itself never crosses the bridge |
| `rosenpassEnabled` / `rosenpassPermissive` | Locks its own toggle and shows the enforced value |
| `disableClientRoutes` | Hides the exit-node selector; the resource list explains why instead of listing |
| `disableAutoConnect` | VPN On Demand becomes read-only |
| `disableProfiles` | Hides the Profiles section — no create, switch, or remove |
| `disableNetworks` | Removes the Resources tab |
| `disableAdvancedView` | Hides Advanced (on tvOS, the whole section) |
| `disableUpdateSettings` | Read-only mode: nothing is hidden, every configuration control is locked |
| `allowRemoteJobs` | Locks the remote debug bundle toggle |
The remaining keys have no iOS surface and are parsed and ignored:
`allowServerSSH`, `disableServerRoutes`, `disableAutostart`,
`disableMetricsCollection`, `enableLocalMetrics`, `localMetricsAddress`,
`debugBundleUploadURL`, `splitTunnelMode`, `splitTunnelApps`. They are
safe to leave in a payload shared with desktop devices.
### Verifying on a device
There is no CLI on iOS. A managed setting shows a lock and a **Managed
by your organization** note in the app; a gated section disappears
altogether. If neither happens, the app did not receive the
configuration — check that it is installed as a managed app, that the
app-configuration payload is assigned to the same device group, and that
the bundle id matches (`io.netbird.app`, or `io.netbird.app.tv` on
tvOS).
## Verifying enforcement
On any platform, the cleanest verification is the daemon's own debug
dump:
On Windows and macOS, the cleanest verification is the daemon's own
debug dump:
```bash
netbird debug config
@@ -373,6 +484,11 @@ Check that the daemon can see the source.
`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`.
- iOS / tvOS: nothing is locked in the app. The app configuration only
reaches an app the MDM itself installed, so confirm NetBird is a
managed app on that device, that the app-configuration payload is
assigned to the same group, and that its bundle id is
`io.netbird.app` (`io.netbird.app.tv` on tvOS).
**The key shows up in the registry / plist but not in `netbird debug
config` `mDMManagedFields`.**
@@ -393,9 +509,15 @@ The device is not MDM-enrolled. macOS protects
controls the directory. Enroll the device with a real MDM provider for
persistent rollouts.
**On iOS, the interface is locked but the connection ignores the policy.**
Expected today: the tunnel engine runs in a Network Extension that does
not receive the app configuration. See the warning under
[iOS](#what-each-key-does-on-i-os).
**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
Any MDM that can push a Configuration Profile on macOS, write a
registry value on Windows, or send an app configuration on iOS 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