mirror of
https://github.com/netbirdio/docs.git
synced 2026-10-09 15:19:04 +02:00
* 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. * docs: iOS enforces every key; document when a change reaches the tunnel The iOS section was written while the policy stopped at the app: the network extension that runs the engine has its own preferences domain, and Apple's app-configuration channel does not deliver there. The app now mirrors the configuration into the shared App Group and tells the extension when it changes, so the keys the engine applies to the connection are enforced too. What replaces the old warning is a timing caveat, which is what an admin actually needs in order to plan a rollout. The hand-off happens only while the app is running: in the foreground a change reaches the tunnel within about 30 seconds or immediately on activation, but iOS suspends a backgrounded app, so a policy pushed — or withdrawn — in that window applies the next time the app is opened. A tunnel brought up by VPN On Demand or the widget therefore runs on the last policy the app saw. tvOS keeps the old behaviour and now says so: the hand-off needs a shared app group, which does not work between the tvOS app and its extension, so only the interface is enforced there. Also corrected: blockInbound, wireguardPort and lazyConnection are no longer listed as having no effect but as having no control to lock, and debugBundleUploadURL moves out of the ignored list, since the engine applies it when a remote job produces a bundle. The app-config template carries the same three keys and the same timing note. * docs: list allowRemoteJobs among the keys iOS enforces without a control The key is enforced on iOS like the other engine-applied ones: a peer whose policy withholds it refuses management-requested jobs. What is not there yet is the lock on the Troubleshoot toggle, which arrives with the iOS client PR that introduces that toggle — so promising it in the table of what the user sees would describe a screen that does not exist. Moved to the group of keys the engine applies with nothing on screen to mark, next to blockInbound, wireguardPort, lazyConnection and debugBundleUploadURL. It moves back to the table once the toggle ships. * docs: sign macOS profiles with CMS, and scope the iOS claims properly Three review findings, all correct. `productsign` signs installer packages, not configuration profiles, and an Apple Developer ID certificate cannot sign a profile at all — Apple's own guidance is that a `.mobileconfig` is a CMS signed-data message. The instruction predates this branch but the commit that fixed the malformed XML comment rewrote it, so it is fixed here rather than carried forward. It was also misleading in a second way: a profile delivered through an MDM is signed by the MDM channel, so a managed rollout has nothing to sign by hand. The step now says that, and gives `security cms -S` for the case where the profile is handed out some other way. "Every key is enforced on iOS" contradicted the list of platform-inapplicable keys three paragraphs below it; it now reads "every key that applies to the platform", and points at that list. The engine-applied group — blockInbound, wireguardPort, lazyConnection, allowRemoteJobs, debugBundleUploadURL — was stated without qualification under a table headed "iOS / tvOS", while the warning just above says the tvOS engine never receives the policy. Marked as iOS, with the tvOS case pointed back at that warning. * docs: group MDM policy keys by job and split delivery into per-platform pages Rewrite /client/mdm-integration as the concept page and key reference: a running example (a three-key baseline), the "pinned switches" model (present means pinned, even as false), and the 23 keys grouped by the job they do, each with its own anchor and the mistake it invites. Correct claims that disagree with the client source (v0.80.0): - disableClientRoutes stops the device using routes (resources, exit nodes); it was described as not routing for others - blockInbound also stops the SSH server and routing; not a kill switch - since v0.75.0 the app hides managed settings, no "(MDM)" tag - a policy change restarts the connection - splitTunnel* have no effect today (Android has no MDM channel yet) - managementURL pins the server, not the account, on NetBird Cloud - the pre-shared key is readable by local users on Windows and macOS Move Windows, macOS and iOS delivery into their own pages under MDM Deployment, add them to the nav, and point inbound links at the new per-key anchors. * docs: fix MDM accuracy points found in review - integer booleans are ignored on macOS (plist decodes as uint64); say so - disableAutostart and disableAdvancedView lock nothing; qualify the lock claim - managementURL: drop the disableProfiles advice, no key pins the account - drop the unverifiable restart duration and a disableAdvancedView claim - macOS page: fix 'never write the file' vs the shell-script channel - Windows page: warn that the sample .reg pins every key - tighten wording * docs: say what disableAdvancedView hides (Peers and Resources tabs) * docs: make clear disableNetworks removes the choice, not the networks * docs: add recommended MDM policies by device role * docs: lazyConnection follows the account setting, on by default since v0.74.0 * docs: drop the open-an-issue line from MDM troubleshooting * docs: make remote jobs optional for every device role * docs: make disableNetworks and disableAdvancedView optional for laptops * docs: rename Recap to TL;DR on the MDM pages * docs: TL;DR on the main MDM page only, drop the lead-in * docs: list MDM Integration first under MDM Deployment in the nav * docs: second review pass on the MDM pages - macOS integer booleans are locked as well as unapplied; say so - the lock-nothing keys are not desktop-only (disableAdvancedView on iOS) - drop Linux from disableAutostart (no MDM channel there) - blockInbound: drop the redundant routed-network clause - encryption intro: Rosenpass permissive does not need to match - troubleshooting: check macOS boolean types, no warning is logged - wording fixes * docs: correct MDM timing and CLI claims from a macOS lab run (v0.80.0) - netbird debug config reads the policy file on each call; the daemon applies a change at its next reload and logs 'MDM policy changed' - netbird up --flag on a connected client ignores its flags; the rejection examples now say login --flag or up on a disconnected client - settings-field locks apply immediately; the disableUpdateSettings, disableProfiles and disableNetworks gates apply at the next reload - the restart on a policy change takes a second or two * docs: MDM results from the Windows and macOS profile labs (v0.80.0) - netbird logout works under every key and deregisters the peer; with managementURL, disableProfiles and disableUpdateSettings pinned, logout plus up --setup-key moves a device to another account on the same URL - a profile-installed macOS policy file is a binary plist, root:wheel 644 - a REG_BINARY (or other unread type) value is not listed as managed - the policy key inherits Authenticated Users read access * docs: tighten four MDM statements to what the labs and source show - no key blocks netbird logout of the active profile (source: only a non-active profile logout is gated, by disableProfiles) - the engine-restart log line appears only while the client is connected - macOS: binary plist is what a configuration profile produces; the shell script writes the same owner and mode - Windows: the observed fact is that the NetBird key inherits Authenticated Users read access, not a statement about the parent key * docs: address CodeRabbit review on the MDM delivery pages - iOS: the app must be managed by the MDM, not necessarily installed by it (Apple supports taking over a user-installed app); same in the iOS app-config template - macOS: Jamf's Application & Custom Settings takes the bare plist, so point Jamf at io.netbird.client.plist (and note the signed .mobileconfig upload route); JumpCloud's MDM Custom Configuration Profile takes a .mobileconfig per JumpCloud's docs, so drop the bare-plist claim and its troubleshooting entry - Windows: ADMXInstall only ingests the template; values are set through Policy/Config/NetBird~Policy~NetBird/<PolicyName>; drop the unverified Registry CSP path * docs: address CodeRabbit's second review of the MDM templates - netbird-macos.sh: managementURL, allowServerSSH and wireguardPort now default to $NULL like every other key; the old defaults pinned them (allowServerSSH=true pinned the SSH server allowed) on any run that did not edit those lines - drop the claim that disableMetricsCollection governs anonymous usage telemetry from the mobileconfig, the bare plist, the script and the ADML help text; the client recognizes the key but nothing reads it * docs: correct the Mosyle path for the macOS MDM profile Mosyle takes the .mobileconfig under Management > Management Profiles > Certificates / Custom Profiles; there is no Custom Settings option with a preference domain (per Orb and SI Prep deployment guides; Mosyle's own docs need a login). Drop Mosyle from the bare-plist template header, and align the mobileconfig header with the Jamf advice on the page. * docs: drop the profiles-command install tip from the macOS template macOS 11 and later cannot install configuration profiles with the profiles command (man profiles, profiles tool 8.0+); point local testing at a double-click and System Settings > General > Device Management, the path the lab used. * docs: make the Windows MDM page Intune-first, with a registry reference section The page now leads with the Intune workflow: import the NetBird ADMX/ADML once, create an Imported Administrative templates profile, assign it to a device group, sync and verify, then the OMA-URI fallback and Intune troubleshooting. Group Policy, .reg files, JumpCloud and the value-type table move to a Registry reference section with its own troubleshooting. New, from Microsoft Learn (Import custom ADMX templates): the current portal paths, en-us ADML only, and that re-importing an updated template fails until the profiles and the old template are deleted. netbird.admx has no namespace dependencies and no combo boxes, so it imports alone. The Intune install page stays separate; both pages now link to each other. Main-page links follow the renamed anchors (#value-types, #intune-troubleshooting). * docs: move the enforce-settings link to the end of the Intune deploy page * docs: group the Windows page's Intune steps under one heading Mirrors the Registry reference section, so the page's table of contents shows the Intune/registry split. Heading texts and anchors are unchanged. * docs: rename the first MDM key group to Set the server and startup The old heading named only managementURL; the group also holds disableAutoConnect and disableAutostart. * docs: add a use case to the Lock the client app section What a developer on a locked Acme laptop sees (app tabs, the two CLI errors, what still works), plus the finance team's disableNetworks and Auto Apply exit node variant. Every behaviour is from the macOS and Windows labs (v0.80.0). * docs: replace the Lock the client app use case with a plain goals table Goal, setting, and what users can still do, written for a junior admin. disableProfiles row says plainly that signing out and into another account still works (Windows lab W12). * docs: OMA-URI payloads: <disabled/> pins an on/off setting off The ADMX's on/off policies write enabledValue 1 and disabledValue 0, so <enabled/> can only pin a setting on; document <disabled/> for off, the String data type, and that un-pinning means Not configured (check the device with reg query, since removing an OMA-URI may not clear it). * docs: add What you can achieve tables to every MDM key section Same goal / setting / what-to-know format as Lock the client app, for server and startup, device exposure, encryption, support and monitoring, and connection tuning; Lock's table now names values too. Every line restates a claim already verified in the labs or source. * docs: reorder the MDM page: goals first, key reference after - What you can achieve (the six goal tables) comes first, then Recommended policies, How the client applies a policy, and the Policy keys reference; goal-table keys link down to their reference entries - reference groups get noun names (Server and startup keys, ...) so no two headings share an anchor - the Acme running example is gone; its three keys become The laptop baseline under Recommended policies, which the Windows, macOS and iOS pages now link to as their example - a one-line pin rule opens the goals, since the explanation now follows * docs: laptop baseline goes to the user-devices group, not just laptops * docs: rename the laptop baseline to the user device baseline It applies to every user device (and the iOS page uses it), so the Recommended policies column becomes User devices, the anchor becomes #the-user-device-baseline, and the three platform pages follow. --------- Co-authored-by: riccardom <riccardomanfrin@gmail.com>
217 lines
10 KiB
Bash
217 lines
10 KiB
Bash
#!/bin/bash
|
|
#
|
|
# SYNOPSIS
|
|
# Push the NetBird MDM policy to a macOS device via JumpCloud Commands.
|
|
#
|
|
# DESCRIPTION
|
|
# This is the macOS counterpart of docs/netbird-policy.reg.ps1.
|
|
# It writes the values declared in the "POLICY VALUES" block below to
|
|
# the managed-preferences plist that the NetBird daemon's
|
|
# client/mdm/policy_darwin.go loader reads on every 1-minute MDM
|
|
# reload tick:
|
|
#
|
|
# /Library/Managed Preferences/io.netbird.client.plist
|
|
#
|
|
# Once the plist lands, the daemon picks up the new values without
|
|
# restart (the ticker calls Config.apply() → applyMDMPolicy() and
|
|
# restarts the engine on diff).
|
|
#
|
|
# DEPLOYMENT (JumpCloud)
|
|
# 1. Admin Console -> Device Management -> Commands -> +.
|
|
# 2. Type: Mac, Shell, Run as: root.
|
|
# 3. Paste this file verbatim into the command body.
|
|
# 4. Bind to the target system group, save, run.
|
|
#
|
|
# IMPORTANT: PERSISTENCE
|
|
# macOS wipes /Library/Managed Preferences/ at every boot on devices
|
|
# that are NOT MDM-enrolled. For a persistent fleet rollout, push the
|
|
# companion docs/netbird-macos.mobileconfig as a Custom Configuration
|
|
# Profile (Admin Console -> MDM -> Mac Custom Configuration Profiles)
|
|
# instead of this script. Use this script when:
|
|
# - the device is MDM-enrolled (file survives reboots), or
|
|
# - you need a one-shot test push before reboot, or
|
|
# - you orchestrate via JumpCloud Commands and want the same
|
|
# variable-driven workflow as the Windows .ps1 sibling.
|
|
#
|
|
# IDEMPOTENCY: re-running with the same values is a no-op from the
|
|
# daemon's point of view (the 1-minute reload ticker diff returns empty).
|
|
#
|
|
# SECURITY: PreSharedKey is redacted in this script's log output.
|
|
|
|
set -euo pipefail
|
|
|
|
### POLICY VALUES — EDIT THIS BLOCK ###########################################
|
|
#
|
|
# Set each variable below to the desired value. Set to empty string ""
|
|
# or to NULL to omit a key entirely (the daemon treats an absent key
|
|
# as "no enforcement" for that field). Booleans use "true"/"false"
|
|
# (lowercase). Integers as decimal.
|
|
#
|
|
# Reference for key names + accepted values:
|
|
# client/mdm/policy.go (Key* constants)
|
|
# docs/netbird-macos.mobileconfig (sample profile)
|
|
# docs/netbird.admx + .adml (Windows ADMX schema)
|
|
#
|
|
NULL='__UNSET__'
|
|
managementURL="$NULL"
|
|
preSharedKey="$NULL" # secret; redacted in log
|
|
allowServerSSH="$NULL"
|
|
blockInbound="$NULL"
|
|
disableAutoConnect="$NULL"
|
|
disableAutostart="$NULL"
|
|
disableClientRoutes="$NULL"
|
|
disableServerRoutes="$NULL"
|
|
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="$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
|
|
##############################################################################
|
|
|
|
readonly PLIST_DIR='/Library/Managed Preferences'
|
|
readonly PLIST_PATH="$PLIST_DIR/io.netbird.client.plist"
|
|
readonly LOG_TAG='netbird-mdm'
|
|
|
|
# log sends a message to the system logger using the configured tag and echoes the message to stdout prefixed by an ISO 8601 UTC timestamp and the tag.
|
|
log() {
|
|
/usr/bin/logger -t "$LOG_TAG" "$*"
|
|
printf '%s [%s] %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$LOG_TAG" "$*"
|
|
}
|
|
|
|
# is_set returns success if the provided value is non-empty and is not equal to the special NULL marker.
|
|
is_set() {
|
|
local value="$1"
|
|
[[ -n "$value" && "$value" != "$NULL" ]]
|
|
}
|
|
|
|
# start_plist creates the temporary plist file at "$PLIST_PATH.tmp" containing the XML plist header and opening `<dict>` for the policy plist.
|
|
start_plist() {
|
|
cat > "$PLIST_PATH.tmp" <<'EOF'
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
EOF
|
|
}
|
|
|
|
# end_plist appends the closing `</dict>` and `</plist>` tags to the temporary plist file.
|
|
end_plist() {
|
|
cat >> "$PLIST_PATH.tmp" <<'EOF'
|
|
</dict>
|
|
</plist>
|
|
EOF
|
|
}
|
|
|
|
# emit_string appends a plist `<key>`/`<string>` entry for the given key and value to "$PLIST_PATH.tmp", XML-escaping `&`, `<`, and `>`, and logs the assignment (masking the logged value as `********** (secret)` when the key is `preSharedKey`).
|
|
emit_string() {
|
|
local key="$1" value="$2" log_value="$2"
|
|
# Escape XML entities in the value
|
|
local escaped
|
|
escaped="$(printf '%s' "$value" | sed -e 's/&/\&/g' -e 's/</\</g' -e 's/>/\>/g')"
|
|
printf ' <key>%s</key>\n <string>%s</string>\n' "$key" "$escaped" >> "$PLIST_PATH.tmp"
|
|
if [[ "$key" == "preSharedKey" ]]; then
|
|
log_value='********** (secret)'
|
|
fi
|
|
log "set $key = $log_value"
|
|
}
|
|
|
|
# emit_bool writes a boolean plist entry for a given key into the temporary plist file.
|
|
# emit_bool writes a boolean plist entry for a key when the provided value matches an accepted boolean token; logs an error and skips the key on invalid input.
|
|
emit_bool() {
|
|
local key="$1" value="$2"
|
|
local xml_bool
|
|
case "$value" in
|
|
true|True|TRUE|1|yes) xml_bool='<true/>' ; value='true' ;;
|
|
false|False|FALSE|0|no) xml_bool='<false/>' ; value='false' ;;
|
|
*) log "invalid boolean for $key: $value (must be true/false); skipping"; return ;;
|
|
esac
|
|
printf ' <key>%s</key>\n %s\n' "$key" "$xml_bool" >> "$PLIST_PATH.tmp"
|
|
log "set $key = $value"
|
|
}
|
|
|
|
# emit_int validates that VALUE contains only decimal digits and, if valid, appends an `<integer>` plist entry for KEY to the temporary plist (`$PLIST_PATH.tmp`) and logs the assignment; on invalid input it logs a skip and does not emit the key.
|
|
emit_int() {
|
|
local key="$1" value="$2"
|
|
if ! [[ "$value" =~ ^[0-9]+$ ]]; then
|
|
log "invalid integer for $key: $value (must be decimal); skipping"
|
|
return
|
|
fi
|
|
if [[ "$key" == "wireguardPort" ]] && (( value < 1 || value > 65535 )); then
|
|
log "invalid integer for $key: $value (must be 1-65535); skipping"
|
|
return
|
|
fi
|
|
printf ' <key>%s</key>\n <integer>%s</integer>\n' "$key" "$value" >> "$PLIST_PATH.tmp"
|
|
log "set $key = $value"
|
|
}
|
|
|
|
# emit_split_tunnel_mode validates that VALUE is "allow" or "disallow" and, if valid, appends a plist string entry for splitTunnelMode; on invalid input it logs a skip and does not emit the key.
|
|
emit_split_tunnel_mode() {
|
|
local value="$1"
|
|
case "$value" in
|
|
allow|disallow) emit_string splitTunnelMode "$value" ;;
|
|
*) log "invalid splitTunnelMode: $value (must be allow/disallow); skipping" ;;
|
|
esac
|
|
}
|
|
|
|
# main builds the NetBird MDM plist from configured policy variables, validates and installs it to /Library/Managed Preferences/io.netbird.client.plist (root:wheel, 644) and optionally triggers the NetBird daemon to reload.
|
|
main() {
|
|
log "applying NetBird MDM policy to $PLIST_PATH"
|
|
/bin/mkdir -p "$PLIST_DIR"
|
|
start_plist
|
|
|
|
is_set "$managementURL" && emit_string managementURL "$managementURL"
|
|
is_set "$preSharedKey" && emit_string preSharedKey "$preSharedKey"
|
|
is_set "$allowServerSSH" && emit_bool allowServerSSH "$allowServerSSH"
|
|
is_set "$blockInbound" && emit_bool blockInbound "$blockInbound"
|
|
is_set "$disableAutoConnect" && emit_bool disableAutoConnect "$disableAutoConnect"
|
|
is_set "$disableAutostart" && emit_bool disableAutostart "$disableAutostart"
|
|
is_set "$disableClientRoutes" && emit_bool disableClientRoutes "$disableClientRoutes"
|
|
is_set "$disableServerRoutes" && emit_bool disableServerRoutes "$disableServerRoutes"
|
|
is_set "$disableMetricsCollection" && emit_bool disableMetricsCollection "$disableMetricsCollection"
|
|
is_set "$disableUpdateSettings" && emit_bool disableUpdateSettings "$disableUpdateSettings"
|
|
is_set "$disableProfiles" && emit_bool disableProfiles "$disableProfiles"
|
|
is_set "$disableNetworks" && emit_bool disableNetworks "$disableNetworks"
|
|
is_set "$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"
|
|
|
|
end_plist
|
|
|
|
if ! /usr/bin/plutil -lint "$PLIST_PATH.tmp" >/dev/null 2>&1; then
|
|
log "ERROR: generated plist failed plutil lint; not installing"
|
|
/usr/bin/plutil -lint "$PLIST_PATH.tmp" >&2 || true
|
|
/bin/rm -f "$PLIST_PATH.tmp"
|
|
exit 1
|
|
fi
|
|
|
|
/bin/mv -f "$PLIST_PATH.tmp" "$PLIST_PATH"
|
|
/usr/sbin/chown root:wheel "$PLIST_PATH"
|
|
/bin/chmod 644 "$PLIST_PATH"
|
|
|
|
log "policy installed; NetBird daemon will pick it up within the next 1-minute reload tick"
|
|
|
|
# Optional: kick the daemon for an immediate apply. Safe — does
|
|
# nothing on a host where NetBird is not yet installed.
|
|
/bin/launchctl kickstart -k system/io.netbird.client 2>/dev/null || true
|
|
}
|
|
|
|
main "$@"
|