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
178 changes: 178 additions & 0 deletions .github/workflows/flowaccount-release.yml
Original file line number Diff line number Diff line change
@@ -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_<v>_linux_{amd64,arm64}.rpm ← ryzen, RHEL terminals
# netbird_<v>_linux_{amd64,arm64}.deb
# netbird_<v>_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
58 changes: 58 additions & 0 deletions release_files/darwin_pkg_headless/README.md
Original file line number Diff line number Diff line change
@@ -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.
69 changes: 69 additions & 0 deletions release_files/darwin_pkg_headless/build-pkg.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env bash
# ============================================================================
# build-pkg.sh — wrap the universal darwin binary in a signed, notarized .pkg
#
# ./build-pkg.sh <path-to-netbird-binary> <version> <output.pkg>
#
# 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"
26 changes: 26 additions & 0 deletions release_files/darwin_pkg_headless/postinstall
Original file line number Diff line number Diff line change
@@ -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.
Loading