nix and nixos configs
Nix 63%
Shell 21%
Just 8%
HCL 4%
3%
Makefile 1%
Smarty <1%

README.md

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.

  1. Build the image from the repository root (several minutes):

    just up     # ISO build + live -> install -> boot test in QEMU
    just iso    # build only
    

    just up also exercises provisioning end to end: the harness runs provision-password with FILE_MAGIC_PASS and asserts on the installed system that the password authenticates via sudo/PAM.

  2. Install on real hardware: boot result/iso/nixos-<version>-x86_64-linux.iso and 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
    reboot
    

    After the reboot, file_magic can log in on the console with the password and use sudo; SSH stays key-only.

  3. Add a recovery key (optional, e.g. Bitwarden) — from the booted target or with AGE_RECIPIENT from 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 activation
    

    This 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.