diff --git a/flake.nix b/flake.nix index da33afc..9a08cdb 100644 --- a/flake.nix +++ b/flake.nix @@ -23,6 +23,7 @@ let system = "x86_64-linux"; inherit (nixpkgs) lib; + pkgs = nixpkgs.legacyPackages.${system}; vars = import ./variables.nix; # Generates a nixosConfiguration from a platform (what it runs on) and @@ -86,9 +87,65 @@ lxc-pxe-boot = mkTarget { platform = "lxc"; buildType = "pxe-boot"; hostPath = ./hosts/pxe-boot/host.nix; }; }; + # Auto-install environments (migrated from the former nix-auto-installer + # flake): a self-contained NixOS installer that boots, discovers this + # flake's own nixosConfigurations over the network, and runs + # nixos-install against whichever one the operator picks. These are + # deliberately not part of the platform x build-type matrix above — + # they're throwaway boot media, not persistent hosts, so they skip + # disko/sops-nix/home-manager and just need `vars`. + installerTargets = { + installer = nixpkgs.lib.nixosSystem { + inherit system; + modules = [ ./modules/installer/iso.nix ]; + specialArgs = { inherit vars; }; + }; + + proxmox-lxc = nixpkgs.lib.nixosSystem { + inherit system; + modules = [ ./modules/installer/proxmox-lxc.nix ]; + specialArgs = { inherit vars; }; + }; + }; + + # Same installer environment, built as netboot (kernel + initrd + + # iPXE script) instead of an ISO — this is what packages.pxe bundles. + netbootSystem = nixpkgs.lib.nixosSystem { + inherit system; + modules = [ + ./modules/installer/iso.nix + ({ modulesPath, ... }: { + imports = [ + (modulesPath + "/installer/netboot/netboot-minimal.nix") + ]; + }) + ]; + specialArgs = { inherit vars; }; + }; + in { - nixosConfigurations = generatedTargets; + nixosConfigurations = generatedTargets // installerTargets; + + # Buildable auto-installer artifacts (`nix build .#`), migrated + # unchanged from nix-auto-installer's interface. + packages.${system} = rec { + iso = installerTargets.installer.config.system.build.isoImage; + + lxc = installerTargets.proxmox-lxc.config.system.build.tarball; + + pxe = pkgs.linkFarm "pxe" [ + { name = "netboot.ipxe"; path = netbootSystem.config.system.build.netbootIpxeScript; } + { name = "initrd"; path = netbootSystem.config.system.build.netbootRamdisk; } + { name = "kernel"; path = netbootSystem.config.system.build.kernel; } + ]; + + all = pkgs.linkFarm "all" [ + { name = "iso"; path = iso; } + { name = "lxc"; path = lxc; } + { name = "pxe"; path = pxe; } + ]; + }; }; } diff --git a/modules/installer/common.nix b/modules/installer/common.nix new file mode 100644 index 0000000..b7118b1 --- /dev/null +++ b/modules/installer/common.nix @@ -0,0 +1,202 @@ +{ pkgs, lib, vars, ... }: + +{ + networking.useDHCP = lib.mkDefault true; + + 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..." + 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 '.[]' + ) + + if [[ ''${#options[@]} -eq 0 ]]; then + echo "ERROR: No NixOS hosts found in ''${FLAKE_BASE_URL}#nixosConfigurations" >&2 + exit 1 + fi + + 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 + + if nix eval --refresh --json --extra-experimental-features "flakes nix-command" "''${nix_extra_opts[@]}" "''${FLAKE_BASE_URL}#nixosConfigurations.''${choice}.config.disko.devices.disk.main.device" >/dev/null 2>&1; then + disko --mode destroy,format,mount \ + --flake "''${FLAKE_BASE_URL}#''${choice}" "''${nix_extra_opts[@]}" --yes + else + # No raw disk to partition — true for every LXC container, which has + # no block device visible from inside it, only its already-mounted + # root filesystem. nixos-install defaults to installing into /mnt; if + # nothing points /mnt at the real root, the new system gets built and + # written to a disconnected empty directory, never actually becomes + # bootable, and the container silently keeps running this installer + # environment forever. Bind-mount / onto /mnt so the install actually + # lands on the filesystem this container boots from. + echo "Selected host has no Disko configuration — installing in place via bind mount." + mount --bind / /mnt + 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 + 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; + }; + + users.users.root = { + hashedPassword = + "$6$Kwv9KAyvcurAViQF$H4.u3feqGE7lVoNgkFXhE3n2Pmo//9JYDTCz8ifrVHBxPjwa1xMby7tEZ8Bpt5MXs9Rkx6/YbZWxs5CpH0s/70"; + }; + + users.users.${vars.primaryUser} = { + isNormalUser = true; + + extraGroups = [ + "wheel" + ]; + + shell = pkgs.bashInteractive; + + hashedPassword = + "$6$Kwv9KAyvcurAViQF$H4.u3feqGE7lVoNgkFXhE3n2Pmo//9JYDTCz8ifrVHBxPjwa1xMby7tEZ8Bpt5MXs9Rkx6/YbZWxs5CpH0s/70"; + + openssh.authorizedKeys.keys = [ + vars.adminSshKey + ]; + }; + + system.stateVersion = "26.05"; +} diff --git a/modules/installer/iso.nix b/modules/installer/iso.nix new file mode 100644 index 0000000..bc68c82 --- /dev/null +++ b/modules/installer/iso.nix @@ -0,0 +1,8 @@ +{ modulesPath, ... }: + +{ + imports = [ + "${modulesPath}/installer/cd-dvd/installation-cd-minimal.nix" + ./common.nix + ]; +} diff --git a/modules/installer/proxmox-lxc.nix b/modules/installer/proxmox-lxc.nix new file mode 100644 index 0000000..887c97f --- /dev/null +++ b/modules/installer/proxmox-lxc.nix @@ -0,0 +1,12 @@ +{ modulesPath, ... }: + +{ + imports = [ + ./common.nix + (modulesPath + "/virtualisation/proxmox-lxc.nix") + ]; + + # LXC has no bootloader + boot.loader.grub.enable = false; + boot.loader.systemd-boot.enable = false; +} diff --git a/variables.nix b/variables.nix index cb1d798..eb8ea29 100644 --- a/variables.nix +++ b/variables.nix @@ -25,6 +25,10 @@ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxXTQxFnArK5HXG7czeoybZebCGfxpUdusJkPn+BCSp root@server" ]; + # Admin SSH public key, authorized on the primary user of every host and + # the installer image's nixos/root users. + adminSshKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCq/Q5LvIXlZwO2kdeAN5nLGZ59nZB7JHYMEszHxmNtGMzv1lM31jiPNsr0z2EKVZhE7OOfa2IF9rhWYD7JUA9G0yzdZ4WTXFNGVVOJoOVH6vAF3XCxoVilOEwTc7h2Wiy+rzd0B28/3spffzQQWJhY6GRQVa8j+6xAGF60Fcvl1vLosYT9Bn2ZbK4TCWOwAn2jqXIieGpZdn/UNZbGOeKRiCvhktDfMAzuQzN/9jMu/oF4pkPn2X1UrsQdNlvp0Ci8md612MozIpncQJyAF1ADhunr3sMx0isUXiqD29R5DS4TftpekqLNLak+zcxFa8N7DcRNp3DcKfJvyTkwQrR4r+b7lFLYOLHLagSso9CzeW/paAS2q9I5SBm/2DtE1diLLg2jZikYcstsu/G5RgvbzbKqjiaMwTdXC3AMvDxQrs7U5pDRZFzoofG3cpODbTm+uy3m0kP70z0M1K45UbDG0p+itnTu9x40JbQEgefbx38AItNvAIx1A8HO4I1VX28= wayne@stream"; + # System timeZone = "Australia/Brisbane"; primaryUser = "nixos"; # main interactive user on every host