Building a CLI-Only .pkg for macOS MDM Deployment

When deploying NetBird across a fleet of macOS devices using an MDM solution (Jamf Pro, Kandji, Munki, etc.), you may want a headless, CLI-only installation without the desktop UI. The official .pkg from pkgs.netbird.io bundles the full NetBird.app, which includes the menu bar UI. This guide shows you how to build a custom .pkg that installs only the netbird CLI binary and registers it as a launchd daemon.

This approach is useful when:

  • You are deploying to headless servers or CI/CD runners that have no user session
  • You want to minimize the footprint on managed endpoints
  • Your MDM workflow requires a .pkg artifact (rather than a raw binary or install script)
  • You need to control exactly which version is deployed across your fleet

Prerequisites

  • A Mac with pkgbuild available (included with Xcode Command Line Tools)
  • Access to your MDM solution with permissions to upload packages and create deployment policies
  • A NetBird setup key for initial enrollment (reusable keys are recommended for fleet deployments)

How the CLI-only install works

The standard NetBird .pkg installer places NetBird.app into /Applications and symlinks the binary to /usr/local/bin/netbird. A CLI-only install skips the app bundle entirely and places the netbird binary directly in /usr/local/bin/.

When you run netbird service install, it creates a launchd daemon plist at /Library/LaunchDaemons/netbird.plist. The service runs as root, which is required for creating and managing the WireGuard network interface (utun device).

Step 1: Download the CLI binary

Download the appropriate tarball from the NetBird GitHub releases page. Each release publishes architecture-specific and universal macOS binaries:

# Set the version you want to deploy
VERSION="0.36.5"

# Universal binary (recommended — works on both Intel and Apple Silicon):
curl -L -o netbird.tar.gz \
  "https://github.com/netbirdio/netbird/releases/download/v${VERSION}/netbird_${VERSION}_darwin_all.tar.gz"

# Or architecture-specific:
# Apple Silicon: netbird_${VERSION}_darwin_arm64.tar.gz
# Intel: netbird_${VERSION}_darwin_amd64.tar.gz

Extract the binary:

mkdir -p pkg-root/usr/local/bin
tar xzf netbird.tar.gz -C pkg-root/usr/local/bin netbird
chmod +x pkg-root/usr/local/bin/netbird

Step 2: Create install scripts

Create a scripts/ directory with preinstall and postinstall scripts. These handle stopping any existing service before the upgrade and starting it after.

mkdir -p scripts

Preinstall script

The preinstall script stops any running NetBird service to allow the binary to be replaced cleanly:

cat > scripts/preinstall << 'PREINSTALL'
#!/bin/sh

LOG_FILE=/var/log/netbird/client_pre_install.log
AGENT=/usr/local/bin/netbird

mkdir -p /var/log/netbird/

{
    echo "=== NetBird CLI preinstall: $(date) ==="

    # Check if netbird was installed with Homebrew
    if command -v brew >/dev/null 2>&1; then
        if brew list --formula 2>/dev/null | grep -q netbird; then
            echo "NetBird was installed with Homebrew. Please use Homebrew to manage the package."
            exit 1
        fi
    fi

    # Stop the running service if it exists
    if [ -f "$AGENT" ]; then
        $AGENT service stop 2>/dev/null || true
    fi

    echo "Preinstall complete"
    exit 0
} >> "$LOG_FILE" 2>&1
PREINSTALL

chmod +x scripts/preinstall

Postinstall script

The postinstall script installs the launchd daemon service and optionally enrolls the peer with a setup key on first install:

cat > scripts/postinstall << 'POSTINSTALL'
#!/bin/sh

LOG_FILE=/var/log/netbird/client_post_install.log
AGENT=/usr/local/bin/netbird

mkdir -p /var/log/netbird/

{
    echo "=== NetBird CLI postinstall: $(date) ==="

    # Install and start the daemon service
    $AGENT service install 2>/dev/null || true
    $AGENT service start || {
        echo "ERROR: Failed to start NetBird service"
        exit 1
    }

    # Enroll the peer if not already configured.
    # The config file exists once the peer has been enrolled at least once.
    CONFIG_FILE="/var/lib/netbird/config.json"
    if [ ! -f "$CONFIG_FILE" ]; then
        echo "First install detected — enrolling with setup key"
        # Replace YOUR_SETUP_KEY with your actual reusable setup key.
        # For self-hosted deployments, add: --management-url https://your-management.example.com
        $AGENT up --setup-key YOUR_SETUP_KEY
    else
        echo "Existing configuration found — skipping enrollment"
    fi

    echo "Postinstall complete"
    exit 0
} >> "$LOG_FILE" 2>&1
POSTINSTALL

chmod +x scripts/postinstall

Step 3: Sign the binary and build the .pkg

The binary from the tarball is unsigned. For MDM deployment, you should sign both the binary (with codesign) and the installer package (with pkgbuild --sign). These are separate signing steps that use different certificate types.

Sign the binary

Use your Developer ID Application certificate to code-sign the binary before packaging:

codesign --sign "Developer ID Application: Your Organization (TEAMID)" \
  --options runtime \
  --timestamp \
  --force \
  pkg-root/usr/local/bin/netbird

The --options runtime flag enables the hardened runtime (required for notarization), and --timestamp embeds a secure timestamp from Apple's servers.

Build the .pkg

Use pkgbuild to assemble the signed binary and scripts into an installer package:

pkgbuild \
  --root pkg-root \
  --scripts scripts \
  --identifier com.your-org.netbird-cli \
  --version "${VERSION}" \
  --install-location / \
  --sign "Developer ID Installer: Your Organization (TEAMID)" \
  netbird-cli-${VERSION}.pkg

Optional: Notarize the .pkg

For full Gatekeeper compatibility, submit the signed .pkg to Apple for notarization:

xcrun notarytool submit netbird-cli-${VERSION}.pkg \
  --apple-id "your@email.com" \
  --team-id "TEAMID" \
  --password "app-specific-password" \
  --wait

# Staple the notarization ticket to the package
xcrun stapler staple netbird-cli-${VERSION}.pkg

Step 4: Deploy via MDM

Upload the .pkg to your MDM solution and create a deployment policy. The exact steps vary by platform:

  • Jamf Pro: Upload the package under Settings > Computer Management > Packages, then create a policy with appropriate triggers (enrollment, recurring check-in). See Deploying NetBird with Jamf Pro for detailed instructions.
  • Kandji: Create a Custom App library item with the Installer Package option. See Deploying NetBird with Kandji for detailed instructions.
  • Munki: Import the .pkg into your Munki repository using munkiimport and assign it to the appropriate manifest.
  • Microsoft Intune: Upload as a macOS LOB app. See Deploying NetBird with Intune for detailed instructions.

Managing updates

Because the CLI-only install does not have the io.netbird.client package receipt, the NetBird automatic update feature does not work for these deployments. You must manage updates through one of these approaches:

Push updated .pkg via MDM

Build a new .pkg with the updated binary (repeating Steps 1-3) and deploy it through your MDM. The preinstall script stops the running service, the new binary replaces the old one, and the postinstall script restarts the service. No re-enrollment is needed.

Use the install script with UPDATE_NETBIRD

For machines where you can run scripts remotely (via MDM script execution or SSH), use the install script's update mode:

curl -fsSL https://pkgs.netbird.io/install.sh | USE_BIN_INSTALL=true SKIP_UI_APP=true UPDATE_NETBIRD=true sh

This downloads the latest binary, replaces the existing one, and restarts the service.

Uninstalling

To remove a CLI-only NetBird install:

sudo netbird down
sudo netbird service stop
sudo netbird service uninstall
sudo rm /usr/local/bin/netbird
sudo rm -rf /var/lib/netbird/
sudo rm -rf /var/log/netbird/

You can wrap this in an MDM uninstall script or a preinstall script for a removal package.

Complete build script

For convenience, here is a self-contained script that builds the CLI-only .pkg. It optionally signs the binary and package if signing identities are provided. Save it as build-netbird-cli-pkg.sh:

#!/bin/sh
# Usage: ./build-netbird-cli-pkg.sh <VERSION> <SETUP_KEY> [MANAGEMENT_URL]
#
# Optional environment variables for signing:
#   APP_SIGN_ID    - Developer ID Application identity (for binary signing)
#   PKG_SIGN_ID    - Developer ID Installer identity (for package signing)
#   APPLE_ID       - Apple ID email (for notarization)
#   APPLE_TEAM_ID  - Apple Team ID (for notarization)
#   APPLE_APP_PWD  - App-specific password (for notarization)
#
# Example (unsigned):
#   ./build-netbird-cli-pkg.sh 0.36.5 A1B2C3D4-E5F6-7890-ABCD-EF1234567890
#
# Example (signed + notarized):
#   APP_SIGN_ID="Developer ID Application: Acme Inc (TEAMID)" \
#   PKG_SIGN_ID="Developer ID Installer: Acme Inc (TEAMID)" \
#   APPLE_ID="dev@acme.com" APPLE_TEAM_ID="TEAMID" APPLE_APP_PWD="xxxx-xxxx-xxxx-xxxx" \
#   ./build-netbird-cli-pkg.sh 0.36.5 A1B2C3D4-E5F6-7890-ABCD-EF1234567890

set -e

VERSION="${1:?Usage: $0 <VERSION> <SETUP_KEY> [MANAGEMENT_URL]}"
SETUP_KEY="${2:?Usage: $0 <VERSION> <SETUP_KEY> [MANAGEMENT_URL]}"
MANAGEMENT_URL="${3:-}"
PKG_ID="com.your-org.netbird-cli"
WORK_DIR=$(mktemp -d)

echo "Building NetBird CLI .pkg v${VERSION}..."

# Download and extract
curl -fSL -o "${WORK_DIR}/netbird.tar.gz" \
  "https://github.com/netbirdio/netbird/releases/download/v${VERSION}/netbird_${VERSION}_darwin_all.tar.gz"
mkdir -p "${WORK_DIR}/root/usr/local/bin"
tar xzf "${WORK_DIR}/netbird.tar.gz" -C "${WORK_DIR}/root/usr/local/bin" netbird
chmod +x "${WORK_DIR}/root/usr/local/bin/netbird"

# Sign the binary if a signing identity is provided
if [ -n "${APP_SIGN_ID:-}" ]; then
  echo "Signing binary with: ${APP_SIGN_ID}"
  codesign --sign "${APP_SIGN_ID}" \
    --options runtime \
    --timestamp \
    --force \
    "${WORK_DIR}/root/usr/local/bin/netbird"
else
  echo "WARNING: No APP_SIGN_ID set — binary will be unsigned."
  echo "Unsigned binaries trigger Gatekeeper warnings and may be blocked by MDM policies."
fi

# Build management URL flag
MGMT_FLAG=""
if [ -n "$MANAGEMENT_URL" ]; then
  MGMT_FLAG="--management-url ${MANAGEMENT_URL}"
fi

# Create scripts
mkdir -p "${WORK_DIR}/scripts"

cat > "${WORK_DIR}/scripts/preinstall" << 'EOF'
#!/bin/sh
AGENT=/usr/local/bin/netbird
mkdir -p /var/log/netbird/
{
    echo "=== NetBird CLI preinstall: $(date) ==="
    if command -v brew >/dev/null 2>&1; then
        if brew list --formula 2>/dev/null | grep -q netbird; then
            echo "NetBird installed via Homebrew. Aborting."
            exit 1
        fi
    fi
    [ -f "$AGENT" ] && $AGENT service stop 2>/dev/null || true
    echo "Preinstall complete"
    exit 0
} >> /var/log/netbird/client_pre_install.log 2>&1
EOF

cat > "${WORK_DIR}/scripts/postinstall" << EOF
#!/bin/sh
AGENT=/usr/local/bin/netbird
mkdir -p /var/log/netbird/
{
    echo "=== NetBird CLI postinstall: \$(date) ==="
    \$AGENT service install 2>/dev/null || true
    \$AGENT service start || { echo "ERROR: Failed to start service"; exit 1; }
    if [ ! -f "/var/lib/netbird/config.json" ]; then
        echo "First install — enrolling with setup key"
        \$AGENT up --setup-key ${SETUP_KEY} ${MGMT_FLAG}
    else
        echo "Existing config found — skipping enrollment"
    fi
    echo "Postinstall complete"
    exit 0
} >> /var/log/netbird/client_post_install.log 2>&1
EOF

chmod +x "${WORK_DIR}/scripts/preinstall" "${WORK_DIR}/scripts/postinstall"

# Build the package
PKG_SIGN_FLAGS=""
if [ -n "${PKG_SIGN_ID:-}" ]; then
  echo "Signing package with: ${PKG_SIGN_ID}"
  PKG_SIGN_FLAGS="--sign ${PKG_SIGN_ID}"
fi

pkgbuild \
  --root "${WORK_DIR}/root" \
  --scripts "${WORK_DIR}/scripts" \
  --identifier "${PKG_ID}" \
  --version "${VERSION}" \
  --install-location / \
  ${PKG_SIGN_FLAGS} \
  "netbird-cli-${VERSION}.pkg"

# Notarize if credentials are provided
if [ -n "${APPLE_ID:-}" ] && [ -n "${APPLE_TEAM_ID:-}" ] && [ -n "${APPLE_APP_PWD:-}" ]; then
  echo "Submitting for notarization..."
  xcrun notarytool submit "netbird-cli-${VERSION}.pkg" \
    --apple-id "${APPLE_ID}" \
    --team-id "${APPLE_TEAM_ID}" \
    --password "${APPLE_APP_PWD}" \
    --wait
  xcrun stapler staple "netbird-cli-${VERSION}.pkg"
  echo "Notarization complete."
fi

# Clean up
rm -rf "${WORK_DIR}"

echo "Built: netbird-cli-${VERSION}.pkg"
echo "Upload this package to your MDM solution for deployment."