- Nix 100%
| hosts | ||
| lib | ||
| modules | ||
| secrets | ||
| users | ||
| .envrc | ||
| .gitignore | ||
| .sops.yaml | ||
| flake.lock | ||
| flake.nix | ||
| README.md | ||
James's Nix Configuration
Nix configuration for an Apple Silicon MacBook Air and an x86_64 NixOS desktop. nix-darwin and NixOS own machine configuration; Home Manager owns the shared user environment.
Hosts
| Flake output | Platform | Role |
|---|---|---|
james-macbook-air |
aarch64-darwin |
nix-darwin laptop |
james-desktop |
x86_64-linux |
NixOS GNOME desktop with NVIDIA graphics |
Structure
.
├── flake.nix
├── hosts/
│ ├── macbook-air/ # nix-darwin, macOS defaults, and Homebrew
│ └── desktop/ # NixOS, GNOME, NVIDIA, storage, and hardware
├── modules/
│ ├── fonts/ # Shared system fonts
│ ├── home/ # Shared Home Manager programs and services
│ ├── nix/ # Shared Nix settings and Darwin additions
│ └── system/ # Shared system shell configuration
├── users/ # OS account data and public SSH keys
└── .sops.yaml # SOPS recipient policy
Common Commands
# macOS
sudo darwin-rebuild switch --flake .#james-macbook-air
# NixOS
sudo nixos-rebuild switch --flake .#james-desktop
# Development and validation
nix develop
nix fmt
nix flake check
# Evaluate both hosts without building them
nix eval .#darwinConfigurations.james-macbook-air.system.drvPath
nix eval .#nixosConfigurations.james-desktop.config.system.build.toplevel.drvPath
NixOS Installation
Before booting the installer, make this exact working tree available through a commit or external media. From the minimal ISO, connect networking, enter the repository root, and complete these checks before erasing anything:
set -euo pipefail
[ -d /sys/firmware/efi/efivars ] || {
printf '%s\n' "Installer is not booted in UEFI mode" >&2
exit 1
}
sudo test -w /sys/firmware/efi/efivars
curl --fail --head https://cache.nixos.org/
test -f flake.nix
test -f secrets/id_ed25519_sk.json
nix eval .#nixosConfigurations.james-desktop.config.system.build.toplevel.drvPath
lspci -nn | grep -i nvidia
Confirm that the reported NVIDIA GPU supports the open kernel module before continuing.
The desktop uses /dev/nvme0n1, declared in hosts/desktop/disko.nix, with
this layout:
- 2 GiB unencrypted EFI system partition mounted at
/boot - LUKS2 partition using FIDO2 YubiKey unlock with passphrase/recovery fallback
- LVM inside LUKS with a Btrfs root and 20 GiB swap logical volume
- Persistent Btrfs subvolumes for
/home,/nix, and/persist - An ephemeral
@rootrestored from@root-blankon each normal boot
The swap volume is slightly larger than the installed 16 GiB RAM and is used for hibernation. Root and swap are encrypted; the EFI partition is not. Secure Boot is not currently configured. Discard/TRIM is passed through LUKS for SSD maintenance, which exposes encrypted-block allocation patterns.
The OpenPGP PIN used for SOPS and the FIDO2 PIN used for disk unlock are separate. Before installation, verify the YubiKey's FIDO2 application and set a PIN if it does not already have one:
nix shell nixpkgs#yubikey-manager -c ykman fido info
nix shell nixpkgs#yubikey-manager -c ykman fido access change-pin
Do not run change-pin if a FIDO2 PIN is already configured unless you intend
to change it.
Disko creates the partition table, encrypted container, logical volumes,
filesystems, subvolumes, and read-only blank-root snapshot. The following
command erases /dev/nvme0n1. Verify the device name before running it from the
NixOS installer:
set -euo pipefail
DISK=/dev/nvme0n1
sudo lsblk -o NAME,SIZE,MODEL,SERIAL,FSTYPE,MOUNTPOINTS "$DISK"
sudo nix run github:nix-community/disko -- \
--mode destroy,format,mount \
--flake .#james-desktop
Disko asks for confirmation before erasing the drive and prompts twice for the
initial LUKS passphrase. After it finishes, /mnt contains the mounted target
filesystems. Enroll the recovery key and both YubiKeys while the LUKS container
is open:
set -euo pipefail
CRYPT_PART=/dev/disk/by-partlabel/james-desktop-cryptroot
# Store the printed recovery key somewhere offline.
sudo systemd-cryptenroll "$CRYPT_PART" --recovery-key
# Insert the primary YubiKey.
sudo systemd-cryptenroll "$CRYPT_PART" \
--fido2-device=auto \
--fido2-with-client-pin=yes \
--fido2-with-user-presence=yes
# Remove it, insert the backup YubiKey, and repeat the enrollment.
sudo systemd-cryptenroll "$CRYPT_PART" \
--fido2-device=auto \
--fido2-with-client-pin=yes \
--fido2-with-user-presence=yes
The checked-in hardware-configuration.nix is a placeholder. Generate the
real hardware configuration after mounting the filesystems:
sudo nixos-generate-config --no-filesystems --root /mnt
cp /mnt/etc/nixos/hardware-configuration.nix hosts/desktop/hardware-configuration.nix
Disko owns fileSystems, swapDevices, and boot.initrd.luks settings. Keep
boot.initrd.availableKernelModules,
boot.initrd.kernelModules, boot.kernelModules, boot.extraModulePackages,
CPU microcode settings, and nixpkgs.hostPlatform.
Review the merge and evaluate the target again before installation:
grep -q 'boot.initrd.availableKernelModules' hosts/desktop/hardware-configuration.nix
git diff -- hosts/desktop/hardware-configuration.nix
nix eval .#nixosConfigurations.james-desktop.config.system.build.toplevel.drvPath
The host otherwise assumes UEFI/systemd-boot, core GNOME on Wayland, Bluetooth, fwupd, rootless Podman with Docker compatibility, and NVIDIA's open kernel module.
Ephemeral Root
On each normal boot, @root is deleted and restored from the read-only empty
@root-blank snapshot. /home, /nix, and /persist remain intact. The
impermanence module also preserves machine identity, NetworkManager
connections, Bluetooth pairings, logs, NixOS/systemd state, and UPower state
under /persist.
Anything else written outside the persistent mounts is discarded at the next
boot. Put additional machine state in hosts/desktop/impermanence.nix rather
than relying on it remaining in the root filesystem.
To inspect or repair the current root without resetting it, edit the
systemd-boot entry at boot and append impermanence=keep to the kernel command
line.
YubiKey Login
GDM, the GNOME lock screen, TTY login, sudo, and Polkit accept the YubiKey's
FIDO2 PIN plus touch. The SOPS-encrypted james password hash provides fallback
authentication. Disk unlock and desktop login are separate authentications, so
a cold boot using a YubiKey requires two FIDO2 PIN-and-touch operations.
The PAM credentials are registered after nixos-install, as described below.
Secrets Bootstrap
SOPS uses the YubiKey-backed PGP key
77A83BD1E7FFF92A3397C63DB19727ABB43AAEE7 as the administrator and recovery
recipient. Each host also uses a local age identity for unattended activation.
The Mac identity is stored with mode 0600 at
~/Library/Application Support/sops/age/keys.txt; back it up to encrypted
storage and never commit it.
Create the desktop identity in the persistent subvolume after mounting the target filesystem:
set -euo pipefail
sudo install -d -m 0700 /mnt/persist/var/lib/sops-nix
sudo nix shell nixpkgs#age -c age-keygen -o /mnt/persist/var/lib/sops-nix/key.txt
sudo chmod 0600 /mnt/persist/var/lib/sops-nix/key.txt
DESKTOP_RECIPIENT=$(sudo nix shell nixpkgs#age -c age-keygen -y /mnt/persist/var/lib/sops-nix/key.txt)
printf '%s\n' "$DESKTOP_RECIPIENT"
Replace the commented desktop recipient in .sops.yaml with the printed
age1... value. A clean installer must import the public OpenPGP certificate
and associate its secret-key stubs with the YubiKey before SOPS can rewrap the
existing files:
set -euo pipefail
export GNUPGHOME=$(mktemp -d)
chmod 0700 "$GNUPGHOME"
trap 'rm -rf "$GNUPGHOME"' EXIT
nix shell nixpkgs#gnupg -c gpg --import \
users/james/gpg/77A83BD1E7FFF92A3397C63DB19727ABB43AAEE7.asc
nix shell nixpkgs#gnupg -c gpg --card-status
nix shell nixpkgs#gnupg -c gpg --list-secret-keys \
--with-subkey-fingerprint 77A83BD1E7FFF92A3397C63DB19727ABB43AAEE7
for file in secrets/*.json; do
nix shell nixpkgs#sops nixpkgs#gnupg -c sops updatekeys -y "$file"
sudo env SOPS_AGE_KEY_FILE=/mnt/persist/var/lib/sops-nix/key.txt \
nix shell nixpkgs#sops -c sops --decrypt "$file" >/dev/null
done
Do not run nixos-install until both decryptions succeed. This proves that the
new desktop identity is present in each encrypted file.
The deployed desktop reads its identity from
/persist/var/lib/sops-nix/key.txt; the private age identity must never be
committed. Back it up to encrypted offline storage, or retain tested access to
the PGP decryption key as the recovery route.
The encrypted SSH key handles under secrets/ are deployed to the NixOS host
as /run/secrets/ssh-primary and /run/secrets/ssh-backup. On macOS, SOPS
manages ~/.ssh/id_ed25519_sk and ~/.ssh/id_ed25519_sk_backup. Public SSH keys
remain in Git.
The staged deletion prevents future commits from including plaintext key handles, but the configured remote already contains them in existing Git history. Rewrite the remote history and clear old server-side objects before treating that exposure as remediated.
First Desktop Build
After replacing the hardware configuration and adding the desktop age recipient:
sudo nixos-install --flake .#james-desktop
Before rebooting, register both YubiKeys for PAM. The configured origin must match these commands exactly. Keep only the key being registered connected:
set -euo pipefail
sudo install -d -m 0700 -o 1000 -g 100 /mnt/home/james/.config
sudo install -d -m 0700 -o 1000 -g 100 /mnt/home/james/.config/Yubico
# Insert the primary YubiKey.
PRIMARY_CREDENTIAL=$(nix shell nixpkgs#pam_u2f -c pamu2fcfg \
-u james \
-o pam://james-desktop \
-i pam://james-desktop \
-N)
# Remove it, insert the backup YubiKey, then register without a username prefix.
BACKUP_CREDENTIAL=$(nix shell nixpkgs#pam_u2f -c pamu2fcfg \
-u james \
-o pam://james-desktop \
-i pam://james-desktop \
-N \
-n)
test -n "$PRIMARY_CREDENTIAL"
test -n "$BACKUP_CREDENTIAL"
case "$PRIMARY_CREDENTIAL" in james:*) ;; *) exit 1 ;; esac
case "$BACKUP_CREDENTIAL" in james:*) exit 1 ;; esac
PAM_MAPPING=$(mktemp)
printf '%s:%s\n' "$PRIMARY_CREDENTIAL" "$BACKUP_CREDENTIAL" >"$PAM_MAPPING"
grep -Eq '^james:[^:]+:[^:]+$' "$PAM_MAPPING"
PAM_TARGET=/mnt/home/james/.config/Yubico/u2f_keys
sudo install -m 0600 -o 1000 -g 100 \
"$PAM_MAPPING" "${PAM_TARGET}.new"
sudo mv -f "${PAM_TARGET}.new" "$PAM_TARGET"
rm -f "$PAM_MAPPING"
Test the login PAM stack from the installed system once with each YubiKey
connected on its own:
sudo nixos-enter --root /mnt -c \
'nix shell nixpkgs#pamtester -c pamtester login james authenticate'
Do not reboot unless both tests succeed. The root password is declaratively
locked; james has a SOPS-encrypted fallback password. If both YubiKeys fail,
use that password or recover from the installer with the LUKS recovery key.
Passwordless PAM login cannot automatically unlock a password-protected GNOME
Keyring, which may prompt separately when first used. The SSH server is
disabled.
For offline PAM recovery, unlock and mount the persistent home from the ISO,
then repeat the registration block against /mnt/james/.config/Yubico/u2f_keys:
sudo cryptsetup open /dev/disk/by-partlabel/james-desktop-cryptroot cryptroot
sudo vgchange -ay james_desktop
sudo mount -o subvol=@home /dev/james_desktop/root /mnt
OpenPGP Smartcard
The desktop enables PC/SC, GnuPG smartcard udev rules, gpg-agent, scdaemon,
and GNOME Pinentry. Home Manager imports the public certificate for
77A83BD1E7FFF92A3397C63DB19727ABB43AAEE7; private operations remain on the
YubiKey.
After first login, verify signing and decryption access:
gpg --card-status
printf 'YubiKey test\n' | gpg --clearsign
Reboot Into NixOS
After both PAM tests succeed, leave any nixos-enter shell and cleanly unmount
the installation:
sudo swapoff /dev/james_desktop/swap
sudo umount -R /mnt
sudo vgchange -an james_desktop
sudo cryptsetup close cryptroot
sudo reboot
Remove the installation USB. A cold boot requires one FIDO2 PIN-and-touch for LUKS and another for GDM.
Post-Install Checks
Test the primary and backup YubiKeys independently for login, screen unlock,
and sudo. Then verify OpenPGP and firmware updates:
sudo true
gpg --card-status
printf 'YubiKey test\n' | gpg --clearsign
fwupdmgr get-devices
fwupdmgr refresh
fwupdmgr get-updates
Test suspend and hibernation before relying on them. To verify impermanence:
sudo touch /impermanence-test
touch ~/persistent-home-test
reboot
After reboot, /impermanence-test should be absent and
~/persistent-home-test should remain. Remove the home test file afterward.
Set system.stateVersion and Home Manager's home.stateVersion to the release
used for the initial installation and do not change them during routine
upgrades.