Pre-seed SSH host keys so sops-nix secrets decrypt on first boot

sops-nix (in the nixos flake) derives each host's age decryption key
from its own /etc/ssh/ssh_host_ed25519_key at activation time, which
runs before systemd would otherwise generate that key on first boot
(sshd-keygen is a plain systemd service gated behind multi-user.target;
activation scripts run earlier). Without pre-seeding, secrets --
including the login password -- fail to decrypt on a fresh install's
very first boot.

- scripts/prepare-host-key.sh: run on the admin workstation before an
  install, generates the host's ed25519 keypair and prints the exact
  steps to register its derived age key in nixos/.sops.yaml and
  re-encrypt the affected secrets/*.yaml files.
- common.nix's auto-install.sh: after disko mounts /mnt and before
  nixos-install, installs a pre-seeded key from /root/host-keys/ into
  /mnt/etc/ssh/ if present, otherwise warns and asks for confirmation
  before continuing without one.
- installer.nix now imports common.nix (previously only proxmox-lxc.nix
  did), so the ISO/netboot path used for EFI VM installs gets the same
  auto-install.sh and pre-seed check, not just the LXC path.
- Also fixes a pre-existing stray backtick in the disko invocation that
  broke auto-install.sh's bash syntax entirely, independent of this
  change (found while rendering the script to verify the new logic).

README.md documents the new pre-flight workflow.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-19 13:21:33 +10:00
co-authored by Claude Sonnet 5
parent a90e736743
commit 02ea1929e5
5 changed files with 151 additions and 50 deletions
+5 -1
View File
@@ -1 +1,5 @@
result result
# Locally-generated SSH host keys staged for transfer to a new machine
# during install (see scripts/prepare-host-key.sh) — never commit these.
host-keys/
+47 -3
View File
@@ -199,7 +199,12 @@ The generated `/etc/auto-install.sh` performs the following steps:
6. Prepare `/mnt` using the Disko-generated mount configuration. 6. Prepare `/mnt` using the Disko-generated mount configuration.
7. Install NixOS: 7. Check for a pre-seeded SSH host key at `/root/host-keys/<host>_ssh_host_ed25519_key`
and install it to `/mnt/etc/ssh/` if present (see "Pre-Seeding SSH Host
Keys for sops-nix" below) — prompts for confirmation before continuing
without one.
8. Install NixOS:
```sh ```sh
nixos-install \ nixos-install \
@@ -207,9 +212,48 @@ The generated `/etc/auto-install.sh` performs the following steps:
--no-root-password --no-root-password
``` ```
8. Remove temporary installation files. 9. Remove temporary installation files.
9. Reboot. 10. Reboot.
## Pre-Seeding SSH Host Keys for sops-nix
The `nixos` flake manages secrets with sops-nix, using an age key derived
from each host's own `/etc/ssh/ssh_host_ed25519_key`. That key is normally
generated by a systemd service (`sshd-keygen`) the first time a host boots
— but sops-nix decrypts secrets (including the login password) earlier
than that, during system *activation*, which runs before systemd starts
pursuing the target that `sshd-keygen` is gated behind. On a genuinely
fresh install, this means secrets fail to decrypt on the very first boot
unless the host key already exists beforehand.
To avoid this, generate the key ahead of time and register it with the
`nixos` flake's sops-nix setup **before** installing:
```sh
./scripts/prepare-host-key.sh <hostname> [path-to-nixos-repo]
```
This generates `host-keys/<hostname>_ssh_host_ed25519_key(.pub)` locally
and prints the exact steps to add its derived age key to `nixos/.sops.yaml`,
re-encrypt the relevant `secrets/*.yaml` files with `sops updatekeys`, and
commit + push the `nixos` repo.
Once that's done and the target machine is booted into the installer,
copy the generated key onto it before running (or continuing)
`/etc/auto-install.sh`:
```sh
scp host-keys/<hostname>_ssh_host_ed25519_key{,.pub} root@<target-ip>:/root/host-keys/
```
`auto-install.sh` checks for this file automatically and installs it to
the target's `/mnt/etc/ssh/` before running `nixos-install`. If it's
missing, the script warns and asks for confirmation before continuing —
useful for hosts that don't consume any sops-nix secrets, but skipping it
for a host that does will lock secrets out of decrypting on first boot.
`host-keys/` is gitignored — never commit private key material.
## Configuration Notes ## Configuration Notes
+25 -1
View File
@@ -105,11 +105,35 @@ fi
if nix eval --refresh --json --extra-experimental-features "flakes nix-command" "''${FLAKE_BASE_URL}#nixosConfigurations.''${choice}.config.disko.devices.disk.main.device" >/dev/null 2>&1; then if nix eval --refresh --json --extra-experimental-features "flakes nix-command" "''${FLAKE_BASE_URL}#nixosConfigurations.''${choice}.config.disko.devices.disk.main.device" >/dev/null 2>&1; then
disko --mode destroy,format,mount \ disko --mode destroy,format,mount \
--flake "''${FLAKE_BASE_URL}#''${choice}" --yes` --flake "''${FLAKE_BASE_URL}#''${choice}" --yes
else else
echo "Selected host has no Disko configuration." echo "Selected host has no Disko configuration."
fi fi
# sops-nix derives this host's decryption key from its own SSH host key
# at *activation* time, which runs before systemd would otherwise
# generate one on first boot. Without pre-seeding it here, secrets
# (including the login password) fail to decrypt on first boot.
# Generate the key + register it with `nixos`'s sops-nix setup ahead of
# time (see nix-auto-installer/scripts/prepare-host-key.sh), then scp it
# to /root/host-keys/ on this machine before continuing.
mkdir -p /root/host-keys
if [[ -f "/root/host-keys/''${choice}_ssh_host_ed25519_key" ]]; then
echo "Found pre-seeded SSH host key for ''${choice}, installing to target..."
install -D -m 0600 "/root/host-keys/''${choice}_ssh_host_ed25519_key" /mnt/etc/ssh/ssh_host_ed25519_key
install -D -m 0644 "/root/host-keys/''${choice}_ssh_host_ed25519_key.pub" /mnt/etc/ssh/ssh_host_ed25519_key.pub
else
echo "WARNING: no pre-seeded host key found at /root/host-keys/''${choice}_ssh_host_ed25519_key"
echo "sops-nix secrets (including the login password) will NOT decrypt on first boot."
echo "Run scripts/prepare-host-key.sh for host ''${choice} on your admin workstation first,"
echo "then scp the result here, if this host needs sops-nix secrets."
read -rp "Continue without a pre-seeded key anyway? (y/N): " skip_key
if [[ ! "$skip_key" =~ ^[Yy]$ ]]; then
echo "Aborted."
exit 1
fi
fi
mkdir -p /mnt/install-tmp mkdir -p /mnt/install-tmp
export TMPDIR=/mnt/install-tmp export TMPDIR=/mnt/install-tmp
+9 -45
View File
@@ -1,54 +1,11 @@
{ pkgs, modulesPath, ... }: { modulesPath, ... }:
{ {
imports = [ imports = [
"${modulesPath}/installer/cd-dvd/installation-cd-minimal.nix" "${modulesPath}/installer/cd-dvd/installation-cd-minimal.nix"
./common.nix
]; ];
# networking.useDHCP = true;
time.timeZone = "Australia/Brisbane";
services.openssh.enable = true;
services.openssh.settings = {
PermitRootLogin = "yes";
};
environment.systemPackages = with pkgs; [
git
curl
jq
parted
e2fsprogs
btrfs-progs
util-linux
disko
];
# environment.etc."auto-install.sh" = {
# source = ./auto-install.sh;
# mode = "0755";
# };
# users.users.root = {
# initialPassword = "nixos";
# };
users.users.nixos = {
isNormalUser = true;
extraGroups = [
"wheel"
];
};
# Only for ISO/Linode serial environments if required # Only for ISO/Linode serial environments if required
# NOT for LXC # NOT for LXC
# #
@@ -56,4 +13,11 @@
# "console=ttyS0,19200n8" # "console=ttyS0,19200n8"
# ]; # ];
# Run installer when logging in (same pattern as proxmox-lxc.nix)
environment.etc."bash_profile-nixos".text = ''
if [ -n "$PS1" ] && [ ! -e ~/.auto_install_ran ]; then
sudo /etc/auto-install.sh
touch ~/.auto_install_ran
fi
'';
} }
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
# Generates a new machine's SSH host key ahead of installing it, so
# sops-nix (in the `nixos` flake) has something to derive an age key
# from before the target ever boots.
#
# Why this is needed: sops-nix derives each host's decryption key from
# its own /etc/ssh/ssh_host_ed25519_key at *activation* time, but that
# activation runs before systemd would otherwise generate this key on
# first boot (sshd-keygen is a normal systemd service gated behind
# multi-user.target; activation scripts run earlier than that). Without
# pre-seeding, secrets — including the root/nixos login password — fail
# to decrypt on the machine's very first boot.
#
# This script only touches your admin workstation and the `nixos` repo's
# .sops.yaml (it never contacts the target machine). Run it, follow the
# printed next steps, then use the resulting key with the auto-install.sh
# prompt (see common.nix) when you actually install the new machine.
set -euo pipefail
hostname="${1:?usage: scripts/prepare-host-key.sh <hostname> [path-to-nixos-repo]}"
nixos_repo="${2:-../nixos}"
sops_yaml="${nixos_repo}/.sops.yaml"
if [[ ! -f "$sops_yaml" ]]; then
echo "ERROR: $sops_yaml not found. Pass the nixos repo path as the 2nd argument." >&2
exit 1
fi
keydir="$(dirname "$0")/../host-keys"
mkdir -p "$keydir"
keyfile="${keydir}/${hostname}_ssh_host_ed25519_key"
if [[ -f "$keyfile" ]]; then
echo "ERROR: $keyfile already exists. Remove it first if you want to regenerate." >&2
exit 1
fi
nix-shell -p openssh ssh-to-age --run "
set -euo pipefail
ssh-keygen -t ed25519 -N '' -C '${hostname}' -f '${keyfile}' >/dev/null
age_pub=\$(ssh-to-age -i '${keyfile}.pub')
cat <<EOF
Generated: ${keyfile}(.pub)
=== 1. Add this line under `keys:` in ${sops_yaml} ===
- &${hostname} \${age_pub}
=== 2. Add *${hostname} to whichever creation_rules key_groups this host needs ===
(e.g. secrets/common.yaml always; add a per-host secrets/${hostname}.yaml
block too if this host will get its own secrets, same pattern as
nix-cache/server.)
=== 3. Re-encrypt every secrets file you just added it to ===
nix-shell -p sops --run 'sops updatekeys ${nixos_repo}/secrets/common.yaml'
=== 4. Commit + push the nixos repo so the flake build picks up the new recipient ===
=== 5. When you boot the installer on the new machine, scp the key in ===
scp ${keyfile}{,.pub} root@<target-ip>:/root/host-keys/
Then continue with /etc/auto-install.sh as normal — it will find the
pre-seeded key and install it before running nixos-install.
EOF
"