[management,client] Gate remote jobs behind an admin opt-in with MDM support (#7153)

This introduces a disabled-by-default allow-remote-jobs setting that
controls whether the management server may run jobs (such as debug
bundles) on a peer. The flag propagates end to end: through client
configuration, the daemon SetConfig and Login requests, authentication,
and system info, up to management, where it is stored on the peer and
exposed on the peers API as remote_jobs_allowed. The client refuses any
management-requested job unless the peer has opted in. Because enabling
remote jobs crosses the user-to-root boundary, turning it on requires
privilege, mirroring the SSH-server gate. Administrators can enforce the
setting through MDM policy on both macOS and Windows, and MDM can also
override the debug-bundle upload URL. The change ships policy
documentation and generated profile templates, and adds configuration,
conflict, and enforcement tests covering the opt-in, privilege, and MDM
paths.
This commit is contained in:
Maycon Santos
2026-09-01 17:53:41 +02:00
committed by GitHub
parent 3027130f0f
commit ebc259e30b
39 changed files with 1770 additions and 1025 deletions

View File

@@ -85,6 +85,21 @@
<false/>
-->
<!-- ===== Remote jobs (debug bundles) =====
allowRemoteJobs : opt this device into management-requested
remote jobs (e.g. debug bundles). Off by
default; enabling is a privileged change.
debugBundleUploadURL : override the debug-bundle upload service URL
for remote jobs (https URL with a host). Takes
precedence over the management-supplied value. -->
<!--
<key>allowRemoteJobs</key>
<true/>
<key>debugBundleUploadURL</key>
<string>https://upload.example.com</string>
-->
<!-- ===== WireGuard UDP port =====
Range 1-65535. Omit to keep the daemon default. -->
<!--

View File

@@ -121,6 +121,19 @@
<false/>
-->
<!-- ===== Remote jobs (debug bundles) =====
allowRemoteJobs : opt into management-requested
remote jobs. Off by default.
debugBundleUploadURL : override the debug-bundle upload
service (https URL with a host);
precedence over the management value. -->
<!--
<key>allowRemoteJobs</key>
<true/>
<key>debugBundleUploadURL</key>
<string>https://upload.example.com</string>
-->
<!-- ===== WireGuard UDP port (int) =====
Range 1-65535. Omit to keep the default. -->
<!--

View File

@@ -36,7 +36,9 @@
# IDEMPOTENCY: re-running with the same values is a no-op from the
# daemon's point of view (the 1-minute reload ticker diff returns empty).
#
# SECURITY: PreSharedKey is redacted in this script's log output.
# SECURITY: PreSharedKey (and any secret-bearing debugBundleUploadURL) is
# redacted in this script's log output, and the installed plist is 0600
# root:wheel so its values are not readable by local non-root users.
set -euo pipefail
@@ -56,6 +58,8 @@ NULL='__UNSET__'
managementURL='https://api.netbird.io:443'
preSharedKey="$NULL" # secret; redacted in log
allowServerSSH='true'
allowRemoteJobs="$NULL"
debugBundleUploadURL="$NULL" # HTTPS URL with a host; overrides management
blockInbound="$NULL"
disableAutoConnect="$NULL"
disableAutostart="$NULL"
@@ -107,21 +111,35 @@ end_plist() {
EOF
}
# emit_string appends a plist `<key>`/`<string>` entry for the given key and value to "$PLIST_PATH.tmp", XML-escaping `&`, `<`, and `>`, and logs the assignment (masking the logged value as `********** (secret)` when the key is `preSharedKey`).
# emit_string appends a plist `<key>`/`<string>` entry for the given key and value to "$PLIST_PATH.tmp", XML-escaping `&`, `<`, and `>`, and logs the assignment (masking the logged value as `********** (secret)` for secret keys — `preSharedKey` and `debugBundleUploadURL`, which can embed credentials or a signed query token).
emit_string() {
local key="$1" value="$2" log_value="$2"
# Escape XML entities in the value
local escaped
escaped="$(printf '%s' "$value" | sed -e 's/&/\&amp;/g' -e 's/</\&lt;/g' -e 's/>/\&gt;/g')"
printf ' <key>%s</key>\n <string>%s</string>\n' "$key" "$escaped" >> "$PLIST_PATH.tmp"
if [[ "$key" == "preSharedKey" ]]; then
log_value='********** (secret)'
fi
case "$key" in
preSharedKey|debugBundleUploadURL) log_value='********** (secret)' ;;
*) ;;
esac
log "set $key = $log_value"
}
# emit_bool writes a boolean plist entry for a given key into the temporary plist file.
# emit_bool writes a boolean plist entry for a key when the provided value matches an accepted boolean token; logs an error and skips the key on invalid input.
# is_bool returns success if the value is an accepted boolean token.
is_bool() {
local value="$1"
case "$value" in
true|True|TRUE|1|yes|false|False|FALSE|0|no) return 0 ;;
*) return 1 ;;
esac
}
# emit_bool writes a boolean plist entry for a key when the provided value matches
# an accepted boolean token; logs an error and skips the key on invalid input.
# It returns success even on invalid input (like emit_int) so a single typo in one
# boolean does not abort the whole policy push under `set -euo pipefail`. Callers
# that must fail closed on an invalid value (e.g. allowRemoteJobs) validate with
# is_bool before calling and substitute a safe default themselves.
emit_bool() {
local key="$1" value="$2"
local xml_bool
@@ -145,15 +163,35 @@ emit_int() {
log "set $key = $value"
}
# main builds the NetBird MDM plist from configured policy variables, validates and installs it to /Library/Managed Preferences/io.netbird.client.plist (root:wheel, 644) and optionally triggers the NetBird daemon to reload.
# main builds the NetBird MDM plist from configured policy variables, validates and installs it to /Library/Managed Preferences/io.netbird.client.plist (root:wheel, 600 — the daemon reads it directly as root, so it need not be world-readable) and optionally triggers the NetBird daemon to reload.
main() {
log "applying NetBird MDM policy to $PLIST_PATH"
# Restrict the temp plist while it is being built: it carries the same
# secret-bearing values as the final file, which is installed 0600 below.
umask 077
/bin/mkdir -p "$PLIST_DIR"
start_plist
# Force 0600 on the temp file explicitly: start_plist writes it with a
# truncating redirect, which keeps an existing file's mode, so a leftover
# 0644 tmp from an interrupted run would not be tightened by umask alone.
# start_plist only wrote the header so far — the secret-bearing values are
# appended after this point.
/bin/chmod 600 "$PLIST_PATH.tmp"
is_set "$managementURL" && emit_string managementURL "$managementURL"
is_set "$preSharedKey" && emit_string preSharedKey "$preSharedKey"
is_set "$allowServerSSH" && emit_bool allowServerSSH "$allowServerSSH"
# Fail closed: an invalid allowRemoteJobs value must not drop the key and
# leave a conflicting local opt-in active — enforce the safe default (false).
if is_set "$allowRemoteJobs"; then
if is_bool "$allowRemoteJobs"; then
emit_bool allowRemoteJobs "$allowRemoteJobs"
else
log "invalid boolean for allowRemoteJobs: $allowRemoteJobs; enforcing safe default (false)"
emit_bool allowRemoteJobs false
fi
fi
is_set "$debugBundleUploadURL" && emit_string debugBundleUploadURL "$debugBundleUploadURL"
is_set "$blockInbound" && emit_bool blockInbound "$blockInbound"
is_set "$disableAutoConnect" && emit_bool disableAutoConnect "$disableAutoConnect"
is_set "$disableAutostart" && emit_bool disableAutostart "$disableAutostart"
@@ -181,7 +219,12 @@ main() {
/bin/mv -f "$PLIST_PATH.tmp" "$PLIST_PATH"
/usr/sbin/chown root:wheel "$PLIST_PATH"
/bin/chmod 644 "$PLIST_PATH"
# 0600, not 0644: the daemon's loader (client/mdm/policy_darwin.go) opens the
# plist directly as root, so it does not need to be world-readable. Restricting
# it keeps secret-bearing values (preSharedKey, a signed debugBundleUploadURL)
# from any local non-root user. The loader's only mode check refuses a
# world-writable file, which 0600 satisfies.
/bin/chmod 600 "$PLIST_PATH"
log "policy installed; NetBird daemon will pick it up within the next 1-minute reload tick"

Binary file not shown.

View File

@@ -39,6 +39,12 @@
<string id="AllowServerSSH_Name">Allow server SSH</string>
<string id="AllowServerSSH_Help">When enabled, this client accepts incoming SSH sessions via NetBird SSH. Equivalent to --allow-server-ssh.</string>
<string id="AllowRemoteJobs_Name">Allow remote jobs</string>
<string id="AllowRemoteJobs_Help">When enabled, this client accepts management-requested remote jobs (e.g. debug bundles). Off by default. Equivalent to --allow-remote-jobs.</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 with a host.</string>
<string id="RosenpassEnabled_Name">Enable Rosenpass</string>
<string id="RosenpassEnabled_Help">Enables Rosenpass post-quantum key exchange on WireGuard tunnels. Both peers must support it.</string>
@@ -79,6 +85,12 @@
</textBox>
</presentation>
<presentation id="DebugBundleUploadURL_Pres">
<textBox refId="DebugBundleUploadURL_Text">
<label>Debug bundle upload URL:</label>
</textBox>
</presentation>
<presentation id="PreSharedKey_Pres">
<textBox refId="PreSharedKey_Text">
<label>Pre-shared key:</label>

View File

@@ -124,6 +124,31 @@
<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="false" />
</elements>
</policy>
<policy name="RosenpassEnabled"
class="Machine"
displayName="$(string.RosenpassEnabled_Name)"