Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Secrets

One personal age key: ~/.sops-nix/key.txt on a workstation and /var/lib/sops-nix/key.txt on each host. The recipients are in .sops.yaml, committed on purpose because they are public keys. The encrypted values are in secrets/secrets.yaml, also committed, which is the point of sops.

SOPS_AGE_KEY_FILE=~/.sops-nix/key.txt nix develop -c sops secrets/secrets.yaml

secrets/secrets.example.yaml shows the decrypted shape, with a comment per key. modules/sops.nix declares every key and is the one place to add one.

The keys

KeyWhatConsumer
root_passwordcrypt(3) hash, mkpasswd -m yescryptmodules/users.nix
user_passwordcrypt(3) hash, mkpasswd -m yescryptmodules/users.nix
luks_passphrasethe passphrase itself, in the clearinstall.sh only, never declared
tailscale_authkeyreusable, pre-authorized, not ephemeralmodules/tailscale.nix
syncthing_gui_passwordplaintext; syncthing-init bcrypts itmodules/syncthing.nix
sunshine_passwordplaintext; sunshine --creds hashes ithosts/hutao-desktop
llm.* (see llmKeys)provider API keysrendered into one llm.env
nix.forgejo_token, nix.github_tokenread tokens for private flake inputsrendered into root’s git credential store

Every declared key must exist, on every host, before anything builds. sops-nix checks the manifest at build time, and a missing key is a failed build that names it:

secret sunshine_password in …-secrets.yaml is not valid: the key 'sunshine_password' cannot be found

install.sh reads the same list out of the flake and checks every key before it touches a disk, so an install stops at the preflight instead of failing inside nixos-install on a wiped disk.

Values with quirks

  • YAML quoting. A value starting with *, &, !, %, @ or a backtick is YAML syntax, and sops refuses to save the file. Single-quote it. Double quotes work too but treat \ as an escape, so check what sops shows after it rewrites the value in its own style. (2026-09-25)
  • luks_passphrase is the only plaintext credential that must never reach /run/secrets: cryptsetup needs the passphrase, not a hash of it. install.sh decrypts it, runs luksFormat and shreds its copy, and modules/sops.nix deliberately does not declare it. You still type it at every boot; nothing on the machine can open its own disk.
  • The password hashes must be crypt(3) hashes. A sha512sum digest is not one, and with users.mutableUsers = false a host that cannot use its hashes is a host nobody can log into.

Templates

modules/sops.nix also renders three files out of those keys, because the consumers want a file of a particular shape rather than a bare value:

TemplateShapeWhy
llm.envVAR=value linesdot-profile.d/environment.sh sources it into every shell
git-credentialsgit’s credential-store format, root-onlysudo nixos-rebuild fetches private inputs as root
sunshine-netrca curl netrcthe desktop’s disconnect watcher, without argv exposure

Keys and recipients

Adding a recipient does not grant access to what is already encrypted; rerun sops updatekeys secrets/secrets.yaml. Lose every private key in .sops.yaml and the values are gone, by design.

Getting the private key onto a new machine is the one unavoidable manual step of an install. Nothing bootstraps a decryption key from nothing.