From 9b2fe7a5a4341ceec097b052d92514986073f0de Mon Sep 17 00:00:00 2001 From: Voravit Takkar Date: Fri, 21 Aug 2026 11:07:43 +0700 Subject: [PATCH] =?UTF-8?q?ci:=20FlowAccount=20release=20pipeline=20?= =?UTF-8?q?=E2=80=94=20signed=20macOS=20pkg=20and=20Linux=20rpm/deb?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fork inherits .goreleaser.yaml and release_files/ but no release automation, because upstream builds its published packages outside this repository. A fork with no workflow produces no artifacts, so there is nothing to deploy from it. goreleaser already covers most of it — universal darwin binaries, plus deb and rpm through nfpms with the existing postinstall and preremove scripts. Only the macOS installer is new: pkgbuild, Developer ID signing and notarization. Adds release_files/darwin_pkg_headless/ as a variant rather than a replacement. Upstream's darwin_pkg/ scripts install /Applications/NetBird.app and quit and relaunch the UI around the install, which is right for a laptop and meaningless on mm-1/2/3 and ryzen where there is no console user. The headless package installs the binary and registers the launchd service using the client's own "service install" subcommand — rather than hand-rolling a plist that would drift from upstream — and stops there. It deliberately does NOT bring the peer up. Joining the mesh needs a setup key, and baking one into a package makes it a reusable bearer credential shipped to every machine: anything that can read the package can join the network. Enrolment stays a per-host step with a one-off key. The macOS job fails loudly when signing secrets are absent rather than producing an uninstallable package. An unsigned or un-notarized pkg is blocked by Gatekeeper and would also be refused by the fleet's Santa rules and System Extensions profile, so shipping one wastes more time than the failed build. Blocked on an Apple Developer Program membership, which also unblocks signing the CrewOps daemon and Tray for ADR-009. One purchase, two blockers. --- .github/workflows/flowaccount-release.yml | 178 ++++++++++++++++++ release_files/darwin_pkg_headless/README.md | 58 ++++++ .../darwin_pkg_headless/build-pkg.sh | 69 +++++++ release_files/darwin_pkg_headless/postinstall | 26 +++ 4 files changed, 331 insertions(+) create mode 100644 .github/workflows/flowaccount-release.yml create mode 100644 release_files/darwin_pkg_headless/README.md create mode 100755 release_files/darwin_pkg_headless/build-pkg.sh create mode 100755 release_files/darwin_pkg_headless/postinstall diff --git a/.github/workflows/flowaccount-release.yml b/.github/workflows/flowaccount-release.yml new file mode 100644 index 00000000000..e2362d16428 --- /dev/null +++ b/.github/workflows/flowaccount-release.yml @@ -0,0 +1,178 @@ +# FlowAccount release pipeline for the NetBird fork. +# +# WHY THIS EXISTS +# Upstream builds its published packages outside this repository — the fork +# inherits .goreleaser.yaml and release_files/ but no release automation, so +# a fork with no workflow produces no artifacts. This adds one. +# +# goreleaser already builds everything except the macOS installer: universal +# darwin binaries, plus deb and rpm via nfpms with the existing postinstall +# and preremove scripts. Only the .pkg wrapping, signing and notarization +# are new here. +# +# WHAT IT PRODUCES +# netbird__linux_{amd64,arm64}.rpm ← ryzen, RHEL terminals +# netbird__linux_{amd64,arm64}.deb +# netbird__darwin_universal.pkg ← signed + notarized, headless +# +# ⚠️ MACOS SIGNING IS NOT OPTIONAL. An unsigned or un-notarized package is +# blocked by Gatekeeper, and would also be refused by the fleet's Santa +# rules and System Extensions profile. Without the secrets below the macOS +# job fails deliberately rather than shipping something that cannot install. +# +# REQUIRED SECRETS +# MACOS_CERT_P12_BASE64 Developer ID Application + Installer, .p12, +# base64. Both identities in one keychain import. +# MACOS_CERT_PASSWORD password for that .p12 +# MACOS_SIGN_APP_IDENTITY "Developer ID Application: … (TEAMID)" +# MACOS_SIGN_INSTALLER_IDENTITY"Developer ID Installer: … (TEAMID)" +# MACOS_NOTARY_ISSUER_ID App Store Connect API key — issuer UUID +# MACOS_NOTARY_KEY_ID key id +# MACOS_NOTARY_KEY_P8 the .p8 contents +# +# All require an Apple Developer Program membership ($99/yr), which also +# unblocks signing the CrewOps daemon and Tray — see ADR-009 in +# flowaccount.infrastructure. + +name: flowaccount release + +on: + push: + tags: ["v*"] + workflow_dispatch: + inputs: + version: + description: "Version to build, e.g. v0.76.3-fa.1" + required: true + +permissions: + contents: write + +jobs: + linux: + name: linux packages + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: { fetch-depth: 0 } + - uses: actions/setup-go@v5 + with: { go-version-file: go.mod, cache: true } + + # --split so this job produces artifacts without needing the macOS + # signing secrets — the two platforms release independently. + - name: goreleaser + uses: goreleaser/goreleaser-action@v6 + with: + version: latest + args: release --clean --skip=publish,docker,sign --split + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - uses: actions/upload-artifact@v4 + with: + name: linux-packages + path: | + dist/**/*.rpm + dist/**/*.deb + if-no-files-found: error + + macos: + name: macos signed pkg + runs-on: macos-14 + steps: + - uses: actions/checkout@v4 + with: { fetch-depth: 0 } + - uses: actions/setup-go@v5 + with: { go-version-file: go.mod, cache: true } + + - name: build universal binary + run: | + CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o /tmp/netbird_amd64 ./client + CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o /tmp/netbird_arm64 ./client + lipo -create -output /tmp/netbird /tmp/netbird_amd64 /tmp/netbird_arm64 + lipo -info /tmp/netbird + + # Fail loudly rather than silently producing an uninstallable package. + - name: check signing secrets + run: | + for v in MACOS_CERT_P12_BASE64 MACOS_CERT_PASSWORD \ + MACOS_SIGN_APP_IDENTITY MACOS_SIGN_INSTALLER_IDENTITY \ + MACOS_NOTARY_ISSUER_ID MACOS_NOTARY_KEY_ID MACOS_NOTARY_KEY_P8; do + eval "val=\${$v}" + [ -n "$val" ] || { echo "::error::$v is not set. See the header of this workflow."; exit 1; } + done + env: + MACOS_CERT_P12_BASE64: ${{ secrets.MACOS_CERT_P12_BASE64 }} + MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }} + MACOS_SIGN_APP_IDENTITY: ${{ secrets.MACOS_SIGN_APP_IDENTITY }} + MACOS_SIGN_INSTALLER_IDENTITY: ${{ secrets.MACOS_SIGN_INSTALLER_IDENTITY }} + MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }} + MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }} + MACOS_NOTARY_KEY_P8: ${{ secrets.MACOS_NOTARY_KEY_P8 }} + + - name: import certificates + run: | + KEYCHAIN=build.keychain + security create-keychain -p "" "$KEYCHAIN" + security default-keychain -s "$KEYCHAIN" + security unlock-keychain -p "" "$KEYCHAIN" + security set-keychain-settings -t 3600 -u "$KEYCHAIN" + echo "$MACOS_CERT_P12_BASE64" | base64 --decode > /tmp/cert.p12 + security import /tmp/cert.p12 -k "$KEYCHAIN" -P "$MACOS_CERT_PASSWORD" \ + -T /usr/bin/codesign -T /usr/bin/productsign + security set-key-partition-list -S apple-tool:,apple: -s -k "" "$KEYCHAIN" + rm -f /tmp/cert.p12 + env: + MACOS_CERT_P12_BASE64: ${{ secrets.MACOS_CERT_P12_BASE64 }} + MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }} + + - name: notarytool credentials + run: | + printf '%s' "$MACOS_NOTARY_KEY_P8" > /tmp/notary.p8 + xcrun notarytool store-credentials fa-notary \ + --key /tmp/notary.p8 --key-id "$MACOS_NOTARY_KEY_ID" \ + --issuer "$MACOS_NOTARY_ISSUER_ID" + rm -f /tmp/notary.p8 + env: + MACOS_NOTARY_KEY_P8: ${{ secrets.MACOS_NOTARY_KEY_P8 }} + MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }} + MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }} + + - name: build signed pkg + run: | + VERSION="${{ github.event.inputs.version || github.ref_name }}" + VERSION="${VERSION#v}" + ./release_files/darwin_pkg_headless/build-pkg.sh \ + /tmp/netbird "$VERSION" "netbird_${VERSION}_darwin_universal.pkg" + env: + MACOS_SIGN_APP_IDENTITY: ${{ secrets.MACOS_SIGN_APP_IDENTITY }} + MACOS_SIGN_INSTALLER_IDENTITY: ${{ secrets.MACOS_SIGN_INSTALLER_IDENTITY }} + MACOS_NOTARY_KEYCHAIN_PROFILE: fa-notary + + - uses: actions/upload-artifact@v4 + with: + name: macos-package + path: "*.pkg" + if-no-files-found: error + + publish: + name: publish release + needs: [linux, macos] + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + steps: + - uses: actions/download-artifact@v4 + with: { path: artifacts } + - name: checksums + run: | + find artifacts -type f \( -name '*.pkg' -o -name '*.rpm' -o -name '*.deb' \) \ + -exec shasum -a 256 {} \; > SHA256SUMS + cat SHA256SUMS + - uses: softprops/action-gh-release@v2 + with: + files: | + artifacts/**/*.pkg + artifacts/**/*.rpm + artifacts/**/*.deb + SHA256SUMS + generate_release_notes: true diff --git a/release_files/darwin_pkg_headless/README.md b/release_files/darwin_pkg_headless/README.md new file mode 100644 index 00000000000..c914713ee33 --- /dev/null +++ b/release_files/darwin_pkg_headless/README.md @@ -0,0 +1,58 @@ +# FlowAccount release tooling + +Added by FlowAccount. Not upstream — keep it separate so rebasing on +`netbirdio/netbird` stays clean. + +## Why the fork builds its own packages + +The fork exists as insurance against an upstream licence change — the pattern +HashiCorp, Redis, Elastic and MongoDB all followed. Holding the code as of the +fork date preserves the right to it. + +**Building from the fork is a separate decision from having the fork**, and it +carries a real cost: NetBird is what stands between an attacker and the internal +network, so running our own build makes a NetBird CVE our rebuild-and-redeploy +emergency rather than a package update. See ADR-010 in +`flowaccount.infrastructure`, which declined forking for exactly that reason. + +If the fleet is deployed from these artifacts, someone owns tracking upstream +security releases and cutting a build promptly. That ownership should be named +before the first machine is deployed from here. + +## Headless vs GUI packaging + +| | upstream `darwin_pkg/` | this `darwin_pkg_headless/` | +|---|---|---| +| Installs | `/Applications/NetBird.app` + daemon | daemon only | +| Launches UI | yes, as the console user | no | +| For | laptops | `mm-1/2/3`, `ryzen`, deployment terminals | + +Upstream's scripts assume a console user and quit/relaunch the UI around the +install. On a headless host there is no console user, so those steps are noise +at best. This variant installs the binary, registers the launchd service via +`netbird service install`, and stops there. + +## What it does NOT do + +**It does not run `netbird up`.** Joining the mesh needs a setup key, and baking +one into a package makes it a reusable bearer credential shipped to every +machine — anything that can read the package can join the network. Enrolment is +a separate per-host step with a one-off key: see +`docs/workloads/netbird-client.md` in `flowaccount.infrastructure`. + +## Prerequisites + +An **Apple Developer Program membership** ($99/yr). Without it there is no +Developer ID to sign with, and an unsigned or un-notarized package is blocked by +Gatekeeper — it will not install on a fleet machine, and would also be refused +by the Santa rules and the System Extensions profile. + +That membership also unblocks signing the CrewOps daemon and Tray, which +ADR-009 requires before Santa can move to Lockdown. One purchase, two blockers. + +## Version pinning + +The client version must match the management server. `flowaccount.infrastructure` +pins `netbirdio/netbird-server:0.76.3`. A client ahead of its server is +unsupported and fails quietly — the peer connects, looks healthy, and does not +route correctly. Upgrade both in the same change. diff --git a/release_files/darwin_pkg_headless/build-pkg.sh b/release_files/darwin_pkg_headless/build-pkg.sh new file mode 100755 index 00000000000..4f4e5dd59f4 --- /dev/null +++ b/release_files/darwin_pkg_headless/build-pkg.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# ============================================================================ +# build-pkg.sh — wrap the universal darwin binary in a signed, notarized .pkg +# +# ./build-pkg.sh +# +# goreleaser produces the universal binary (universal_binaries: id netbird) +# but not a macOS installer package. Upstream builds that outside the repo, +# which is why this exists. +# +# Signing and notarization are REQUIRED, not optional: an unsigned or +# un-notarized package is blocked by Gatekeeper, and would also be refused by +# the fleet's System Extensions and Santa policies. +# +# Environment: +# MACOS_SIGN_APP_IDENTITY "Developer ID Application: … (TEAMID)" +# MACOS_SIGN_INSTALLER_IDENTITY "Developer ID Installer: … (TEAMID)" +# MACOS_NOTARY_KEYCHAIN_PROFILE notarytool profile name (optional; skips +# notarization if unset — for local testing +# only, never for a release) +# ============================================================================ +set -euo pipefail + +BIN="${1:?path to netbird binary}" +VERSION="${2:?version}" +OUT="${3:?output .pkg path}" + +: "${MACOS_SIGN_APP_IDENTITY:?Developer ID Application identity required}" +: "${MACOS_SIGN_INSTALLER_IDENTITY:?Developer ID Installer identity required}" + +ROOT=$(mktemp -d) +trap 'rm -rf "$ROOT"' EXIT +install -d "$ROOT/payload/usr/local/bin" +install -m 0755 "$BIN" "$ROOT/payload/usr/local/bin/netbird" +install -d "$ROOT/scripts" +install -m 0755 "$(dirname "$0")/postinstall" "$ROOT/scripts/postinstall" + +echo "── signing binary" +codesign --force --timestamp --options runtime \ + --sign "$MACOS_SIGN_APP_IDENTITY" \ + "$ROOT/payload/usr/local/bin/netbird" +codesign --verify --strict --verbose=2 "$ROOT/payload/usr/local/bin/netbird" + +echo "── building component package" +pkgbuild \ + --root "$ROOT/payload" \ + --scripts "$ROOT/scripts" \ + --identifier io.netbird.client \ + --version "$VERSION" \ + --install-location / \ + "$ROOT/component.pkg" + +echo "── signing installer" +productbuild --package "$ROOT/component.pkg" "$ROOT/unsigned.pkg" +productsign --sign "$MACOS_SIGN_INSTALLER_IDENTITY" "$ROOT/unsigned.pkg" "$OUT" +pkgutil --check-signature "$OUT" + +if [ -n "${MACOS_NOTARY_KEYCHAIN_PROFILE:-}" ]; then + echo "── notarizing (this blocks until Apple responds)" + xcrun notarytool submit "$OUT" \ + --keychain-profile "$MACOS_NOTARY_KEYCHAIN_PROFILE" --wait + xcrun stapler staple "$OUT" + xcrun stapler validate "$OUT" +else + echo " ⚠️ MACOS_NOTARY_KEYCHAIN_PROFILE unset — NOT notarized." + echo " Gatekeeper will block this package. Local testing only." +fi + +echo " ✓ $OUT" diff --git a/release_files/darwin_pkg_headless/postinstall b/release_files/darwin_pkg_headless/postinstall new file mode 100755 index 00000000000..43868f7593e --- /dev/null +++ b/release_files/darwin_pkg_headless/postinstall @@ -0,0 +1,26 @@ +#!/bin/bash +# Installed by the FlowAccount HEADLESS NetBird .pkg. Registers and starts the daemon. +# +# netbird ships `service install` which writes the launchd plist itself, so we +# call that rather than hand-rolling a plist that would drift from upstream. +set -euo pipefail + +BIN=/usr/local/bin/netbird +[ -x "$BIN" ] || { echo "netbird binary missing at $BIN"; exit 1; } + +# Idempotent: a reinstall or upgrade must not fail because the service exists. +"$BIN" service uninstall >/dev/null 2>&1 || true +"$BIN" service install + +# Do NOT `netbird up` here. Joining the mesh needs a setup key, and baking one +# into a package would make it a reusable bearer credential shipped to every +# machine. Enrolment is a separate, per-host step — see +# docs/workloads/netbird-client.md in flowaccount.infrastructure. +"$BIN" service start >/dev/null 2>&1 || true + +exit 0 + +# NOTE: this is the HEADLESS variant. Upstream's release_files/darwin_pkg/ +# scripts install /Applications/NetBird.app and launch the UI — correct for a +# laptop, wrong for mm-1/2/3 and ryzen, which have no console user. This +# package installs the daemon only.