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:
| Feature | Entitlement | Which binary |
|---|---|---|
apple.vmnet networks, the shared vmnet broker, tenant networks (isolate_tenant_networks) | com.apple.vm.networking | the runner, and the broker fluxvm-vmnetd |
apple.bridge_interface (VMs directly on the LAN) | com.apple.vm.networking | the runner |
| Physical USB passthrough (Accessory Access) | com.apple.developer.accessory-access.usb | FluxVMUSBAccess.app |
macOS honours a restricted entitlement only when three things are all true:
- the executable is signed with a Developer ID certificate;
- it sits in an app bundle that embeds a provisioning profile granting that entitlement;
- 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
- Developer ID Application certificate for the team, in Certificates, Identifiers & Profiles. Export it with its
private key as a
.p12. - Ask Apple for the restricted entitlements.
com.apple.vm.networkingis 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.
- App IDs (explicit, not wildcard):
dev.zyvor.fluxvm.runner,dev.zyvor.fluxvm.vmnetdanddev.zyvor.fluxvm.usbd. Enable the granted capability on each. - Provisioning profiles of type Developer ID for each App ID. Download the three
.provisionprofilefiles. - Notarization credentials. Either an App Store Connect API key (
.p8, key id, issuer id), orxcrun notarytool store-credentialsfor 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:
- writes the entitlements, including
com.apple.application-identifierandcom.apple.developer.team-identifier; - signs with the hardened runtime and a secure timestamp;
- verifies every signature;
- 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.
| Secret | Content |
|---|---|
MACOS_DEVELOPER_ID_P12 | base64 of the Developer ID Application .p12 |
MACOS_DEVELOPER_ID_P12_PASSWORD | its password |
MACOS_PROFILE_RUNNER, MACOS_PROFILE_VMNETD, MACOS_PROFILE_USBD | base64 of each .provisionprofile (each optional) |
MACOS_NOTARY_KEY_P8 | base64 of the App Store Connect API key |
MACOS_NOTARY_KEY_ID, MACOS_NOTARY_ISSUER | its 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 validateandspctlon each bundle when the package was notarized; - checks that
FluxVMRunner.appcarriescom.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)
| Piece | State |
|---|---|
| Developer ID Application cert | Present 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 package | Built 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 credentials | Available on the release Mac (notarytool keychain profile). Wire MACOS_NOTARY_* + P12 into GitHub Actions when CI should notarize |
| Restricted entitlement profiles | Missing — dev.zyvor.fluxvm.runner / .vmnetd / .usbd Developer ID .provisionprofile files not installed; vmnet/bridge/physical USB stay admission-refused |
| GitHub Actions secrets | Wire 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.