Skip to main content

Signing FluxVM for macOS

Out of the box, every FluxVM binary on a Mac is signed ad hoc. That is enough for Linux and macOS VMs, NAT, apple.usernet, private apple.networks, shared folders, Rosetta, snapshots and USB disk images.

Three features need entitlements that Apple restricts:

FeatureEntitlementWhich binary
apple.vmnet networks, the shared vmnet broker, tenant networks (isolate_tenant_networks)com.apple.vm.networkingthe runner, and the broker fluxvm-vmnetd
apple.bridge_interface (VMs directly on the LAN)com.apple.vm.networkingthe runner
Physical USB passthrough (Accessory Access)com.apple.developer.accessory-access.usbFluxVMUSBAccess.app

macOS honours a restricted entitlement only when three things are all true:

  1. the executable is signed with a Developer ID certificate;
  2. it sits in an app bundle that embeds a provisioning profile granting that entitlement;
  3. it is launched from that bundle.

An ad hoc binary that claims one of them is killed at launch (exit 137). This is what happened to fluxvm-vmnetd and the USB helper on the test Mac.

What the release package contains​

scripts/package-macos.sh builds and signs everything:

fluxvm-<version>-macos-arm64/
bin/fluxctl, fluxvm-vz-switch, fluxvm-blockd, fluxvm-usernet
bin/fluxvm-vz-runner virtualization entitlement only (always works)
bin/FluxVMRunner.app only with a runner profile: + com.apple.vm.networking
libexec/FluxVMNetworking.app the vmnet broker (+ vm.networking with its profile)
libexec/FluxVMUSBAccess.app the USB consent helper (+ accessory access with its profile); only when the build
SDK has the AccessoryAccess framework
share/launchd/*.plist launch agents for the two helpers (__PREFIX__ = the unpacked package)

The daemon picks bin/FluxVMRunner.app/Contents/MacOS/fluxvm-vz-runner when it exists and falls back to the plain runner. FLUXVM_VZ_RUNNER still overrides both. Without a profile, a bundle is signed without its restricted entitlement: it is shipped, starts, and admission refuses the features above with a clear error, exactly as today.

The USB helper is optional in a package. It links Apple's AccessoryAccess framework, which is in the macOS 27 SDK but not in every Xcode a hosted CI runner has. scripts/package-macos.sh checks that the SDK can import it and, when it cannot (or with FLUXVM_SKIP_USBD=1), builds the package without FluxVMUSBAccess.app and its launch agent and says so; the release workflow's summary notes it too. Everything else in the package is unchanged, and USB passthrough then needs crates/fluxvm-apple/macos/usbd/build-install.sh on a Mac with the SDK (and, for claiming devices, a profile).

One-time Apple setup​

  1. Developer ID Application certificate for the team, in Certificates, Identifiers & Profiles. Export it with its private key as a .p12.
  2. Ask Apple for the restricted entitlements.
    • com.apple.vm.networking is granted on request, per team, through Apple's entitlement request form for virtualization networking. Explain the use: a VM manager that attaches guests to vmnet and bridged networks.
    • Accessory Access USB is a capability you enable for the App ID once your team has it.
    • Neither can be self-granted, and approval can take a while.
  3. App IDs (explicit, not wildcard): dev.zyvor.fluxvm.runner, dev.zyvor.fluxvm.vmnetd and dev.zyvor.fluxvm.usbd. Enable the granted capability on each.
  4. Provisioning profiles of type Developer ID for each App ID. Download the three .provisionprofile files.
  5. Notarization credentials. Either an App Store Connect API key (.p8, key id, issuer id), or xcrun notarytool store-credentials for local use.

Building a signed package locally​

export FLUXVM_SIGN_IDENTITY="Developer ID Application: Your Org Name (TEAMID)"
export FLUXVM_PROFILE_RUNNER=~/profiles/FluxVM_Runner.provisionprofile
export FLUXVM_PROFILE_VMNETD=~/profiles/FluxVM_Networking.provisionprofile
export FLUXVM_PROFILE_USBD=~/profiles/FluxVM_USB.provisionprofile
export FLUXVM_NOTARIZE=1 FLUXVM_NOTARY_PROFILE=fluxvm-notary
scripts/package-macos.sh --version 0.5.0

Do not commit real Team IDs, certificate subjects, .p12 / .p8 material, or provisioning profiles.

Before signing, the script checks each profile:

  • it was issued for the right bundle id;
  • it grants the entitlement being claimed;
  • it belongs to the same team as the identity.

It then:

  1. writes the entitlements, including com.apple.application-identifier and com.apple.developer.team-identifier;
  2. signs with the hardened runtime and a secure timestamp;
  3. verifies every signature;
  4. notarizes the whole package and staples the three bundles.

Each profile is optional. Leave one out and that bundle is built without its restricted entitlement.

In CI​

.github/workflows/release-macos.yml signs and notarizes when these repository secrets exist. Without them it builds the ad hoc package as before, so forks still work.

SecretContent
MACOS_DEVELOPER_ID_P12base64 of the Developer ID Application .p12
MACOS_DEVELOPER_ID_P12_PASSWORDits password
MACOS_PROFILE_RUNNER, MACOS_PROFILE_VMNETD, MACOS_PROFILE_USBDbase64 of each .provisionprofile (each optional)
MACOS_NOTARY_KEY_P8base64 of the App Store Connect API key
MACOS_NOTARY_KEY_ID, MACOS_NOTARY_ISSUERits key id and issuer id

The certificate goes into a temporary keychain that is deleted at the end of the job. The smoke test then:

  • verifies every bundle;
  • runs stapler validate and spctl on each bundle when the package was notarized;
  • checks that FluxVMRunner.app carries com.apple.vm.networking.

Installing the helpers from a package​

FLUXVM_PACKAGE_DIR=/opt/homebrew/opt/fluxvm crates/fluxvm-apple/macos/vmnetd/build-install.sh
FLUXVM_PACKAGE_DIR=/opt/homebrew/opt/fluxvm crates/fluxvm-apple/macos/usbd/build-install.sh
fluxctl vz host # vm_networking_entitlement should now be true

Run without FLUXVM_PACKAGE_DIR, the scripts still build and sign ad hoc from source for development. The broker is then signed with the virtualization entitlement only, so it starts, but VMs cannot use its networks.

Status (2026-10-16)​

PieceState
Developer ID Application certPresent on the build Mac (do not publish the identity string or Team ID; check locally with security find-identity -v -p codesigning)
Local Developer ID packageBuilt and notarized for 0.4.11 (scripts/package-macos.sh with FLUXVM_NOTARIZE=1). Apps staple; fluxctl uses disable-library-validation for Homebrew libhivex
Notary credentialsAvailable on the release Mac (notarytool keychain profile). Wire MACOS_NOTARY_* + P12 into GitHub Actions when CI should notarize
Restricted entitlement profilesMissing — dev.zyvor.fluxvm.runner / .vmnetd / .usbd Developer ID .provisionprofile files not installed; vmnet/bridge/physical USB stay admission-refused
GitHub Actions secretsWire when material exists: MACOS_DEVELOPER_ID_P12, MACOS_DEVELOPER_ID_P12_PASSWORD, optional MACOS_PROFILE_*, MACOS_NOTARY_KEY_P8, MACOS_NOTARY_KEY_ID, MACOS_NOTARY_ISSUER (see table above)

Once the notary API key is in secrets (or xcrun notarytool store-credentials locally), rebuild with FLUXVM_NOTARIZE=1, confirm stapler validate / spctl -a -t exec on the bundles, attach the tarball to the GitHub release, and refresh the Homebrew formula sha256.

Production gates after profiles land: fluxctl vz host, a VM on apple.vmnet, two VMs on one named network through the broker, and USB passthrough — macos-vz27-remaining-complete.md.

Mac App Store — non-goal​

FluxVM is not submitted to the Mac App Store. Distribution is Developer ID + notarization via GitHub releases and Homebrew (zyvorai/tap/fluxvm).

MAS is a poor fit for this product: App Sandbox is required and incompatible with the hypervisor control plane; packaging is a multi-binary CLI/daemon with LaunchAgent helpers, not a sandboxed single .app; restricted entitlements (com.apple.vm.networking, Accessory Access USB) follow the Developer ID profile path documented above; App Store Connect appears here only as the notarytool credential source.