My NixOS and Nix-Darwin configurations.
hosts- one directory per machine, holding everything specific to it:default.nix,hardware.nix, andhome.nix/disko.nixwhere there is onesystem- shared system-level (NixOS/nix-darwin) configurationhome- shared home-manager configurationsecrets- sops-encrypted secrets,system.yamlandhome.yamlpkgs- own packages (fonts,scripts), available aspkgs.custom.*in configs and as flakepackagesfiles- static assets (wallpapers, grafana dashboards - the dashboards are imported by hand through the Grafana UI, not provisioned, so nothing in nix references them)templates- flake templatesrouter- home network and MikroTik router config, see router/readme.md
Inside system and home:
- modules - something that's imported on-demand and does not support explicitly enabling
- options - something that's always imported but requires explicit enable. It's also configurable via some abstraction.
- profiles - prepared modules for a system kind or use-case: nixos, darwin, server, desktop, laptop
- user - per-user system-level config (
systemonly)
| Host | Kind | Notes |
|---|---|---|
framework |
NixOS | desktop, Ryzen AI Max+ 395 |
x1c6 |
NixOS | laptop, ThinkPad X1 Carbon 6th gen |
rpi4-1 |
NixOS | server, aarch64 |
rpi4-2 |
NixOS | server, aarch64 |
m4 |
nix-darwin | macbook air m4 |
Note
in all commands flake location can be one of the following:
# github repo
github:konradmalik/dotfiles#<target>
# local current dir
.#<target>
# absolute local git repo
git+file://$HOME/Code/github.com/konradmalik/dotfiles#<target>I'll use the local version for brevity.
Formatting goes through treefmt (treefmt.nix), and the style itself lives in
.editorconfig, so the devshell's formatters, and with them the editor, format
exactly like these:
$ nix fmt
$ nix flake checkThe check covers formatting, shellcheck and the lua and qml lints, and on Linux the NixOS VM tests too; the qml parts run on Linux only, where quickshell and its tools are. To run just one, build it directly:
$ nix build .#checks.aarch64-darwin.lint-lua$ sudo nixos-rebuild --flake . switch
# or
$ sudo nixos-rebuild --flake . bootTo just build (for example for a test):
$ nix build .#nixosConfigurations.framework.config.system.build.toplevel$ nix build .#rpi4-2-sd-imageThe image lands in result/sd-image/nixos-sd-image-<version>-aarch64-linux.img.zst.
Unpack and flash it to the card:
$ unzstd -c result/sd-image/*.img.zst | sudo dd of=/dev/sdX bs=4M conv=fsync status=progressNote
The filesystem won't be complete, it will miss etc and more. NixOS will populate those dirs on first boot.
So if you need to modify something on the card (like read host keys) then the steps are:
- boot rpi with the newly flashed card once
- wait a minute or two
- poweroff rpi and mount the card on your PC
- filesystem will be complete
In NixOS ISO:
Clone this repo
$ git clone https://github.com/konradmalik/dotfilesEnter shell
$ nix-shellUse disko to format and mount:
$ sudo disko --mode destroy,format,mount ./hosts/x1c6/disko.nixGenerate hardware configuration and put it in the host's dir:
$ sudo nixos-generate-config --no-filesystems --root /mnt
$ cp /mnt/etc/nixos/hardware-configuration.nix ./hosts/x1c6/hardware.nixGenerate/add sops keys (if required for the configuration). Do this later only if no critical services rely on them (like user passwords).
Host ones will be picked up automatically. Add user ones to /home/USER/.config/sops/age/keys.txt.
For details refer to sops-nix section.
Finally, use hardware-configuration and disko to install nixos:
$ sudo nixos-install --flake .#x1c6 --root /mntIn NixOS ISO:
Clone this repo
$ git clone https://github.com/konradmalik/dotfilesEnter shell
$ nix-shellUse disko to mount:
$ sudo disko --mode mount ./hosts/x1c6/disko.nixEnter your system
$ cd /mnt
$ nixos-enterFirst clone this repo to the machine.
Then you need to install nix. nix-darwin manual suggests using lix as the bootstrapper.
Next install homebrew.
Enter the devshell from this repo.
Finally, build and enable config locally:
$ sudo darwin-rebuild switch --flake .Once that succeeds, uninstall the bootstrapper. nix.package owns nix from here on, but the installer
leaves itself in root's default profile, which stays on PATH next to the real one. Two nix
implementations sharing ~/.cache/nix do not agree on the narHash of every git input, which surfaces
later as mismatch in field 'narHash' of input ... in flakes and direnv:
$ sudo nix-env --profile /nix/var/nix/profiles/default --uninstall lix
$ sudo nix-env --profile /nix/var/nix/profiles/default --delete-generations oldnix-env -q --profile /nix/var/nix/profiles/default should then list only nss-cacert.
To just build a darwin host (for example for a test):
$ nix build .#darwinConfigurations.m4.config.system.build.toplevel
# or shortened by nix-darwin
$ nix build .#darwinConfigurations.m4.systemIt is useful to have a Linux builder on a macOS machine to build linux-specific stuff.
nix-darwin supports it as an option. It is a NixOS VM run via Apple's
Virtualization.framework (linux-builder-vz) and it serves both aarch64-linux
and x86_64-linux - the latter through Rosetta, which needs
softwareupdate --install-rosetta on the host once.
The launchd daemon is started on demand with the
linux-builder-ctl helper that ships with that module.
Use colima. It's installed via homebrew.
Strategy with keys:
- none of the keys block new machines. If they're missing, they'll just fail to decrypt on runtime.
agederived from host ssh key for host-wide secretsagederived from personal ssh key for personal secrets- one global
agekey per person that is kept secret and not directly on any machine. Serves as a backup to decrypt in case of 'tragedy'
To get age key for the machine, use:
$ cat /etc/ssh/ssh_host_ed25519_key.pub | ssh-to-ageAdd this key to .sops.yaml and propagate re-encryption to all secrets:
$ for file in $(grep -lr "^sops:$"); do sops updatekeys -y $file; doneCreate age directory for sops:
$ mkdir -p "$XDG_CONFIG_HOME/sops/age"
$ touch "$XDG_CONFIG_HOME/sops/age/keys.txt"
$ chmod 700 "$XDG_CONFIG_HOME/sops/age"
$ chmod 600 "$XDG_CONFIG_HOME/sops/age/keys.txt"Create age key from your personal ssh key:
Why do this when decryption keys are also derived from host ssh keys?
- Redundancy, 2. Personal (user-specific) secrets, 3. Keys generated here can also be used in the home-manager module below
$ ssh-to-age -private-key -i ~/.ssh/personal > "$XDG_CONFIG_HOME/sops/age/keys.txt"Add this key to .sops.yaml and propagate re-encryption to all secrets:
$ for file in $(grep -lr "^sops:$"); do sops updatekeys -y $file; doneFor user-specific secrets, a home-manager modules of sops-nix is used.
We similarly use age. The key is reused from system-wide config (the one derived from personal ssh).
See how sops is configured in the home-manager (it just points at the keys.txt file).
Keys live in the machine's security chip: non-exportable, impossible to back up, one per
machine. ~/.ssh/personal stays as the offline fallback.
ssh-tpm-agent serves the sealed keys and proxies to the plain ssh-agent, so ordinary
key files keep working alongside them.
$ ssh-tpm-keygen -C konrad@$(hostname) -f ~/.ssh/hardwareThat writes ~/.ssh/hardware.tpm and its .pub. The agent loads every sealed key it
finds in ~/.ssh on start, so any number of them can coexist:
$ systemctl --user restart ssh-tpm-agent.service
$ ssh-add -lThe .tpm blob is the key, encrypted to a hierarchy seed that never leaves the chip.
There is no per-key state inside the tpm, so removing a key is removing its files - while
clearing the tpm regenerates that seed and destroys every key at once:
$ rm ~/.ssh/hardware.tpm ~/.ssh/hardware.pub
$ systemctl --user restart ssh-tpm-agent.serviceRequires macOS Tahoe. Apple's middleware is already wired up in the ssh config and in
SSH_SK_PROVIDER, so ssh, ssh-add and ssh-keygen find it on their own.
Create the identity - -l is the label, -t bio asks for Touch ID on every use while
-t none never asks:
$ sc_auth create-ctk-identity -l ssh -k p-256-ne -t bioThen download the key handle. -K writes one file pair per identity into the current
directory, named after the label, and they can be renamed afterwards:
$ cd ~/.ssh
$ SSH_ASKPASS_REQUIRE=force SSH_ASKPASS=true ssh-keygen -w /usr/lib/ssh-keychain.dylib -K
$ ssh-keygen -lf hardware.pubList and remove identities:
$ sc_auth list-ctk-identities
$ sc_auth delete-ctk-identity -h <hash>ssh-egress.nix sets IdentitiesOnly, so only listed keys are offered. See that file for names or add a new one.
Non-hardware keys are in Bitwarden, only their .pub stays on disk - ssh resolves an IdentityFile through its
.pub and lets the agent sign. The vault is a last resort by ordering:
ssh-egress offers the hardware key first and never asks bitwarden when it is accepted.
Linux chains it behind the plain agent, darwin points IdentityAgent at it since the
enclave key needs no agent at all. Both need the desktop app running and unlocked.
$ ssh -i ~/.ssh/somekey user@host # ad-hoc, works despite IdentitiesOnly
$ ssh-add ~/.ssh/somekey # linux, proxied to the plain agent
$ ssh-add --apple-use-keychain ~/.ssh/somekey # darwin, needs IdentityAgent overriddenPer host, add a block to ssh-egress or a drop-in to the already-included config.d:
$ cat >> ~/.ssh/config.d/somehost <<'EOF'
Host somehost
IdentitiesOnly yes
IdentityFile ~/.ssh/somekey
EOFrestic, via home/options/restic. Every host backs up to backblaze b2 (home/modules/base/restic.nix),
desktops also to local (system/modules/local-backup.nix), an external disk mounted at /mnt/backup
and shared by whichever machine has it attached.
Each repository gets a baker-<name> command, e.g. baker-local snapshots.
The local repository is created once, on any host with the disk attached. The others just use it:
$ findmnt /mnt/backup # must be mounted, or init lands on the root disk
$ sudo install -d -o konrad -g users -m 700 /mnt/backup/restic
$ baker-local initMisterio77 - big inspiration for hyprland and nix files structure.