nix #
NixOS configurations for personal machines, managed with flakes and Home Manager.
Layout #
├── flake.nix # Flake entry point — defines both NixOS configurations
├── home.nix # Shared Home Manager module (imported by all hosts)
├── hyprland.nix # Shared Hyprland config
├── thick-black-cannon/ # Host: thick-black-cannon
│ ├── configuration.nix # NixOS config
│ └── home.nix # Home Manager config
├── 13-inch-thin-cannon/ # Host: 13-inch-thin-cannon
│ ├── configuration.nix # NixOS config
│ └── home.nix # Home Manager config
└── .tangled/workflows/ # Spindle CI pipelines
Testing #
nix flake check
This evaluates and builds both NixOS configurations. Add --no-build to skip
building (eval-only).
The repository avoids NUR because its community-maintained packages receive a
different level of review and provenance assurance than the pinned nixpkgs
packages used here. This reduces supply-chain and malware risk. Firefox
policies and profile settings remain managed in modules/firefox.nix; the
NUR-only packaged add-ons are not installed by the declarative profile.
Applying #
sudo nixos-rebuild switch --flake .#"<hostname>"
Provision the file_magic account password as an age-encrypted yescrypt hash.
First installs need nothing after the ISO boots — the ISO's
provision-password prompts for the password, generates the host key, and
writes both to the target, so the password is live at the very first login
prompt:
# on the live ISO, after disko-install:
persist-config && provision-password && reboot
The installed system decrypts the hash at every activation from
/persistent/etc/nixos/secrets/file_magic-password.age with its own SSH host
key (/persistent/etc/ssh/ssh_host_ed25519_key) — the plaintext hash exists
only in tmpfs (/run), never on disk and never in the store.
To rotate the password later, run the target on the target (no sudo, no rebuild — applied at the next reboot):
just set-file-magic-password # in /etc/nixos on the target
The target enforces the recipient: it fails when run on any other host unless
AGE_RECIPIENT holds the target's public key:
AGE_RECIPIENT="$(ssh file_magic@13-inch-thin-cannon cat /etc/ssh/ssh_host_ed25519_key.pub)" just set-file-magic-password
# then: scp secrets/file_magic-password.age file_magic@13-inch-thin-cannon:/etc/nixos/secrets/ && ssh file_magic@13-inch-thin-cannon sudo reboot
Reinstalling regenerates the host key, so re-run provision-password after
every install (or re-encrypt the existing secret to the new key with the
command above). The repo no longer tracks the password secret.
Extra recipients (recovery keys, e.g. Bitwarden) #
AGE_EXTRA_RECIPIENTS encrypts the secret to additional keys (a
whitespace-separated list of age1…/AGE-PLUGIN-… keys or two-field
ssh-ed25519/ssh-rsa public keys, no comment — e.g. the output of
ssh-keygen -y -f KEY; commented entries fail loudly before the password
prompt). The target host key always stays the primary recipient — it
performs the activation-time decrypt — so extra keys are for humans:
recovery if the host key changes, and manual decrypt/edit from any machine.
Extra recipients apply via just set-file-magic-password (on the target or
with AGE_RECIPIENT); the ISO's provision-password encrypts to the host
key only — re-provision afterwards to add recovery keys.
age only accepts age1…/AGE-PLUGIN-… keys and ssh-ed25519/ssh-rsa
public keys as recipients. Bitwarden's own account public key is a
vault-encryption key and cannot be used; store an SSH key item in Bitwarden
instead and pass its public key (strip a trailing comment first with
awk '{print $1, $2}'):
AGE_RECIPIENT="$(ssh file_magic@13-inch-thin-cannon cat /etc/ssh/ssh_host_ed25519_key.pub)" \
AGE_EXTRA_RECIPIENTS="$(bw get item my-ssh-key --raw | jq -r .sshKey.publicKey | awk '{print $1, $2}')" \
just set-file-magic-password
To decrypt manually with such a key, export the private key material (Bitwarden's SSH agent is not usable by age) and run:
bw get item my-ssh-key --raw | jq -r .sshKey.privateKey > /tmp/k && chmod 600 /tmp/k
nix shell nixpkgs#age --command age --decrypt -i /tmp/k -o - secrets/file_magic-password.age
For the full walk-through from Bitwarden to a built image, see 13-Inch ISO → End-to-end below.
This password is intentionally not accepted over SSH: the installed host uses
key-only SSH authentication. It remains useful for local console login and
sudo/PAM authentication. The target does not change sshd settings.
If password SSH is required, enable it explicitly in the installed host:
services.openssh.settings = {
PasswordAuthentication = true;
KbdInteractiveAuthentication = true;
};
Prefer enabling this only on a trusted network or WireGuard interface. SSH
keys, preferably FIDO2-backed ed25519-sk keys, provide a better remote-login
security boundary than passwords.
13-Inch ISO #
Build and test the self-contained installer from the repository root:
just iso # build result/iso/*.iso
just ovmf # refresh test firmware
just test-iso # live -> install -> installed boot
just up # full build and test lifecycle
The ISO contains the flake and install closure. On real hardware, boot it and
run the commands printed by the installer: disko-install, then
persist-config, then provision-password (prompts for the file_magic
password and generates the host key), then reboot — the password is live at
the first login prompt. The production host keeps SSH password login disabled
until explicitly changed for a test or local provisioning step.
The live ISO intentionally has the known root password toor for local
console access while installing. Root password SSH login remains disabled by
the ISO's PermitRootLogin = "prohibit-password"; remote test/install access
uses the temporary authorized test key.
End-to-end: install → password → recovery key #
Prerequisites for the recovery-key step: bw (Bitwarden CLI) installed and
logged in, jq on PATH.
-
Build the image from the repository root (several minutes):
just up # ISO build + live -> install -> boot test in QEMU just iso # build onlyjust upalso exercises provisioning end to end: the harness runsprovision-passwordwithFILE_MAGIC_PASSand asserts on the installed system that the password authenticates via sudo/PAM. -
Install on real hardware: boot
result/iso/nixos-<version>-x86_64-linux.isoand run the commands printed by the installer:disko-install --flake /iso/repo#13-inch-thin-cannon \ --disk main /dev/nvme0n1 --write-efi-boot-entries persist-config provision-password # prompts for the file_magic password rebootAfter the reboot,
file_magiccan log in on the console with the password and use sudo; SSH stays key-only. -
Add a recovery key (optional, e.g. Bitwarden) — from the booted target or with
AGE_RECIPIENTfrom anywhere:# on the target, in /etc/nixos: AGE_EXTRA_RECIPIENTS="$(bw get item my-ssh-key --raw | jq -r .sshKey.publicKey | awk '{print $1, $2}')" \ just set-file-magic-password reboot # the replacement secret applies at the next activationThis re-encrypts the same password hash to the host key plus the Bitwarden key; see Applying → Extra recipients for the details and the manual-decrypt procedure.
CI #
CI is provided by Tangled Spindle.
Pipelines are defined in .tangled/workflows/ and run nix flake check on
every push and pull request to main.