Introduction
Building a Windows agent is relatively straightforward—MSI installer, Windows Service, done. macOS is a different beast entirely. Apple’s security requirements mean every piece of software needs to be signed, notarized, and packaged in very specific ways. This is the story of how we built, signed, and ship the InventoryOS macOS agent.
Why macOS Is Different
If you’ve shipped on Windows before, the macOS story will feel unfamiliar. Apple tightened the screws with Gatekeeper in Catalina, and it’s only gotten stricter since. Here’s what you’re up against:
- Gatekeeper blocks unsigned or unnotarized apps by default. Users see that scary “this app is from an unidentified developer” warning, and many will bail rather than right-click → Open.
- Enterprise deployment tools like Jamf and Munki still require proper signing. Even if your users are tech-savvy, MDM admins expect a clean, signed package they can push silently.
- System extensions and background processes need special entitlements. The InventoryOS agent runs as a LaunchDaemon—a background daemon that starts at boot—and that comes with its own set of rules.
- macOS Ventura and later added even more restrictions on background agents. If you skip hardening or entitlements, things break in subtle ways on newer systems.
The bottom line: you can’t just compile a binary and zip it up. You need certificates, signing, notarization, and a proper .pkg installer. Here’s how we do it.
The Apple Developer Program
First, you need an Apple Developer account. That’s $99/year. No way around it.
From there, you generate two key certificates in the Certificates, Identifiers & Profiles section:
- Developer ID Application—this signs the app binary itself. Every executable, framework, and helper inside your app bundle (or, in our case, the daemon binary) needs to be signed with this.
- Developer ID Installer—this signs the
.pkginstaller. Different cert, different job. Don’t mix them up.
We store these certificates securely in CI/CD as encrypted secrets in GitHub Actions. Never commit them or leave them on a developer machine. If a cert leaks, Apple can revoke it, and you’re back to square one.
Code Signing the Agent
The InventoryOS agent is a background daemon written to run as a LaunchDaemon. It has no UI. It lives in /Library/Application Support/InventoryOS/ and starts at boot. Signing it is straightforward once you know the flags.
Conceptually, you run codesign on the binary with your Developer ID Application certificate. The important bits:
- --deep signs nested frameworks and helpers. If your binary embeds or loads other binaries, they all need to be signed. Miss one and notarization will fail.
- --options runtime enables the “hardened runtime,” which is required for notarization. Without it, Apple will reject your submission.
- You point to an entitlements file that declares what the agent is allowed to do: network access, file system access, etc. Apple reviews these. Request too much and you’ll get rejected; request too little and the agent won’t function.
After signing, verify with:
codesign --verify --verbose /path/to/InventoryOS-Agent
If that passes, you’re good. If not, you’ve got an unsigned nested binary or a broken entitlements setup.
Notarization
Notarization is Apple’s automated security check. They scan your binary for malware, check the signature, and attach a “ticket” that tells Gatekeeper the app is safe. Users never see this unless something goes wrong.
We submit via notarytool (which replaced altool in Xcode 14+). You upload the signed .pkg, wait for Apple’s response (usually 2–5 minutes, sometimes longer under load), and then staple the notarization ticket to the package so it works offline:
xcrun notarytool submit InventoryOS-Agent.pkg --wait xcrun stapler staple InventoryOS-Agent.pkg
What happens when notarization fails? Common culprits:
- An unsigned nested binary (a helper or framework you forgot to sign)
- Missing entitlement (e.g. you need
com.apple.security.network.clientbut didn’t declare it) - Hardened runtime not enabled (forgot
--options runtime)
Apple returns a JSON payload with the exact issue. Fix it, re-sign, resubmit. Rinse and repeat until it passes.
Packaging as a .pkg
macOS uses .pkg installers for enterprise deployment, not .dmg. DMGs are fine for manual drag-to-Applications installs, but Jamf, Munki, and MDM tools expect a proper package.
We use two tools:
- pkgbuild creates the package: install location, scripts (
preinstall,postinstall), and the signed binary. - productbuild wraps it into a distribution package, which is what MDM systems expect for deployment.
The postinstall script does the heavy lifting: it registers the LaunchDaemon plist, starts the agent, and sets the enrollment key (injected at build time per-org). The agent then phones home and registers with the InventoryOS backend.
Finally, sign the .pkg with the Developer ID Installer certificate. That signature is what Gatekeeper checks when a user (or MDM) runs the installer.
Distribution
We distribute the macOS agent in three ways:
- Direct download from the InventoryOS dashboard. Each org gets a per-org download link with the enrollment key baked in. Install, and the agent auto-registers.
- MDM deployment via Jamf or Munki. Admins upload the
.pkg, assign it to device groups, and push silently. The agent installs at next check-in. - Auto-updates: the agent updates itself via a secure update channel. We ship new signed, notarized builds; the agent fetches and installs them in the background.
Lessons Learned
After a few iterations, here’s what we wish we’d known from day one:
- Always test on a clean macOS VM before shipping. Your dev machine has all kinds of exceptions and keychain entries. A fresh Monterey, Ventura, or Sonoma VM will surface issues your Mac won’t.
- Apple revokes notarization if they detect issues post-release. Monitor your Apple Developer email. If you get a revocation notice, you need to fix the binary and resubmit immediately.
- Certificate expiration is a real threat. Developer ID certs typically last a year. Set calendar reminders for renewal. If your cert expires mid-release, you’re blocked until you renew.
- Different macOS versions have different quirks. Test on at least the last three major versions. Ventura tightened background agent rules; Sonoma changed something in the network stack. Don’t assume what works on one version works on all.
- Keep your CI/CD pipeline reproducible. If you can’t rebuild the exact same signed package from source, you have a problem. Version your build scripts, lock Xcode versions, and document every step.
Conclusion
macOS code signing and notarization is painful the first time, but once your pipeline is solid, it’s seamless. The InventoryOS macOS agent ships as a signed, notarized .pkg that installs cleanly on any Mac from Monterey onwards. No Gatekeeper warnings, no “unidentified developer” popups—just a clean, silent install.