Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277{ config, lib, pkgs, ...}:let cfg = config.modules.installer; q = lib.escapeShellArg;
hostname = config.networking.hostName;
chrootPersistDir = lib.removePrefix "/mnt" cfg.persistDir; passwdDir = "${cfg.persistDir}/user-passwords"; ageKeyDir = "${cfg.persistDir}/sops/age"; ageKeyFile = "${ageKeyDir}/keys.txt"; passwdFile = user: "${passwdDir}/${user}";
askpass = lib.getExe (pkgs.callPackage ./helpers/askpass.nix { }); sbctl = lib.getExe pkgs.sbctl;
renderStep = total: index: { title, run, skipWhen ? null, skipTitle ? title, always ? "", ... }: let header = msg: "echo ${q "==> [${toString index}/${toString total}] ${msg}"}"; in if skipWhen == null then '' ${header title} ${run} ${always} '' else '' if ${skipWhen}; then ${header skipTitle} else ${header title} ${run} fi ${always} '';
mkInstaller = { resume ? false, }: let diskoMode = if resume then "mount" else "destroy,format,mount"; diskoScript = if resume then "${config.system.build.mount}/bin/disko-mount" else "${config.system.build.destroyFormatMount}/bin/disko-destroy-format-mount";
installSource = if cfg.prebuilt then "--system ${q "${config.system.build.toplevel}"}" else ''--flake "$flake_dir"${q "#${hostname}"}'';
passwordsExist = lib.concatMapStringsSep " && " (u: "[ -s ${q (passwdFile u)} ]") cfg.passwordUsers;
setPassword = user: '' password="$(ask_password ${q user})" mkpasswd -m sha-512 -s <<< "$password" > ${q (passwdFile user)} chmod 600 ${q (passwdFile user)} unset password '';
steps = lib.filter (s: s.enable or true) [ { title = "disko (mode: ${diskoMode}) for host '${hostname}'"; run = '' ${q diskoScript}
echo " initializing persistent directories and machine-id" mkdir -p ${q "${cfg.persistDir}/var/lib/dbus"} mkdir -p ${q ageKeyDir} ${q passwdDir} chmod 700 ${q ageKeyDir} ${q passwdDir} dbus-uuidgen --ensure=${q "${cfg.persistDir}/machine-id"} ''; }
{ title = "passwords: setting passwords for ${lib.concatStringsSep ", " cfg.passwordUsers}"; skipWhen = if resume then passwordsExist else null; skipTitle = "passwords: existing password files found, skipping"; run = '' ( umask 077 ${lib.concatMapStrings setPassword cfg.passwordUsers} ) ''; always = '' echo " linking password files into the installation root" mkdir -p /mnt/etc ln -sfT ${q "${chrootPersistDir}/user-passwords"} /mnt/etc/user-passwords ''; }
{ enable = cfg.requireSops; title = "sops: decrypting host age key with the FIDO2 token"; run = '' host_key_blob="$flake_dir/secrets/host-age-key.age" fido2_identity="$flake_dir/secrets/fido2-identity.txt" installed_age_key=false
decrypt_age_key() { for required_file in "$host_key_blob" "$fido2_identity"; do [ -f "$required_file" ] || die "$required_file not found" done
echo " plug in the token and touch it when it blinks" if ! ( umask 077 age --decrypt \ --identity "$fido2_identity" \ --output ${q ageKeyFile} \ "$host_key_blob" ); then rm -f ${q ageKeyFile} die "could not decrypt $host_key_blob with the FIDO2 token" fi chmod 600 ${q ageKeyFile} }
${ if resume then '' if [ -s ${q ageKeyFile} ]; then echo " age key already present, skipping installation" else installed_age_key=true decrypt_age_key fi '' else '' installed_age_key=true decrypt_age_key '' }
echo " verifying key by decrypting secrets/secrets.yaml..." if SOPS_AGE_KEY_FILE=${q ageKeyFile} sops -d "$flake_dir/secrets/secrets.yaml" >/dev/null; then echo " OK - key decrypts secrets.yaml successfully." else echo " FAILED - this key cannot decrypt $flake_dir/secrets/secrets.yaml" >&2 grep -h '^# public key:' ${q ageKeyFile} >&2 || true grep -h 'recipient:' "$flake_dir/secrets/secrets.yaml" >&2 || true if [ "$installed_age_key" = "true" ]; then rm -f ${q ageKeyFile} fi exit 1 fi ''; }
{ title = "nixos-install"; run = '' nixos-install \ ${installSource} \ --no-root-passwd \ --no-channel-copy ''; }
{ title = "sbctl: creating Secure Boot keys"; run = '' ${lib.optionalString (chrootPersistDir != "") '' mkdir -p ${q "${cfg.persistDir}/var/lib/sbctl"} /mnt/var/lib ln -sfT ${q "${chrootPersistDir}/var/lib/sbctl"} /mnt/var/lib/sbctl ''} nixos-enter --root /mnt -c ${q "${sbctl} create-keys"} ''; } ];
prelude = '' die() { echo "ERROR: $*" >&2 exit 1 }
ask_password() { local label="$1" pass pass2 while true; do pass="$(${askpass} " password for $label: ")" \ || die "could not ask for a password (no display and no tty)" pass2="$(${askpass} " confirm password for $label: ")" \ || die "could not ask for a password (no display and no tty)" if [ -z "$pass" ]; then echo " empty password, try again" >&2 continue fi if [ "$pass" = "$pass2" ]; then printf '%s' "$pass" return fi echo " passwords don't match, try again" >&2 done }
${lib.optionalString (cfg.requireSops || !cfg.prebuilt) ''flake_dir="$(pwd)"''} ''; in pkgs.writeShellApplication { name = "${lib.optionalString resume "resume-"}install-${hostname}";
runtimeInputs = [ pkgs.coreutils pkgs.dbus pkgs.mkpasswd pkgs.nixos-enter pkgs.nixos-install ] ++ lib.optionals cfg.requireSops [ pkgs.age pkgs.age-plugin-fido2-hmac pkgs.sops ];
text = lib.concatStringsSep "\n" ( [ prelude ] ++ lib.imap1 (renderStep (lib.length steps)) steps ++ [ ''echo "==> Ready!"'' ] );
meta.description = "${if resume then "Resume installation" else "Install"} for ${hostname}"; };in{ options.modules.installer = { requireSops = lib.mkOption { type = lib.types.bool; default = false; description = "Whether the host installer requires SOPS setup."; };
prebuilt = lib.mkOption { type = lib.types.bool; default = false; description = "Install the system closure built by this flake (nixos-install --system) instead of letting nixos-install evaluate and build it from the flake."; };
persistDir = lib.mkOption { type = lib.types.str; default = if builtins.hasAttr "/persist" config.fileSystems then "/mnt/persist" else "/mnt"; description = "Where the installer keeps persistent state (passwords, age key, machine-id) while /mnt is mounted."; };
passwordUsers = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ config.modules.user.name "root" ]; description = "Users the installer asks a password for."; }; };
config.system.build = { installer = mkInstaller { }; resumeInstaller = mkInstaller { resume = true; }; };}