nobe4 / Posts / NixOS on an Intel macBook _

 |  Tech Nix

I had a MacBook Pro sitting unused. An Intel, with an Apple T2 security chip. I thought with NixOS on it, it might actually be useful again.

Here’s how that journey went.

The wrong ISO

My first attempt went like this:

The T2 chip blocks standard Linux kernels. I learned the hard way that the vanilla NixOS ISO doesn’t include the T2 drivers.

The right ISO

The t2linux project maintains patched kernels and ISOs for T2 Macs. They have a NixOS-specific ISO with all the drivers baked in.

Once more:

The brcm firmware

T2 Macs use a Broadcom Wi-Fi chip. Linux needs the firmware blobs that Apple doesn’t distribute for Linux. The t2linux firmware script extracts them from macOS.

I used Method 2: run the script on macOS, which creates a firmware.tar in ~/Downloads. Then copy the tarball to the NixOS installer.

The NixOS config then installs the firmware at build time:

hardware.firmware = [
  (pkgs.stdenvNoCC.mkDerivation (final: {
    name = "brcm-firmware";
    src = ./firmware.tar;
    dontUnpack = true;
    installPhase = ''
      mkdir -p $out/lib/firmware/brcm
      tar -xf ${final.src} -C $out/lib/firmware/brcm
    '';
  }))
];

Keep firmware.tar in the NixOS host dir so rebuilds pick it up.

Installation

I followed the t2linux NixOS guide. Two critical warnings:

So, I used macOS Disk Utility to carve out the disk: 100GB for macOS, 900GB for NixOS.

The setup is pretty standard after running nixos-install.

The installer config pulls in T2 hardware support and the Wi-Fi firmware:

{ config, lib, pkgs, ... }:

{
  imports = [
    ./hardware-configuration.nix
    "${builtins.fetchGit { url = "https://github.com/NixOS/nixos-hardware.git"; }}/apple/t2"
  ];

  hardware.firmware = [
    (pkgs.stdenvNoCC.mkDerivation (final: {
      name = "brcm-firmware";
      # ...
    }))
  ];

  boot.loader.systemd-boot.enable = true;
  boot.loader.efi.efiSysMountPoint = "/boot";

  networking.networkmanager.enable = true;

  environment.systemPackages = with pkgs; [ vim wget git ];

  services.openssh = {
    enable = true;
    settings.PermitRootLogin = "yes";
  };

  system.stateVersion = "26.11";
}

I configured Wi-Fi manually, enabled SSH, and switched to managing it remotely.

Remote rebuild

I pulled the config into my dotfiles and ran:

nixos-rebuild switch --target-host root@10.0.0.10 \
  -I nixos-config=./configuration.nix \
  -I nixpkgs=channel:nixos-unstable

This broke in a few ways. Each one taught me something about nixos-rebuild.

SSH drops mid-switch

nixos-rebuild switch activates the new config live. It restarts NetworkManager and dbus-broker, which kills the SSH connection.

But we can prevent those services from restarting during activation:

systemd.services.NetworkManager.stopIfChanged = false;
systemd.services.dbus-broker.stopIfChanged = false;

Chicken-and-egg on first deploy

The stopIfChanged fix only takes effect after it’s deployed. The first deploy still uses the old activation config and drops SSH.

The fix is to run boot instead of switch for the first deploy. It installs the config but doesn’t activate it. Then reboot manually:

nixos-rebuild boot --target-host root@10.0.0.10 \
  -I nixos-config=./configuration.nix \
  -I nixpkgs=channel:nixos-unstable

ssh root@10.0.0.10 reboot

After reboot, the stopIfChanged settings are active. Future switch commands work fine.

Wrong hardware-configuration.nix

The config used to import /etc/nixos/hardware-configuration.nix as an absolute path. Since the build runs locally, it used the local hardware config. The target booted into “timed out waiting for device” because of wrong disk UUIDs.

I had to copy the target’s hardware config into the local machine:

scp root@10.0.0.10:/etc/nixos/hardware-configuration.nix \
  ./hardware-configuration.nix

Then use a relative import: ./hardware-configuration.nix.

Stale transient unit

After interrupting a failed switch, systemd left behind a stale service unit:

Failed to start transient service unit: Unit
nixos-rebuild-switch-to-configuration.service was already loaded

The fix was to manually stop it on the target:

systemctl stop nixos-rebuild-switch-to-configuration.service

Finishing up

After the rebuild landed, I set up the user:

ssh root@10.0.0.10 passwd nobe4
ssh root@10.0.0.10 "su - nobe4 -c \"ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N '' -C nobe4@10.0.0.10\""

Then disabled root SSH by dropping the authorized key from the config, replacing it instead by the user’s authorizedKeys.

  users.users.nobe4 = {
    openssh.authorizedKeys.keys = [
      # ...
    ];
  };

Default boot

T2 Macs use Apple’s boot manager, not standard UEFI boot order. Installing a bootloader to the EFI partition isn’t enough. The T2 chip keeps macOS as the default unless you explicitly bless another .efi file.

Running bless from macOS failed with 0xe00002e2: the T2 chip blocks boot device changes from the full OS. I had to do it from Recovery mode, which has higher privilege:

Boot and hold Cmd-R, then run:

diskutil list
# find the ~300MB EFI partition, here disk0s1

sudo mkdir -p /Volumes/EFI
sudo mount -t msdos /dev/disk0s1 /Volumes/EFI
sudo bless --mount /Volumes/EFI \
  --setBoot \
  --file /Volumes/EFI/EFI/systemd/systemd-bootx64.efi

After that, the Mac boots straight into NixOS.