This repo contains flakectl, a wrapper to build, install, and update NixOS/nix-darwin/home-manager configurations:
nix run github:mirkolenz/infra -- --helpThis flake follows the dendritic pattern: every .nix file under modules/ is auto-loaded by import-tree (see flake.nix) and contributes to a flake.modules.<class>.<bucket> declaration, so the directory a file lives in is purely organizational.
Feature modules are grouped by the config-path namespace they write, mirroring the option tree (and the options/ layout), so anything is found where it sits in the configuration.
A module is one cohesive feature filed under one directory, the namespace of its primary subject, while keeping all of its config inline, so a feature like ssh or nix is configured end-to-end across home-manager and nixos/darwin in a single file.
- https://github.com/nix-community/disko/blob/master/docs/quickstart.md
- https://github.com/nix-community/disko/blob/master/docs/reference.md
# find device name and update this flake accordingly
ls -l /dev/disk/by-id
# format the disk
nix run github:mirkolenz/infra -- disko MACHINE_NAME --mode destroy,format,mount
# print hardware config and verify it is in sync with the flake
nixos-generate-config --no-filesystems --show-hardware-config
# set up user passwords
nix run github:mirkolenz/infra -- passwd --root /mnt MACHINE_NAME USER
# install the system
nix run github:mirkolenz/infra -- nixos-install MACHINE_NAMEThe NixOS manual recommends activating swap before nixos-install on machines with limited memory, since its builds may need quite a bit of RAM.
Swap partitions declared via disko are already activated by the mount mode.
Hosts with a swapfile can create a temporary one at its swapDevices path below /mnt after formatting the disk with disko.
NixOS recreates it on the first boot.
mkswap --file works on any filesystem and sets the nocow attribute required by btrfs.
mkswap --file /mnt/SWAPFILE --size 8G
swapon /mnt/SWAPFILE
# after nixos-install
swapoff /mnt/SWAPFILE
rm /mnt/SWAPFILELanzaboote replaces systemd-boot on UEFI hosts whose firmware can enroll custom keys via Setup Mode.
This excludes all Macs, as pre-T2 models lack Secure Boot and T2 models only trust Apple and Microsoft.
Replace boot.loader.systemd-boot.enable of the host with the following, modules/boot/default.nix provides the rest.
boot.lanzaboote = {
enable = true;
pkiBundle = "/var/lib/sbctl";
};- Put the firmware into Setup Mode via "Reset to Setup Mode", not "Clear All Keys".
- Switch and reboot, the first boot generates the keys and reboots again to enroll them.
- Verify with
bootctl status(Secure Boot: enabled (user)) andsudo sbctl verify.
To unlock a LUKS2 volume through the TPM2, also set boot.lanzaboote.measuredBoot.enable = true and add settings.crypttabExtraOpts = [ "tpm2-device=auto" ]; to its disko luks entry.
Check that /run/current-system/systemd/lib/systemd/systemd-pcrlock is-supported prints yes, then enroll once after Secure Boot is active.
Every switch afterwards updates the TPM policy, and the passphrase stays as recovery.
Drop --tpm2-with-pin=true for unattended hosts.
sudo systemd-cryptenroll --tpm2-device=auto --tpm2-with-pin=true --tpm2-pcrlock=/var/lib/systemd/pcrlock.json /dev/DEVICETo revert, disable Secure Boot in the firmware before switching back to systemd-boot.
- A warning about
/bootbeing world-readable is not an issue, the permissions are correctly set after a reboot. - If you get an error like
too many open filesduringnixos-install, try one of the following options:- Execute
ulimit -n 65535before installing to increase the number of open files for the current shell session - Use
--max-jobs 2to limit the number of parallel jobs (could also try with 1 max job)
- Execute
- If you get an error like
Getting status of /nix/store/...: No such file or directory, try the following:nix-store --verify --check-contents --repair
- Create an auth key, tagged
tag:serverfor servers: https://login.tailscale.com/admin/settings/keys - Save it on the machine to
./tsauth.txt - Run
sudo tailscale up --auth-key file:tsauth.txt && rm tsauth.txt - Run
sudo systemctl restart tailscaledto reapplyextraSetFlags
- Install Apple Developer Tools:
xcode-select --install - Install Homebrew
- Install Nix
- Sign into the App Store
- Enable Full Disk Access for terminal application
- Sign into 1Password and enable
Settings > Developer > Use the SSH agent
sudo nix run github:mirkolenz/infra
sudo rebootFor Macs that should not have Nix installed, the mirkos-macbook-rsync app copies a curated set of configuration files (Ghostty, SSH client, Homebrew bundle) from this flake to the remote machine via rsync over SSH.
The list of files and the brew bundle invocation are derived directly from darwinConfigurations.mirkos-macbook, so they stay in sync with the nix-darwin configuration.
Before running, make sure SSH key-based login to the remote works, see SSH Key Deployment.
Then sync the configs and apply any manual steps.
nix run .#mirkos-macbook-rsync -- USER@HOSTThe files themselves live in system.build.remoteConfig (see modules/hosts/mirkos-macbook/remote-config.nix), a ~-rooted tree that the script builds on demand.
To extend it with additional files, add an entry to the files attribute set there.
chsh -s /bin/zsh
# if nix is not in PATH, use this: /nix/var/nix/profiles/default/bin/nix
sudo nix run github:mirkolenz/infra#darwin-uninstaller
sudo /nix/nix-installer uninstallShell
- Copy
$HOME/.local/share/fish/fish_history
System Preferences
General > About: Set nameWallpaper: Solid color (black)Appearance > Allow wallpaper tinting in windows: OffKeyboard > Keyboard Shortcuts > Modifier Keys: Caps Lock -> OptionKeyboard > Keyboard Shortcuts > App Shortcuts: Disable allKeyboard > Keyboard Shortcuts > Spotlight: Disable all
Raycast
Settings > Advanced > Import/Export(also set up new schedule)Launch > Manage Fallback Command: Add Kagi
Zotero
- Install Better BibTeX
- Install Open PDF
Settings > General > File Renaming > {{ creators max="1" case="hyphen" }}-{{ year }}-{{ title truncate="50" case="hyphen" }}Settings General > Reader > Tabs > Creator - Year - TitleSettings > Export > Item Format > Better BibTeX Citation Key Quick CopySettings > Better BibTeX > Citation Key Format > auth.capitalize + year + shorttitle(3,3)Settings > Better BibTeX > Fields > Omit > file, keywords
Default Folder X
Settings > Options > Sync settings via iCloud
Arq Backup
Additional wildcard exclude rules:
.cache
.devenv
.direnv
.orbstack
.venv
__pycache__
node_modules
*/OrbStack
*/Library/CloudStorage
*/Library/Application Support/Note: Authorize your key as described in SSH Key Deployment and reconnect via SSH after installing nix.
sudo /nix/var/nix/profiles/default/bin/nix upgrade-nix
nix run github:mirkolenz/infra
sudo usermod -s $(which fish) "$USER"The configuration sets LC_ALL=en_US.UTF-8, and macOS forwards it via SSH, so the host has to provide this locale.
Otherwise, its system shell warns setlocale: LC_ALL: cannot change locale (en_US.UTF-8) on login.
Nix programs are not affected, since targets.genericLinux points them to the locale archive of Nix, so check with /usr/bin/locale -a instead.
On Ubuntu, the following generates the locale without changing the system default.
sudo locale-gen en_US.UTF-8Determinate Nix overwrites /etc/nix/nix.conf on upgrades, so append the cache to the included /etc/nix/nix.custom.conf instead.
sudo vi /etc/nix/nix.custom.conf
sudo systemctl restart nix-daemonextra-trusted-substituters = https://mirkolenz.cachix.org
extra-trusted-public-keys = mirkolenz.cachix.org-1:R0dgCJ93t33K/gncNbKgUdJzwgsYVXeExRsZNz5jpho=opnix renders two GitHub tokens from the item GitHub in the 1Password vault NixOS on the NixOS and macOS hosts that set services.onepassword-secrets.enable, other hosts get neither.
The token of the agents is only rendered on hosts whose user sets programs.agents.enable.
Use a single fine-grained token for public repositories or selected ones, with read-only access to Contents, Metadata, Actions, Pull requests, Issues, and Commit statuses.
- The field
nix-confholds the access tokens of nix, which avoid rate limits, as the lineaccess-tokens = github.com=github_pat_XXX. It lands in/var/lib/opnix/nix/secrets.confon NixOS and/usr/local/var/opnix/nix/secrets.confon macOS, owned by root and readable by the admin group, since the nix client fetches flakes as your user. nix skips the include while the file is missing or unreadable, as within the agent sandbox, where nono injects its content asNIX_CONFIGinstead. - The field
credentialholds the bare token, which nono injects into the sandboxed agents asGH_TOKEN. It lands in/var/lib/opnix/agents/github-tokenon NixOS and/usr/local/var/opnix/agents/github-tokenon macOS, owned by your user with mode0400. nono aborts the launch of an agent while the file is missing, so provision it before the first start.
The agent sandbox denies the file of the nix access tokens, and the nix daemon trusts no user but root, so nobody can hand the daemon other settings.
Standalone home-manager hosts get neither token, their admin has to add an include to /etc/nix/nix.custom.conf.
To provision a host, create a 1Password service account with read access to the vault NixOS and store its token on every host.
opnix token set reads the token from stdin, so it also works headless, e.g. sudo opnix token set < token.txt.
sudo opnix token set
# NixOS
sudo systemctl restart opnix-secrets
# macOS
sudo launchctl kickstart -k system/org.nixos.opnix-secretsThe service account token stays in /etc/opnix-token, readable by root only, since the group onepassword-secrets has no members.
flakectl is prepared to replace Cachix with an S3 bucket such as Cloudflare R2.
With --cache s3://..., check-build pushes every target and its build closure, and gc-cache deletes everything not needed by the current targets of any system and older than 30 days.
Switching requires the following steps:
- Create an R2 bucket, serve it publicly via a custom domain (e.g.,
cache.mirkolenz.com), and create an API token with write access. - Generate a signing key with
nix key generate-secret --key-name cache.mirkolenz.com-1 > keyand derive its public half withnix key convert-secret-to-public < key. - Add the secrets
CACHE_SIGNING_KEY,CACHE_ACCESS_KEY_ID, andCACHE_SECRET_ACCESS_KEYas well as the variablesCACHE_BUCKETandCACHE_ENDPOINT(https://<account-id>.r2.cloudflarestorage.com) to the repository. - In
flake.nix, replace the Cachix substituter and public key with the custom domain and the new public key. - In
checks.yaml, replacecachix-actionwithsecret-key-filespointing to the signing key and runnix run . -- --cache "s3://$CACHE_BUCKET?endpoint=$CACHE_ENDPOINT®ion=auto&compression=zstd" check-buildwith the secrets exported asAWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEY.gc-cachereads them from the environment only, not from~/.awsprofiles. - Add a job that runs
gc-cachewith the same arguments aftercheck-buildon pushes tomain. - Replace
*.cachix.orgin the agent sandbox domains with the custom domain.
If building for another architecture on NixOS: Enable cross compiling
# for iso installer disc
nix build github:mirkolenz/infra#.legacyPackages.x86_64-linux.installer-default.iso-installer
# for apple t2
nix build github:mirkolenz/infra#.legacyPackages.x86_64-linux.installer-apple-t2.iso-installerFlash the image to a USB drive or SD card with caligula:
sudo caligula burn --hash skip --compression auto result/iso/*.isoThe SD card image holds the final raspi system, which then updates itself like every other host.
Building it runs a VM, so the builder needs /dev/kvm.
The root partition grows to the size of the card on first boot.
nix build github:mirkolenz/infra#nixosConfigurations.raspi.config.system.build.diskoImages
sudo caligula burn --hash skip --compression auto result/main.raw.zstEvery switch rewrites the boot code, U-Boot, and config.txt on /boot.
The bootloader in the EEPROM is updated by hand, the update applies on the next reboot.
sudo rpi-eeprom-update -a
sudo reboot# otherwise the optical drive is not found: https://discourse.nixos.org/t/makemkv-cant-find-my-usb-blu-ray-drive/23714
sudo modprobe sg
# if that does not work, try again with sudo
nix run github:mirkolenz/infra#makemkvsudo podman run --rm --subuidname=$USER ubuntu cat /proc/self/uid_mapPrompts for a password and writes its yescrypt hash to the users.users.USER.hashedPasswordFile that the configuration of MACHINE_NAME declares.
If USER has a running GNOME session on this host, it also asks for the current password and re-encrypts the login keyring right away, so the keyring keeps unlocking at login.
sudo nix run github:mirkolenz/infra -- passwd MACHINE_NAME USERzellij web --create-tokenssh-keygen -t ed25519 -a 128 -C "USER@HOST" -f ~/.ssh/id_ed25519Keys live in 1Password (vault Mirko) and are offered by its SSH agent, so the private key never exists as a file.
Long-lived keys additionally belong in custom.user.sshKeys (see modules/core/identity.nix) and in the agent config (see modules/programs/op.nix).
Since ssh-copy-id expects a local key file, copy the public key in 1Password and add it to the remote machine by hand instead.
The following command logs in with the account password and opens the file in vim, where the key is pasted on its own line.
-t allocates the terminal vim needs and umask makes both the directory and the file private.
ssh -t "USER@MACHINE_NAME" "umask 077; mkdir -p ~/.ssh; vim ~/.ssh/authorized_keys"