Managing a Mac with Nix (and Jamf)#

This guide covers bringing up a Mac as a nix-darwin host in this flake, and — for corporate-managed machines — how to make Nix a delight to use without stepping on what Jamf (MDM) is doing. The running example is work-laptop-m4, a Jamf-managed MacBook Pro where the primary user has root.

The Adding a New Host guide is written for the NixOS install flow (disko, nixos-install, nixos-anywhere). macOS is different enough — no disko, an existing OS, Determinate Nix, and MDM in the picture — that it gets its own page here.


Division of responsibility: Nix vs Jamf#

The single most important thing to understand: MDM “managed preferences” win. When Jamf pushes a configuration profile, macOS reads those values from /Library/Managed Preferences/ and they take precedence over anything written with defaults write — which is exactly how nix-darwin’s system.defaults are applied. So if you set a preference in a domain Jamf also manages, your value is silently ignored. It’s harmless, but it’s config drift that shows up as “why isn’t my setting sticking?” and can read as tampering in a compliance audit.

The clean split is to let each tool own the layer it’s good at:

Layer Owner Examples
Security & compliance posture Jamf FileVault + key escrow, Application Firewall, Gatekeeper/XProtect, software-update policy & OS-version enforcement, passcode policy, corp root CAs (SCEP), 802.1X / VPN profiles, iCloud/AirDrop restrictions, EDR/telemetry agents
Developer & productivity layer nix-darwin + home-manager fish + starship + helix + tmux, the CLI toolbelt (eza/fd/fzf/ripgrep/zoxide/jq/…), Touch-ID sudo, Caps-Lock→Escape + key-repeat rates, git/gh/ssh/gnupg/yubikey config, a curated set of Homebrew CLI formulae

Rule of thumb: if it’s a security control, let Jamf keep it. If it’s your shell, your editor, or a dev CLI, let Nix own it. Don’t assert a preference in a Jamf-managed domain just because nix-darwin can — you’ll only create drift.

Sharp edges the current config carries#

work-laptop-m4 deliberately mirrors the old work-laptop config, so it inherits a few things worth being conscious of on a managed machine:

  • homebrew.onActivation.cleanup = "uninstall" — this only ever touches Homebrew-managed packages, not Jamf-deployed .pkg/VPP apps. Keep the casks list empty and manage only CLI brews, so a rebuild can never uninstall an app Jamf deployed via Homebrew.
  • SoftwareUpdate.AutomaticallyInstallMacOSUpdates = true (from the shared apple module) — if Jamf enforces an update/deferral policy, the managed profile wins and this assertion is a no-op. Harmless; just don’t rely on it.
  • Personal FalseBlue root CA in the System Keychain — the shared darwin security module runs security add-trusted-cert for the personal FalseBlue root and adds the personal PKI to the OpenSSL bundle (SSL_CERT_FILE, NIX_SSL_CERT_FILE, …). This is needed to trust the personal Wi-Fi/EAP and the personal Nix binary cache, but it does put a personal trust anchor on a corporate device. That’s a defensible trade-off for a machine you fully control, but an auditor may flag it — know it’s there.

These are behaviours of the shared darwin modules, applied to every non-catalina Mac. We chose to mirror rather than add per-host gating; if MDM conflicts ever surface in practice, the fix is to add a tsunaminoai.apple.* toggle to gate the specific domain.

What Nix gives you that Jamf usually doesn’t#

  • Touch-ID for sudo (security.pam.services.sudo_local.touchIdAuth) — survives OS updates, no pam.d hand-editing.
  • A fully declarative, reproducible shell/editor/CLI environment identical to every other host in the fleet.
  • darwin-rebuild switch as a single, auditable, version-controlled change surface for the developer layer — much easier to reason about than a pile of manual defaults and brew installs.

First-boot bring-up#

Do this once, on the new Mac, after the host config + git-email changes have landed on master.

1. Install Determinate Nix#

Install Determinate Nix (the flake uses the determinate darwin module; nix.enable = false — the daemon is managed by Determinate, not nix-darwin). Then clone this flake locally.

2. Preserve secret access#

Copy bcraton’s existing age key from the old machine so SOPS-encrypted home secrets keep working under the same identity (no new user recipient needed):

# from the old machine
scp ~/.config/sops/age/keys.txt new-mac:~/.config/sops/age/keys.txt

3. Reconcile the nixbld IDs#

nix-darwin asserts the _nixbld user/group IDs match reality; the Determinate installer may have picked different numbers than the old machine’s 350. Read the real values and update hosts/aarch64-darwin/work-laptop-m4/default.nix if they differ (shared default is 300/30000):

dscl . -read /Groups/nixbld PrimaryGroupID
dscl . -read /Users/_nixbld1 UniqueID

4. Register the host SOPS age key#

The darwin host key decrypts system secrets (e.g. the Borg passphrase). Derive its age key and add it as a recipient:

cat /etc/ssh/ssh_host_ed25519_key.pub | nix run nixpkgs#ssh-to-age   # → age1…

In .sops.yaml:

# under the hosts: anchor
- &work-laptop-m4 age1…

# in the secrets.yaml$ creation-rule age: group
- *work-laptop-m4

Then re-key and commit:

sops updatekeys secrets.yaml
git add .sops.yaml secrets.yaml && git commit -m "feat: add sops age key for work-laptop-m4"

5. Borg backups#

just borg-generate-secrets work-laptop-m4

Register the printed public key in BorgWarehouse, take the repo id, and paste it into tsunaminoai.borg.repo in the host config.

6. Set the hostname and build#

Set networking.hostName from the machine’s serial (MacBook-Pro-<last-4-of-serial> to match the fleet convention), then activate:

sudo darwin-rebuild switch --flake .#work-laptop-m4

Deploy-rs#

Darwin hosts are auto-discovered as deploy nodes (they deploy over the builder user, not root — see Deployment). Laptops are usually managed with local darwin-rebuild switch rather than being push targets, so adding this host to tsunaminoai.deploy.monitoredHosts is optional.


Verification#

  • sudo -v prompts for Touch ID.
  • ls /run/secrets shows the decrypted system secrets; borgmatic check connects.
  • fish, starship, and hx are present and themed.
  • git config user.email shows bcraton@onboardmeetings.com.
  • No macOS preference you set in system.defaults is being overridden by a file in /Library/Managed Preferences/ (if one is, that domain belongs to Jamf — stop asserting it in Nix).