Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/components/NavigationDocs.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,7 @@ export const docsNavigation = [
links: [
{ title: 'Profiles', href: '/client/profiles' },
{ title: 'Environment Variables', href: '/client/environment-variables' },
{ title: 'MDM Integration', href: '/client/mdm-integration' },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Run ESLint on the modified navigation file

npm run lint -- src/components/NavigationDocs.jsx

Repository: netbirdio/docs

Length of output: 2055


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show lint script
echo "## package.json lint script"
cat package.json | sed -n '1,120p' | rg -n '"lint"\s*:'

# List likely eslint config entrypoints
echo "## Possible eslint config files"
ls -la . | rg -i 'eslint|eslintrc|prettier' || true
fd -t f -i '*eslint*' -a 2>/dev/null . || true
fd -t f -i '*eslintrc*' -a 2>/dev/null . || true
fd -t f -i 'eslint.config.*' -a 2>/dev/null . || true

# Print eslint config package versions (package-lock/pnpm-lock may be absent; best-effort)
echo "## eslint/eslintrc versions (if present)"
cat package.json | rg -n 'eslint|eslintrc' || true

Repository: netbirdio/docs

Length of output: 501


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## package.json (relevant sections)"
cat package.json | sed -n '1,120p'

echo "## eslint.config.mjs (first 200 lines)"
sed -n '1,200p' eslint.config.mjs

echo "## run lint as-script (no extra args)"
npm run lint

Repository: netbirdio/docs

Length of output: 5272


🏁 Script executed:

npm run lint

Repository: netbirdio/docs

Length of output: 2021


Verify ESLint compliance.

npm run lint (eslint src/) currently fails with ESLint: 9.39.4 and TypeError: Converting circular structure to JSON while loading next/core-web-vitals from eslint.config.mjs, so linting can’t be used to confirm this change is clean. Fix the ESLint config/tooling crash, then rerun npm run lint.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/NavigationDocs.jsx` at line 749, ESLint is crashing when
loading the next/core-web-vitals config due to a circular structure in the
exported config; open eslint.config.mjs and remove any direct require/import
that injects complex module objects into the exported config (e.g., spreading a
require(...) result or exporting non-plain objects), and instead reference the
Next config by name in a plain extends array (use extends:
['next/core-web-vitals'] or the equivalent flat-config entry) so the exported
default is a simple serializable object; also ensure you aren’t exporting
functions or Next internals and that eslint-config-next / eslint-plugin-next
versions are compatible, then rerun npm run lint to confirm the crash is
resolved.

Source: Coding guidelines

{
title: 'Settings',
isOpen: false,
Expand Down
390 changes: 390 additions & 0 deletions src/pages/client/mdm-integration.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,390 @@
import { Note, Warning } from "@/components/mdx";

export const description =
"Enforce NetBird client configuration through your MDM channel (Intune, Jamf, Kandji, Mosyle, Workspace ONE, JumpCloud, Group Policy). Push management URL, kill switches, and other settings to a fleet of Windows and macOS devices.";

# MDM Integration

NetBird's client honors policies pushed by your Mobile Device Management
(MDM) channel, so an administrator can enforce configuration across a
fleet of devices instead of touching each machine. On every supported
platform the daemon reads from the **OS-native managed-configuration
store** that your MDM already writes to. No agent of ours sits between
you and the MDM provider; whatever you can push to that store (manually,
via Group Policy, via a Configuration Profile, via your MDM console)
becomes effective NetBird policy.

This page covers Windows and macOS. iOS and Android support is on the
roadmap.

## At a glance

| Platform | Where NetBird reads policy | How an admin writes it |
| --- | --- | --- |
| Windows | `HKLM\Software\Policies\NetBird` (registry) | Group Policy (ADMX) · Intune ADMX ingestion or OMA-URI · `reg import` · MDM-vendor scripts |
| macOS | `/Library/Managed Preferences/io.netbird.client.plist` | Configuration Profile (`.mobileconfig`) pushed by your MDM, targeting bundle id `io.netbird.client` |

Both backends are the de-facto convention for desktop apps (the same
shape Chrome, Edge, Firefox, Zoom, Tailscale, Citrix Workspace, and
others use). Any MDM that supports the platform also supports NetBird —
there is no NetBird-specific integration to build.

## How enforcement works

When NetBird starts, and every minute while it runs, the daemon:

1. Reads the platform-native managed-configuration store.
2. Merges the values **on top of every other configuration layer**
(defaults → on-disk profile → environment variables → CLI/UI input →
**MDM**). MDM always wins.
3. Locks any field that came from the MDM source. Attempts to change
that field from the GUI, the CLI (`netbird up --flag=...`, `netbird
login --flag=...`) or via direct gRPC are rejected with a clear
error listing the locked fields. The client UI greys these fields
out and tags them with **(MDM)** so the user knows they cannot
change them.
4. If the MDM payload changes (admin pushes new values), the change
takes effect within ~1 minute on the device — no client restart
needed.

<Note>
MDM is **authoritative**. When a key is present in the MDM payload, the
MDM value wins regardless of whether it is `true` or `false`. An admin
pushing `disableNetworks=false` via MDM re-enables the feature even on a
host that was installed with `--disable-networks`. The MDM payload is
the source of truth as long as the policy is in place. MDM-supplied
values are also never written back to the on-disk profile, so removing
the policy at the MDM side takes effect on the next reload with no
stale residue on the device.
</Note>

## Policy keys reference

The same 16 keys apply on every platform. Names are camelCase in the
managed-configuration payload; the Windows ADMX template renders the
PascalCase variant in the Group Policy Editor — both are recognized.

| Key | Type | Description |
| --- | --- | --- |
| `managementURL` | string | Override the management server URL (e.g. `https://api.netbird.io:443` or a self-hosted URL). |
| `preSharedKey` | string | WireGuard pre-shared key. Treated as secret and redacted in logs. |
| `wireguardPort` | integer | UDP port the local WireGuard interface binds to. Range `1–65535`. |
| `allowServerSSH` | boolean | Allow the embedded NetBird SSH server on this peer. |
| `disableAutoConnect` | boolean | Skip auto-connecting on startup; require an explicit `netbird up`. |
| `rosenpassEnabled` | boolean | Turn on the post-quantum Rosenpass key exchange. |
| `rosenpassPermissive` | boolean | Permissive mode for Rosenpass (interop with non-Rosenpass peers). |
| `blockInbound` | boolean | Drop all inbound traffic except established/related — kill-switch style. |
| `disableClientRoutes` | boolean | This peer does not route traffic to other peers. |
| `disableServerRoutes` | boolean | This peer is not a router for others. |
| `disableMetricsCollection` | boolean | Disable anonymous usage telemetry. |
| `disableUpdateSettings` | boolean | Block every configuration change from UI or CLI on this device (read-only mode). |
| `disableProfiles` | boolean | Hide the profile menu in the GUI and reject profile CRUD via CLI. |
| `disableNetworks` | boolean | Hide the Networks / Exit Node menus in the GUI and reject the related RPCs. |
| `splitTunnelMode` | string | `allow` or `disallow` — split-tunnel policy mode (Android only at the client level; harmless on desktop). |
| `splitTunnelApps` | string | Comma-separated list of package names that the split-tunnel mode applies to (Android only). |

### Notes on a few keys

- `disableUpdateSettings` and `disableProfiles` overlap with the
service-install CLI flags `--disable-update-settings` and
`--disable-profiles`. Either source can disable the feature; the MDM
value wins when present.
- `disableUpdateSettings` keeps the Settings view in the GUI visible
(so users can inspect current values) but rejects every attempt to
save changes. Use it for read-only fleets.
- `splitTunnelMode` and `splitTunnelApps` are wired into Android's
`VpnService.Builder.addAllowedApplication()` flow; on Windows and
macOS the daemon parses the keys but ignores them. They are safe to
ship in a cross-platform payload.
- The `disableMetricsCollection` key is reserved for an upcoming
metrics integration; the client recognizes it today but no metrics
pipeline is shipped yet.

## Windows

The NetBird daemon reads policies from
`HKLM\Software\Policies\NetBird`. Anything that ends up under that
registry key — through whichever delivery channel — becomes policy.
The shapes are:

| Value name | Registry type | Example |
| --- | --- | --- |
| `ManagementURL`, `PreSharedKey`, `SplitTunnelMode`, `SplitTunnelApps` | `REG_SZ` | `"https://api.netbird.io:443"` |
| All `Disable*` flags, `AllowServerSSH`, `RosenpassEnabled`, `RosenpassPermissive` | `REG_DWORD` (0 / 1) | `0x00000001` |
| `WireguardPort` | `REG_DWORD` | `0x0000ca6c` (51820 decimal) |

Choose one of the delivery channels below. All four converge on the
same registry key.

### Group Policy (on-prem AD / local gpedit)

1. Copy the ADMX/ADML files into the system Policy Definitions store:
- Place `netbird.admx` in `C:\Windows\PolicyDefinitions\`.
- Place `netbird.adml` in `C:\Windows\PolicyDefinitions\en-US\`.
2. Open `gpedit.msc` (or the AD Group Policy Management Editor).
3. Navigate to **Computer Configuration → Administrative Templates →
NetBird**.
4. Edit any policy (e.g. **Management URL**), set it to **Enabled**
with the desired value, and click **OK**.
5. Run `gpupdate /force` on each target device (or wait for the
periodic refresh).
6. Verify with `reg query HKLM\Software\Policies\NetBird` — the values
you set should appear there.

The ADMX template is shipped in the NetBird repo at
`docs/netbird.admx` / `docs/netbird.adml`.

### Microsoft Intune (ADMX ingestion)

Recommended for cloud-managed Windows fleets.

1. In the Intune admin center, go to **Devices → Configuration → Import
ADMX**, upload `netbird.admx` together with `netbird.adml`. Wait for
the **Available** status.
2. Create a new **Configuration Profile → Templates → Imported
Administrative templates → NetBird**.
3. Configure the policies you want to enforce.
4. Assign the profile to your device group(s) and save.

Devices pick up the policy on the next Intune sync (typically within
8 hours, sooner if you trigger a manual sync from the device). The
values end up in `HKLM\Software\Policies\NetBird`.

### Microsoft Intune (custom OMA-URI)

If you cannot ingest the ADMX template, you can push individual values
via OMA-URI under
`./Device/Vendor/MSFT/Policy/ConfigOperations/ADMXInstall/...` or via
the Registry CSP at
`./Device/Vendor/MSFT/Registry/HKEY_LOCAL_MACHINE/Software/Policies/NetBird/<ValueName>`.
ADMX ingestion is simpler and gives admins the same UI as on-prem GPO,
so prefer that.

### `.reg` import (single source of truth)

For fleets without an MDM, or as a quick-test path, you can carry the
whole policy in a single `.reg` file:

1. Configure the policy values on a reference machine (via `gpedit` or
`reg add`).
2. Export the key:
```
reg export "HKLM\Software\Policies\NetBird" netbird-policy.reg /y
```
3. Distribute the resulting file and apply with:
```
reg import netbird-policy.reg
```

A sample is in the NetBird repo at `docs/netbird-policy.reg`.

### JumpCloud

NetBird ships a JumpCloud companion script at
`docs/netbird-policy.reg.ps1`. To use it:

1. In the JumpCloud admin console, go to **Device Management →
Commands → +**.
2. Type: **Windows PowerShell**. Run as: **SYSTEM**.
3. Paste `netbird-policy.reg.ps1` verbatim into the command body.
4. In the same command, attach the `netbird-policy.reg` file you
produced above. JumpCloud copies attached files into the command's
working directory before invoking the script.
5. Bind the command to the target system group and run it.

The script wipes the existing `HKLM\Software\Policies\NetBird` key
before importing the `.reg`, so the `.reg` is the **single source of
truth** for that device. To unset all policy, attach an empty (header-
only) `.reg`; the daemon will pick up the absence on the next reload.

Comment on lines +119 to +199

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify that the referenced Windows documentation files exist

echo "Checking for Windows MDM documentation files..."
for file in "docs/netbird.admx" "docs/netbird.adml" "docs/netbird-policy.reg" "docs/netbird-policy.reg.ps1"; do
  if [ -f "$file" ]; then
    echo "✓ Found: $file"
  else
    echo "✗ Missing: $file"
  fi
done

Repository: netbirdio/docs

Length of output: 238


🏁 Script executed:

#!/bin/bash
# Search for the referenced files anywhere in the repo (in case they moved under different paths)
echo "Searching for referenced Windows MDM documentation files by name..."
for name in "netbird.admx" "netbird.adml" "netbird-policy.reg" "netbird-policy.reg.ps1"; do
  echo "---- $name ----"
  fd -a "$name" . || true
done

Repository: netbirdio/docs

Length of output: 234


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Checking under docs/ for similarly-named files..."
find . -type f \( \
  -iname "*netbird*.admx*" -o \
  -iname "*netbird*.adml*" -o \
  -iname "*netbird*policy*.reg*" -o \
  -iname "*netbird*policy*.ps1*" \
\) | sed 's|^\./||' | sort

echo
echo "Exact-path checks (as referenced in MDX):"
for file in "docs/netbird.admx" "docs/netbird.adml" "docs/netbird-policy.reg" "docs/netbird-policy.reg.ps1"; do
  if [ -f "$file" ]; then
    echo "✓ Found: $file"
  else
    echo "✗ Missing: $file"
  fi
done

Repository: netbirdio/docs

Length of output: 283


Fix broken file references in mdm-integration.mdx (missing docs/ assets)
All referenced files are missing from the repository at the stated locations: docs/netbird.admx, docs/netbird.adml, docs/netbird-policy.reg, docs/netbird-policy.reg.ps1 (no similarly-named matches found under docs/). Update the links to the correct paths or add the referenced files.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/client/mdm-integration.mdx` around lines 119 - 199, The MDX
references to docs assets (netbird.admx, netbird.adml, netbird-policy.reg,
netbird-policy.reg.ps1) are broken in src/pages/client/mdm-integration.mdx;
either add the missing files to the docs/ directory with those exact names or
update the links in mdm-integration.mdx to point to the actual locations (e.g.,
repo paths that currently contain the ADMX/ADML and .reg/.ps1 samples);
specifically check and fix occurrences of "docs/netbird.admx",
"docs/netbird.adml", "docs/netbird-policy.reg", and
"docs/netbird-policy.reg.ps1" so they resolve to existing files in the repo.

## macOS

The NetBird daemon reads policy from
`/Library/Managed Preferences/io.netbird.client.plist`. macOS writes
that file when an MDM provider pushes a Configuration Profile whose
`com.apple.ManagedClient.preferences` payload targets the bundle id
`io.netbird.client`.

<Warning>
macOS wipes the contents of `/Library/Managed Preferences/` on every
boot if the device is not MDM-enrolled. Manual `defaults write` works
for a quick test but does not survive a reboot on an un-enrolled Mac.
Use a real MDM channel for production rollouts.
</Warning>

### Custom Configuration Profile (recommended)

This is the canonical macOS path and works with every MDM
(Jamf, Kandji, Mosyle, Microsoft Intune for Mac, Workspace ONE,
JumpCloud, Apple Configurator 2, etc.).

1. Start from the template at `docs/netbird-macos.mobileconfig` in the
NetBird repo. 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 `docs/io.netbird.client.plist` from the NetBird repository
as the plist payload. Edit the file before upload to enable just
the keys you want to enforce — leave the rest commented out.
4. Bind the policy to the target Device Group and save.

Notes:

- JumpCloud's **MDM Custom Configuration Profile** accepts a bare
managed-preferences plist (the inner Apple managed-prefs dictionary)
— **not** a full `.mobileconfig` envelope. Uploading
`netbird-macos.mobileconfig` will be rejected. Use the bare
`io.netbird.client.plist` for this code path; reserve
`netbird-macos.mobileconfig` for other MDMs that expect the full
Configuration Profile shape.
- Keep the filename as `io.netbird.client.plist`. The Apple
convention for managed-preferences plists is
`<bundle.id>.plist` (this is how macOS materializes the file at
`/Library/Managed Preferences/<bundle.id>.plist`), and JumpCloud's
policy form does not currently expose a separate bundle-identifier
field — keeping the canonical filename is the safest path. If your
JumpCloud console version surfaces a bundle-id / preference-domain
field elsewhere in the policy wizard, set it to `io.netbird.client`
too.

JumpCloud wraps the plist into an Apple Configuration Profile and
pushes it via the MDM channel. The OS materializes the file at
`/Library/Managed Preferences/io.netbird.client.plist`, where the
NetBird daemon picks it up within the next 1-minute reload tick.
Removing the policy from JumpCloud removes the file on the next sync,
which un-locks the corresponding fields on the client.

#### Shell Command (no MDM enrollment required)

If your fleet is JumpCloud-managed but not MDM-enrolled, NetBird ships
a companion script at `docs/netbird-macos.sh`. It is the macOS
Comment on lines +221 to +306

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify that the referenced macOS documentation files exist

echo "Checking for macOS MDM documentation files..."
for file in "docs/netbird-macos.mobileconfig" "docs/io.netbird.client.plist" "docs/netbird-macos.sh"; do
  if [ -f "$file" ]; then
    echo "✓ Found: $file"
  else
    echo "✗ Missing: $file"
  fi
done

Repository: netbirdio/docs

Length of output: 220


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locating referenced files by name (any path)..."
for f in "netbird-macos.mobileconfig" "io.netbird.client.plist" "netbird-macos.sh"; do
  echo "---- $f ----"
  fd -a "$f" . || true
done

echo
echo "Checking for likely folder under docs/ that contains macOS MDM templates..."
ls -la docs || true

Repository: netbirdio/docs

Length of output: 454


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Searching entire repo for the referenced filenames (any path)..."
rg -n --hidden --no-ignore-vcs -S --files | rg -i 'netbird-macos\.mobileconfig|io\.netbird\.client\.plist|netbird-macos\.sh' || true

echo
echo "Searching for likely related patterns..."
rg -n --hidden --no-ignore-vcs -S --files | rg -i 'netbird-macos|managed preferences|io\.netbird\.client' || true

echo
echo "Listing contents of docs/integrations..."
ls -Rla docs/integrations || true

Repository: netbirdio/docs

Length of output: 888


Fix broken macOS template references in src/pages/client/mdm-integration.mdx

The MDX instructions reference the following files under docs/, but none of them exist in this repository:

  • docs/netbird-macos.mobileconfig
  • docs/io.netbird.client.plist
  • docs/netbird-macos.sh

Add these templates to the repo or update the MDX steps to point to the correct existing paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/client/mdm-integration.mdx` around lines 221 - 306, The MDX page
src/pages/client/mdm-integration.mdx references three non-existent template
files (docs/netbird-macos.mobileconfig, docs/io.netbird.client.plist,
docs/netbird-macos.sh); either add the missing template files under docs/ with
the expected names and contents, or update the references in mdm-integration.mdx
to point to the actual existing filenames/paths in the repo (and adjust the
JumpCloud instructions to reference the correct plist name if different); ensure
the chosen fix is consistent across the text (all mentions of
netbird-macos.mobileconfig, io.netbird.client.plist, and netbird-macos.sh) and
confirm the site builds with the updated links.

counterpart of the Windows `.reg.ps1` script — same fleet, different
backend:

1. Edit the `### POLICY VALUES ###` block at the top of the script;
set the variables for the keys you want to enforce and leave the
rest at `$NULL`.
2. In the JumpCloud admin console, go to **Device Management →
Commands → +**. Type: **Mac, Shell**. Run as: **root**.
3. Paste the edited script verbatim into the command body.
4. Bind to the target system group and run.

The script writes
`/Library/Managed Preferences/io.netbird.client.plist`, sets ownership
to `root:wheel` with mode `644`, and kicks the NetBird daemon so the
change applies immediately. On MDM-enrolled devices the file survives
reboots; on un-enrolled devices the file is wiped at the next reboot
(macOS-imposed). Prefer the Custom Mac Application Settings policy
above when the fleet is enrolled.

## Verifying enforcement

On any platform, the cleanest verification is the daemon's own debug
dump:

```bash
netbird debug config
```

The response includes a `mDMManagedFields` array that lists every key
the daemon is currently honoring from the MDM source. If a key you
expected to be locked is missing from that array, the MDM payload did
not reach the device (or used a value name the daemon does not
recognize).

The client UI mirrors the same state: any submenu item, settings field,
or kill switch driven by MDM appears greyed out with a **(MDM)** tag
next to its label.

Daemon logs (`/var/log/netbird/client.log` on Linux/macOS,
`%ProgramData%\Netbird\` on Windows) contain a one-line
`MDM enrolled with N managed key(s): [...]` entry on every reload, plus
one `MDM override <key> = <value>` line per applied key. Secrets are
redacted.

## Troubleshooting

**The policy did not apply at all.**
Check that the daemon can see the source.

- Windows: `reg query HKLM\Software\Policies\NetBird` — if empty, the
delivery channel did not write the values. Check `gpresult /h` for
GPO failures or the Intune sync status in **Settings → Accounts →
Access work or school → Info → Sync**.
- macOS:
`sudo defaults read /Library/Managed\ Preferences/io.netbird.client`
— if the file is missing, the MDM payload was not pushed or the
bundle id in the profile does not match `io.netbird.client`.

**The key shows up in the registry / plist but not in `netbird debug
config` `mDMManagedFields`.**
The value name is misspelled. Names are case-insensitive but must match
one of the keys in the reference table above. The daemon log emits an
`MDM ignoring unknown <key>` warning when this happens.

**The user can still change the field from the GUI / CLI.**
The change is being rejected by the daemon but the UI may not have
caught up yet. The UI refreshes within a couple of seconds after a
config change; try closing and reopening the Settings window. If the
change actually sticks, double-check that the MDM payload is still
present on the device — it may have been removed by another policy.

**On macOS, the file disappears after a reboot.**
The device is not MDM-enrolled. macOS protects
`/Library/Managed Preferences/` by wiping it at boot if no MDM
controls the directory. Enroll the device with a real MDM provider for
persistent rollouts.

**My MDM provider is not in the list above.**
Any MDM that can push a Configuration Profile on macOS or write a
registry value on Windows works. The mechanism is OS-native, not
NetBird-specific. If you hit a quirk specific to your provider, please
open an issue at
https://github.com/netbirdio/netbird/issues with the provider name and
what you observed.
Loading