No description
Find a file
2026-08-13 19:38:37 +01:00
hosts various 2026-08-11 15:47:28 +01:00
lib updoots 2026-08-02 16:18:54 +01:00
modules updates 2026-08-13 19:38:37 +01:00
secrets shite 2026-08-06 21:53:05 +01:00
users updoots 2026-08-02 16:18:54 +01:00
.envrc updoots 2026-08-02 16:18:54 +01:00
.gitignore updoots 2026-08-02 16:18:54 +01:00
.sops.yaml stuff 2026-08-03 14:43:34 +01:00
flake.lock updates 2026-08-13 19:38:37 +01:00
flake.nix shite 2026-08-06 21:53:05 +01:00
README.md remnove vscode icon 2026-08-06 16:04:55 +01:00

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 @root restored from @root-blank on 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.