Skip to content

Setup a New Machine: Pre-Deployment Checklist

Duration: 15–30 minutes (all on the admin host, target machine not needed yet)

This phase prepares everything needed to deploy a new NixOS machine. All steps happen on the admin host (your development machine with Nix installed). The target machine can remain powered off.

Overview: The Three-Phase Process

  1. Pre-Deployment (this document): Prepare configuration and deployment artifacts on admin host
  2. Deployment: Boot target machine to live environment and run nixos-anywhere
  3. Post-Deployment: Minimal verification and optional additional setup

Terminology

  • Admin host: The machine you are using to orchestrate the setup (your laptop, desktop, or CI system with Nix installed)
  • Target host: The new NixOS machine being deployed (may have no OS yet)
  • Live environment: NixOS minimal ISO running in RAM on the target machine during deployment

Prerequisites

  • Nix package manager installed on admin host (required to use this repository)
  • Repository cloned and up to date
  • You are in the devshell: nix develop (or direnv allow if using direnv)
  • SSH key pair on admin host for connecting to the repository (for git operations)
  • For cloud VPS targets: IP address or hostname already known from provider
  • For physical targets: Network connectivity and way to boot the NixOS minimal image

Step 1: Create Target's NixOS Configuration

Choose a new machine hostname (e.g., hypervisor, gaming, staging-web) and create its configuration directory.

To make the following steps easier to follow, I recommend setting the HOSTNAME variable in your shell so you don't have to change it in all commands.

export HOSTNAME=your_hostname

1.1 Copy the Directory Structure

mkdir -p nix/hosts/${HOSTNAME}/users nix/hosts/${HOSTNAME}/data

Replace ${HOSTNAME} with your chosen name throughout these steps.

1.2 Create configuration.nix

Copy from a reference machine and customize:

cp nix/hosts/blackfog/configuration.nix nix/hosts/${HOSTNAME}/configuration.nix

Then edit to update: - Hostname: Change networking.hostName = "blackfog" to your target hostname - System name: Change system.name = "blackfog" to your target hostname - Imports: Remove or adjust imports based on target machine purpose (e.g., remove gnome module if headless server) - Disko config: If using disk encryption (LUKS), ensure disk IDs match target hardware - Tailscale and other services: Enable/disable as appropriate

1.3 Create User Configuration

For desktop/workstation machines (e.g., gaming, blackfog):

cp nix/hosts/blackfog/users/snyssen.nix nix/hosts/${HOSTNAME}/users/snyssen.nix

For server machines (e.g., hypervisor, ingress):

mkdir -p nix/hosts/${HOSTNAME}/users
touch nix/hosts/${HOSTNAME}/users/.gitkeep
# No user config needed; root will be used via Tailscale or SSH

1.4 Create Placeholder hardware-configuration.nix

cp nix/hosts/blackfog/hardware-configuration.nix nix/hosts/${HOSTNAME}/hardware-configuration.nix

Note: This is a placeholder. The actual hardware configuration will be generated by nixos-anywhere during deployment and will overwrite this file.

1.5 Create secrets.yaml

Create a secrets file for target-specific secrets (e.g., Tailscale auth keys):

touch nix/hosts/${HOSTNAME}/data/secrets.yaml

Edit it with initial placeholder content (will be encrypted later):

# nix/hosts/${HOSTNAME}/data/secrets.yaml
tailscale:
  authKey: "PLACEHOLDER_WILL_BE_REPLACED"

Step 2: Generate SSH Keypairs for Target

The target machine needs SSH keypairs for: - Git operations (cloning, pushing commits) - Commit signing - Connecting to other machines via SSH - Deriving age keys for SOPS secret decryption

We generate these on the admin host and stage them for deployment, then delete them from the admin host immediately after successful deployment.

2.1 Determine Key Type and Location

For server/headless machines (root-based): - Key placement: /etc/ssh/ssh_host_ed25519_key (and .pub) - Owner: root

For desktop/workstation machines (user-based): - Key placement: /home/snyssen/.ssh/id_ed25519 (and .pub) - Owner: snyssen user

2.2 Generate SSH Keypair

Create a temporary directory to stage deployment files:

mkdir -p /tmp/${HOSTNAME}-deploy/etc/ssh

Or for desktop:

mkdir -p /tmp/${HOSTNAME}-deploy/home/snyssen/.ssh

Generate the keypair with no passphrase:

ssh-keygen -t ed25519 -f /tmp/${HOSTNAME}-deploy/etc/ssh/ssh_host_ed25519_key -N "" -C "root@${HOSTNAME}"

Or for desktop:

ssh-keygen -t ed25519 -f /tmp/${HOSTNAME}-deploy/home/snyssen/.ssh/id_ed25519 -N "" -C "snyssen@${HOSTNAME}"

Verify the keypair was created:

ls -la /tmp/${HOSTNAME}-deploy/etc/ssh/
# or
ls -la /tmp/${HOSTNAME}-deploy/home/snyssen/.ssh/

Step 3: Derive and Authorize Age Keys

The age key (used for SOPS secret decryption) is derived from the SSH private key. At deployment time, sops-nix on the target will automatically derive the age key from the SSH key you staged. During pre-deployment, we only need the age public key to authorize the target in .sops.yaml.

3.1 Derive the Age Public Key

We'll generate a temporary age private key from the SSH key, extract the public key, then delete the temporary file (it's not needed anywhere—the age key is derived from the SSH key at runtime).

Set your hostname variable:

HOSTNAME=hypervisor  # or your target hostname
TEMP_AGE_KEY="/tmp/${HOSTNAME}-age-privkey-temp"

Generate the temporary age private key from your staged SSH key:

just sops-gen-privkey /tmp/${HOSTNAME}-deploy/etc/ssh/ssh_host_ed25519_key $TEMP_AGE_KEY

Or for desktop machines:

just sops-gen-privkey /tmp/${HOSTNAME}-deploy/home/snyssen/.ssh/id_ed25519 $TEMP_AGE_KEY

Extract the age public key:

just sops-get-pubkey $TEMP_AGE_KEY

This prints the age public key in format: age1xxxxxxxxxxxxxx...

Delete the temporary age private key (no longer needed):

rm "$TEMP_AGE_KEY"

Why we delete it: The age private key doesn't need to be stored. At runtime, the target machine will derive the age key from the SSH private key you staged, so it can decrypt its own secrets. The temporary age key was only needed to extract the public key.

Copy the printed public key (age1xxxxxxxxxxxxxx...); you'll need it in the next step.

3.2 Update .sops.yaml

Edit .sops.yaml in the repository root and add the new machine's age key to the appropriate creation_rules section:

keys: &all_keys
  - &snyssen_gaming age1qxzfz6w99ptdyen3mwp3wr93yd8690m20s8p4daqn0qqczujhghsua5x28
  - &snyssen_sninful age1n9kh0lcwpcth7ex4n9lc2h5enl45pmmy2wqzhznwulgq9r9elf4qp3vr2k
  - &snyssen_purplehaze age1299cj79spmwd93hjz80v5s743a0vr6cjjkm8wggmc4z0x54e69fs9a0hqs
  - &snyssen_blackfog age104pa77q73yt8hht4kv05gpk7hyayefy697lfe63dl0ua6dsej54s3mmrgj
  - &root_ingress age17gnf6sp839a0wlhd998vn0wv5rrcwle34pky8mfn5d8dymm99vtsm9xygx
  - &root_hypervisor age1psnzzfwejkyx4aduux42lt6a6kghcg3sex5qwuqcuucmdx3hddwsex0w6h
  - &root_gaming age1XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX  # NEW
creation_rules:
  # ... existing rules ...
  - path_regex: nix/hosts/gaming/data/[^/]+\.(yaml|json|env|ini)$
    key_groups:
      - pgp:
        age:
          - *root_gaming  # NEW

Define the key anchor at the top with other keys, then reference it in the appropriate creation_rules entry for your target machine.

3.3 Re-encrypt Secrets Files

Now update the secret files to include the new machine's age key:

just sops-update-keys nix/hosts/${HOSTNAME}/data/secrets.yaml

If the target needs access to global secrets:

just sops-update-keys nix/data/secrets.yaml

3.4 Add Secrets Values

Edit the target's secrets file and add actual secret values:

just sops-update nix/hosts/${HOSTNAME}/data/secrets.yaml

Your editor will open with decrypted content. Add values like Tailscale auth keys:

tailscale:
  authKey: "tskey-auth-Cxxxxxx..."

Save and exit; SOPS will re-encrypt the file automatically.

Step 4: Prepare Deployment Credentials

4.1 Generate Temporary Root Password

Generate a temporary password that will be used only during the live environment. You'll enter this password once when nixos-anywhere connects via SSH:

read -s > /tmp/${HOSTNAME}-root-password.txt

Important: This password exists only in the live environment and is discarded after reboot. You won't need it again.

4.2 Prepare LUKS Encryption (if applicable)

Most LUKS configurations use a dual-unlock approach:

  • Primary: Binary keyfile on USB (convenient, requires USB present during installation)
  • Secondary: Password/passphrase (backup, works without USB if needed later)

4.2.1 Create Binary Keyfile on USB (Before Deployment)

If your disko configuration uses a USB keyfile (check for keyFile and usbKeysIds in the config):

  1. Create the keyfile on your USB device beforehand using the steps in Full Disk Encryption - Create The Keyfile
  2. Name it something identifiable (e.g., hypervisor or hypervisor.key)
  3. Remember the USB device's UUID (you'll need it in Deployment Part A)

  4. Have the USB key ready for the deployment. You'll physically insert it into the target machine during the live environment setup (see Deployment Part A, step A.7).

4.2.2 Prepare Backup Password

Generate a strong backup password for cases where the USB key is unavailable or lost:

read -s > /tmp/${HOSTNAME}-luks-password.txt

Note: This password is only for fallback. Primary unlock will use the USB keyfile.

Step 5: Validate Configuration

5.1 Build the Flake

Test that your NixOS configuration builds correctly:

nh os build -H ${HOSTNAME}

This will build the entire closure. If there are errors in your Nix code, they'll appear here before deployment.

5.2 Verify Secrets

Ensure secret files are properly encrypted:

cat nix/hosts/${HOSTNAME}/data/secrets.yaml | head -20
# Should show encrypted content with "ENC[AES256_GCM..." entries

Step 6: Summary and Next Steps

At this point you should have:

✅ Complete NixOS configuration for target ✅ SSH keypairs generated in /tmp/${HOSTNAME}-deploy/ ✅ Age keys derived and authorized in .sops.yaml ✅ Secrets encrypted with target's age key ✅ Flake builds successfully ✅ Temporary credentials prepared (root password, LUKS password if needed)

Next step: Proceed to Setup New Machine - Deployment

Before you begin deployment, ensure you know: - Target IP address or hostname (from DHCP or cloud provider) - Target hardware (disk IDs if using disko) - Network connectivity (can admin host reach target over SSH during live environment?)