{ pkgs, lib, vars, ... }: { imports = [ ./host-keys.nix ]; networking.useDHCP = lib.mkDefault true; # Recommended over the true default (bypasses ZFS's own import safeguards) # per the option's own docs. This installer environment has no ZFS pools # of its own to import, so this is a no-op here — just silences the # eval-time warning, matching modules/common/configuration.nix. boot.zfs.forceImportRoot = false; time.timeZone = vars.timeZone; # Without this, the installer only ever sees cache.nixos.org, which # doesn't carry sops-install-secrets (it's built straight from the # sops-nix flake's own Go source, not part of nixpkgs) — every install # would otherwise compile it from scratch, which is what ran an 8GB LXC # container's disk out of space. Push a built copy to nix-cache once # (from a machine with real disk headroom) and every future install, # of any type, fetches instead of rebuilding. nix.settings = { substituters = [ "http://nix-cache" "https://cache.nixos.org/" ]; trusted-public-keys = [ "cache.local-1:usoWYanY3Kpq2+kDIS2nhWoLZiRxanmdysdzqCFBHW4=" "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" ]; }; environment = { systemPackages = with pkgs; [ git curl jq parted e2fsprogs btrfs-progs util-linux disko ]; # Write auto-install script to /root etc."auto-install.sh" = { text = '' #!/run/current-system/sw/bin/bash set -eux set -euo pipefail export FLAKE_BASE_URL="git+https://${vars.lanDomain}/beatzaplenty/nixos.git" echo "Fetching available NixOS hosts from flake..." # Two categories deliberately excluded from the menu: # lxc-* — these build a config.system.build.tarball meant for # `pct restore` on Proxmox directly, not an install. # Running nixos-install against one here would # bind-mount / onto /mnt and then refuse to touch the # filesystem it's currently running on — see # docs/auto-installer.md. # installer — this *is* the installer image's own flake target, # not a deployable host; "installing" it means # nixos-install-ing a copy of the installer into # itself. mapfile -t options < <( nix eval --json --no-use-registries --no-accept-flake-config --extra-experimental-features "flakes nix-command" \ "''${FLAKE_BASE_URL}#nixosConfigurations" \ --apply builtins.attrNames \ | jq -r '.[] | select(startswith("lxc-") | not) | select(. != "installer")' ) if [[ ''${#options[@]} -eq 0 ]]; then echo "ERROR: No NixOS hosts found in ''${FLAKE_BASE_URL}#nixosConfigurations" >&2 exit 1 fi echo "Note: lxc-* targets aren't installed this way — build them with" echo " nix build .#nixosConfigurations..config.system.build.tarball" echo "and 'pct restore' the result on Proxmox directly. See docs/auto-installer.md." echo "Choose the flake profile to install:" select choice in "''${options[@]}"; do if [[ -n "$choice" ]]; then echo "You selected: $choice" break else echo "Invalid selection. Try again." fi done echo "Starting install with flake: ''${FLAKE_BASE_URL}#''${choice}" # Optional: confirm before proceeding read -rp "Proceed with installation? (y/N): " confirm if [[ ! "$confirm" =~ ^[Yy]$ ]]; then echo "Aborted." exit 1 fi # A nix-cache host is *the* substituter/remote-builder for every other # host once installed (its own config explicitly excludes itself from # using either — see buildType != "nix-cache" in the nixos flake.nix). # Installing one shouldn't depend on a nix-cache substituter either, # for the same reason — plus in practice "nix-cache" only resolves over # Tailscale, which a fresh installer environment was never connected to # anyway, so it's dead weight even for non-nix-cache installs until # that's sorted out. Override it away here specifically for nix-cache # targets to keep install-time behaviour consistent with run-time. nix_extra_opts=() if [[ "''${choice}" == *-nix-cache ]]; then echo "Installing a nix-cache host — skipping the nix-cache substituter." nix_extra_opts+=(--option substituters "https://cache.nixos.org/") fi # Every host reachable through this menu has a Disko config (lxc-* # is filtered out above, and is the only category that doesn't — # see docs/auto-installer.md), so this can run unconditionally: no # need to probe the flake first and branch on whether Disko applies. disko --mode destroy,format,mount \ --flake "''${FLAKE_BASE_URL}#''${choice}" "''${nix_extra_opts[@]}" --yes-wipe-all-disks # 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 with scripts/secrets/prepare-host-key.sh first. # # Two places a key can come from, checked in order: # /etc/host-keys — baked into this image at build time (see # modules/installer/host-keys.nix; only present # if built with NIXOS_HOST_KEYS_DIR set) # /root/host-keys — scp'd in manually after boot (older fallback, # still supported for images built without keys) mkdir -p /root/host-keys if [[ -f "/etc/host-keys/''${choice}_ssh_host_ed25519_key" ]]; then echo "Found baked-in SSH host key for ''${choice}, installing to target..." install -D -m 0600 "/etc/host-keys/''${choice}_ssh_host_ed25519_key" /mnt/etc/ssh/ssh_host_ed25519_key install -D -m 0644 "/etc/host-keys/''${choice}_ssh_host_ed25519_key.pub" /mnt/etc/ssh/ssh_host_ed25519_key.pub elif [[ -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 SSH host key found for ''${choice} (checked /etc/host-keys and /root/host-keys)" echo "sops-nix secrets (including the login password) will NOT decrypt on first boot." echo "Run scripts/secrets/prepare-host-key.sh for host ''${choice} on your admin workstation first," echo "then either rebuild this image with NIXOS_HOST_KEYS_DIR set, or scp the result to" echo "/root/host-keys/ on this machine." 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 export TMPDIR=/mnt/install-tmp nixos-install \ --flake "''${FLAKE_BASE_URL}#''${choice}" \ "''${nix_extra_opts[@]}" \ --no-root-password rm -rf /mnt/install-tmp # Redundant copy of the host's private key — the real one is now at # /etc/ssh/ssh_host_ed25519_key. Nothing NixOS-managed ever cleans this # up on its own since it was written imperatively, not declaratively. rm -rf /root/host-keys sleep 10 reboot ''; mode = "0755"; }; }; programs.git.enable = true; # Run the installer on first login. Previously this copied an /etc file # into the nixos user's ~/.bash_profile via an activation script that # got dropped in a refactor (and only ever worked for that one user # anyway) — loginShellInit is NixOS's native hook for this, applies to # any user's login shell (root included), and needs no home-directory # file-copying/chown. programs.bash.loginShellInit = '' if [ -n "$PS1" ] && [ ! -e "$HOME/.auto_install_ran" ]; then sudo /etc/auto-install.sh touch "$HOME/.auto_install_ran" fi ''; services.openssh.enable = true; services.openssh.settings = { PermitRootLogin = "yes"; PasswordAuthentication = true; }; # nixpkgs' own installer profile (profiles/installation-device.nix, pulled # in via installation-cd-minimal.nix) sets initialHashedPassword = "" for # both users — its own passwordless-login convention for install media. # That's a second, non-null password option alongside our hashedPassword # below, which NixOS warns about as ambiguous precedence. Force it null # rather than adopting passwordless login: this image now also boots over # LAN PXE with PasswordAuthentication enabled, so passwordless root SSH # would be reachable by anyone on the LAN, not just local console. users.users.root = { hashedPassword = "$6$Kwv9KAyvcurAViQF$H4.u3feqGE7lVoNgkFXhE3n2Pmo//9JYDTCz8ifrVHBxPjwa1xMby7tEZ8Bpt5MXs9Rkx6/YbZWxs5CpH0s/70"; initialHashedPassword = lib.mkForce null; }; users.users.${vars.primaryUser} = { isNormalUser = true; extraGroups = [ "wheel" ]; shell = pkgs.bashInteractive; hashedPassword = "$6$Kwv9KAyvcurAViQF$H4.u3feqGE7lVoNgkFXhE3n2Pmo//9JYDTCz8ifrVHBxPjwa1xMby7tEZ8Bpt5MXs9Rkx6/YbZWxs5CpH0s/70"; initialHashedPassword = lib.mkForce null; openssh.authorizedKeys.keys = [ vars.adminSshKey ]; }; system.stateVersion = "26.05"; }