diff --git a/build-images.sh b/build-images.sh index 4ac3279..23ea25d 100755 --- a/build-images.sh +++ b/build-images.sh @@ -27,6 +27,12 @@ build_kexec_installer() { echo "$out/nixos-kexec-installer${variant}-$arch.tar.gz" } +build_image_installer() { + declare -r tag=$1 arch=$2 tmp=$3 + out=$(nix build --print-out-paths --option accept-flake-config true -L ".#packages.${arch}.image-installer-${tag//./}${variant}") + echo "$out/iso/nixos-installer-${arch}.iso" +} + main() { declare -r tag=${1:-nixos-unstable} arch=${2:-x86_64-linux} tmp="$(mktemp -d)" @@ -35,6 +41,7 @@ main() { build_kexec_installer "$tag" "$arch" "$tmp" "" build_kexec_installer "$tag" "$arch" "$tmp" "-noninteractive" build_netboot_image "$tag" "$arch" "$tmp" + build_image_installer "$tag" "$arch" "$tmp" ) | readarray -t assets for asset in "${assets[@]}"; do pushd "$(dirname "$asset")" diff --git a/flake.nix b/flake.nix index 54d613d..68b8183 100644 --- a/flake.nix +++ b/flake.nix @@ -18,6 +18,7 @@ netboot = nixpkgs: (import (nixpkgs + "/nixos/release.nix") { }).netboot.${system}; kexec-installer = nixpkgs: modules: (nixpkgs.legacyPackages.${system}.nixos (modules ++ [ self.nixosModules.kexec-installer ])).config.system.build.kexecTarball; netboot-installer = nixpkgs: (nixpkgs.legacyPackages.${system}.nixos [ self.nixosModules.netboot-installer ]).config.system.build.netboot; + image-installer = nixpkgs: (nixpkgs.legacyPackages.${system}.nixos [ self.nixosModules.image-installer ]).config.system.build.isoImage; in { netboot-nixos-unstable = netboot nixos-unstable; @@ -25,6 +26,9 @@ kexec-installer-nixos-unstable = kexec-installer nixos-unstable [ ]; kexec-installer-nixos-2311 = kexec-installer nixos-2311 [ ]; + image-installer-unstable = image-installer nixos-unstable; + image-installer-2311 = image-installer nixos-2311; + kexec-installer-nixos-unstable-noninteractive = kexec-installer nixos-unstable [ { system.kexec-installer.name = "nixos-kexec-installer-noninteractive"; @@ -46,6 +50,7 @@ noninteractive = ./nix/noninteractive.nix; # TODO: also add a test here once we have https://github.com/NixOS/nixpkgs/pull/228346 merged netboot-installer = ./nix/netboot-installer/module.nix; + image-installer = ./nix/image-installer/module.nix; }; checks = let diff --git a/nix/image-installer/hidden-ssh-announcement.nix b/nix/image-installer/hidden-ssh-announcement.nix new file mode 100644 index 0000000..41ff88c --- /dev/null +++ b/nix/image-installer/hidden-ssh-announcement.nix @@ -0,0 +1,63 @@ +{ + config, + lib, + pkgs, + ... +}: +{ + options.hidden-ssh-announce = { + enable = lib.mkEnableOption "hidden-ssh-announce"; + script = lib.mkOption { + type = lib.types.package; + default = pkgs.writers.writeDash "test-output" "echo $1"; + description = '' + script to run when the hidden tor service was started and they hostname is known. + takes the hostname as $1 + ''; + }; + }; + + config = lib.mkIf config.hidden-ssh-announce.enable { + services.openssh.enable = true; + services.tor = { + enable = true; + relay.onionServices.hidden-ssh = { + version = 3; + map = [ + { + port = 22; + target.port = 22; + } + ]; + }; + client.enable = true; + }; + systemd.services.hidden-ssh-announce = { + description = "announce hidden ssh"; + after = [ + "tor.service" + "network-online.target" + ]; + wants = [ + "tor.service" + "network-online.target" + ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + # ${pkgs.tor}/bin/torify + ExecStart = pkgs.writeShellScript "announce-hidden-service" '' + set -efu + until test -e ${config.services.tor.settings.DataDirectory}/onion/hidden-ssh/hostname; do + echo "still waiting for ${config.services.tor.settings.DataDirectory}/onion/hidden-ssh/hostname" + sleep 1 + done + + ${config.hidden-ssh-announce.script} "$(cat ${config.services.tor.settings.DataDirectory}/onion/hidden-ssh/hostname)" + ''; + PrivateTmp = "true"; + User = "tor"; + Type = "oneshot"; + }; + }; + }; +} diff --git a/nix/image-installer/module.nix b/nix/image-installer/module.nix new file mode 100644 index 0000000..fb5a7b8 --- /dev/null +++ b/nix/image-installer/module.nix @@ -0,0 +1,122 @@ +{ + lib, + pkgs, + modulesPath, + ... +}: +let + network-status = pkgs.writeShellScript "network-status" '' + export PATH=${ + lib.makeBinPath ( + with pkgs; + [ + iproute2 + coreutils + gnugrep + nettools + gum + ] + ) + } + set -efu -o pipefail + msgs=() + if [[ -e /var/shared/qrcode.utf8 ]]; then + qrcode=$(gum style --border-foreground 240 --border normal "$(< /var/shared/qrcode.utf8)") + msgs+=("$qrcode") + fi + network_status="Root password: $(cat /var/shared/root-password) + Local network addresses: + $(ip -brief -color addr | grep -v 127.0.0.1) + $([[ -e /var/shared/onion-hostname ]] && echo "Onion address: $(cat /var/shared/onion-hostname)" || echo "Onion address: Waiting for tor network to be ready...") + Multicast DNS: $(hostname).local" + network_status=$(gum style --border-foreground 240 --border normal "$network_status") + msgs+=("$network_status") + msgs+=("Press 'Ctrl-C' for console access") + + gum join --vertical "''${msgs[@]}" + ''; +in +{ + imports = [ + (modulesPath + "/installer/cd-dvd/installation-cd-base.nix") + ../installer.nix + ./wifi.nix + ./hidden-ssh-announcement.nix + ]; + systemd.tmpfiles.rules = [ "d /var/shared 0777 root root - -" ]; + services.openssh.settings.PermitRootLogin = "yes"; + system.activationScripts.root-password = '' + mkdir -p /var/shared + ${pkgs.xkcdpass}/bin/xkcdpass --numwords 3 --delimiter - --count 1 > /var/shared/root-password + echo "root:$(cat /var/shared/root-password)" | chpasswd + ''; + hidden-ssh-announce = { + enable = true; + script = pkgs.writeShellScript "write-hostname" '' + set -efu + export PATH=${ + lib.makeBinPath ( + with pkgs; + [ + iproute2 + coreutils + jq + qrencode + ] + ) + } + + mkdir -p /var/shared + echo "$1" > /var/shared/onion-hostname + local_addrs=$(ip -json addr | jq '[map(.addr_info) | flatten | .[] | select(.scope == "global") | .local]') + jq -nc \ + --arg password "$(cat /var/shared/root-password)" \ + --arg onion_address "$(cat /var/shared/onion-hostname)" \ + --argjson local_addrs "$local_addrs" \ + '{ password: $password, tor: $onion_address, addresses: $local_addrs }' \ + > /var/shared/login.json + cat /var/shared/login.json | qrencode -s 2 -m 2 -t utf8 -o /var/shared/qrcode.utf8 + ''; + }; + + services.getty.autologinUser = lib.mkForce "root"; + + console.earlySetup = true; + console.font = lib.mkDefault "${pkgs.terminus_font}/share/consolefonts/ter-u22n.psf.gz"; + + # Less ipv6 addresses to reduce the noise + networking.tempAddresses = "disabled"; + + # Tango theme: https://yayachiken.net/en/posts/tango-colors-in-terminal/ + console.colors = lib.mkDefault [ + "000000" + "CC0000" + "4E9A06" + "C4A000" + "3465A4" + "75507B" + "06989A" + "D3D7CF" + "555753" + "EF2929" + "8AE234" + "FCE94F" + "739FCF" + "AD7FA8" + "34E2E2" + "EEEEEC" + ]; + + programs.bash.interactiveShellInit = '' + if [[ "$(tty)" =~ /dev/(tty1|hvc0|ttyS0)$ ]]; then + # workaround for https://github.com/NixOS/nixpkgs/issues/219239 + systemctl restart systemd-vconsole-setup.service + + watch --no-title --color ${network-status} + fi + ''; + + # No one got time for xz compression. + isoImage.squashfsCompression = "zstd"; + isoImage.isoName = lib.mkForce "nixos-installer-${pkgs.system}.iso"; +} diff --git a/nix/image-installer/wifi.nix b/nix/image-installer/wifi.nix new file mode 100644 index 0000000..ec78c90 --- /dev/null +++ b/nix/image-installer/wifi.nix @@ -0,0 +1,17 @@ +{ + imports = [ ../networkd.nix ]; + # use iwd instead of wpa_supplicant + networking.wireless.enable = false; + + # Use iwd instead of wpa_supplicant. It has a user friendly CLI + networking.wireless.iwd = { + enable = true; + settings = { + Network = { + EnableIPv6 = true; + RoutePriorityOffset = 300; + }; + Settings.AutoConnect = true; + }; + }; +} diff --git a/nix/installer.nix b/nix/installer.nix index 45989d6..3fed13d 100644 --- a/nix/installer.nix +++ b/nix/installer.nix @@ -1,4 +1,7 @@ { config, lib, pkgs, ... }: { + # more descriptive hostname than just "nixos" + networking.hostName = lib.mkDefault "nixos-installer"; + # We are stateless, so just default to latest. system.stateVersion = config.system.nixos.version; @@ -8,20 +11,7 @@ }).latestCompatibleLinuxPackages; boot.zfs.removeLinuxDRM = lib.mkDefault pkgs.hostPlatform.isAarch64; - # IPMI SOL console redirection stuff - boot.kernelParams = - [ "console=tty0" ] ++ - (lib.optional (pkgs.stdenv.hostPlatform.isAarch32 || pkgs.stdenv.hostPlatform.isAarch64) "console=ttyAMA0,115200") ++ - (lib.optional (pkgs.stdenv.hostPlatform.isRiscV) "console=ttySIF0,115200") ++ - [ "console=ttyS0,115200" ]; - documentation.enable = false; - # Not really needed. Saves a few bytes and the only service we are running is sshd, which we want to be reachable. - networking.firewall.enable = false; - - networking.useNetworkd = true; - systemd.network.enable = true; - networking.dhcpcd.enable = false; environment.systemPackages = [ # for zapping of disko @@ -36,40 +26,4 @@ # Don't add nixpkgs to the image to save space, for our intended use case we don't need it system.installer.channel.enable = false; - - systemd.services.log-network-status = { - wantedBy = [ "multi-user.target" ]; - # No point in restarting this. We just need this after boot - restartIfChanged = false; - - serviceConfig = { - Type = "oneshot"; - StandardOutput = "journal+console"; - ExecStart = [ - # Allow failures, so it still prints what interfaces we have even if we - # not get online - "-${pkgs.systemd}/lib/systemd/systemd-networkd-wait-online" - "${pkgs.iproute2}/bin/ip -c addr" - "${pkgs.iproute2}/bin/ip -c -6 route" - "${pkgs.iproute2}/bin/ip -c -4 route" - "${pkgs.systemd}/bin/networkctl status" - ]; - }; - }; - - # Restore ssh host and user keys if they are available. - # This avoids warnings of unknown ssh keys. - boot.initrd.postMountCommands = '' - mkdir -m 700 -p /mnt-root/root/.ssh - mkdir -m 755 -p /mnt-root/etc/ssh - mkdir -m 755 -p /mnt-root/root/network - if [[ -f ssh/authorized_keys ]]; then - install -m 400 ssh/authorized_keys /mnt-root/root/.ssh - fi - install -m 400 ssh/ssh_host_* /mnt-root/etc/ssh - cp *.json /mnt-root/root/network/ - if [[ -f machine-id ]]; then - cp machine-id /mnt-root/etc/machine-id - fi - ''; } diff --git a/nix/kexec-installer/module.nix b/nix/kexec-installer/module.nix index 61c56b2..a10e6a5 100644 --- a/nix/kexec-installer/module.nix +++ b/nix/kexec-installer/module.nix @@ -10,6 +10,9 @@ in imports = [ (modulesPath + "/installer/netboot/netboot-minimal.nix") ../installer.nix + ../networkd.nix + ../serial.nix + ../restore-remote-access.nix ]; options = { system.kexec-installer.name = lib.mkOption { diff --git a/nix/kexec-installer/restore_routes.py b/nix/kexec-installer/restore_routes.py index c8d3bc8..1635376 100644 --- a/nix/kexec-installer/restore_routes.py +++ b/nix/kexec-installer/restore_routes.py @@ -78,8 +78,6 @@ MACAddress = {interface["address"]} [Network] # both ipv4 and ipv6 DHCP = yes -# link-local multicast name resolution -LLMNR = yes # lets us discover the switch port we're connected to LLDP = yes # ipv6 router advertisements diff --git a/nix/kexec-installer/test.nix b/nix/kexec-installer/test.nix index d7b3cf6..a11354d 100644 --- a/nix/kexec-installer/test.nix +++ b/nix/kexec-installer/test.nix @@ -160,7 +160,7 @@ makeTest' { assert ssh(["ls", "-la", "/run/foo"], check=False).returncode != 0, "kexeced node1 still has /run/foo" print(ssh(["parted", "--version"])) host = ssh(["hostname"], stdout=subprocess.PIPE).stdout.strip() - assert host == "nixos", f"hostname is {host}, not nixos" + assert host == "nixos-installer", f"hostname is {host}, not nixos-installer" host_ed25519_after = ssh(["cat", "/etc/ssh/ssh_host_ed25519_key.pub"], stdout=subprocess.PIPE).stdout.strip() assert host_ed25519_before == host_ed25519_after, f"'{host_ed25519_before}' != '{host_ed25519_after}'" diff --git a/nix/log-network-status.nix b/nix/log-network-status.nix new file mode 100644 index 0000000..557c7b4 --- /dev/null +++ b/nix/log-network-status.nix @@ -0,0 +1,22 @@ +{ pkgs, ... }: +{ + systemd.services.log-network-status = { + wantedBy = [ "multi-user.target" ]; + # No point in restarting this. We just need this after boot + restartIfChanged = false; + + serviceConfig = { + Type = "oneshot"; + StandardOutput = "journal+console"; + ExecStart = [ + # Allow failures, so it still prints what interfaces we have even if we + # not get online + "-${pkgs.systemd}/lib/systemd/systemd-networkd-wait-online" + "${pkgs.iproute2}/bin/ip -c addr" + "${pkgs.iproute2}/bin/ip -c -6 route" + "${pkgs.iproute2}/bin/ip -c -4 route" + "${pkgs.systemd}/bin/networkctl status" + ]; + }; + }; +} diff --git a/nix/netboot-installer/module.nix b/nix/netboot-installer/module.nix index 0f9d3d7..590bf56 100644 --- a/nix/netboot-installer/module.nix +++ b/nix/netboot-installer/module.nix @@ -3,6 +3,9 @@ imports = [ (modulesPath + "/installer/netboot/netboot-minimal.nix") ../installer.nix + ../networkd.nix + ../serial.nix + ../restore-remote-access.nix ]; # We are stateless, so just default to latest. @@ -25,7 +28,6 @@ matchConfig.Type = "ether"; networkConfig = { DHCP = "yes"; - LLMNR = "yes"; EmitLLDP = "yes"; IPv6AcceptRA = "yes"; MulticastDNS = "yes"; diff --git a/nix/networkd.nix b/nix/networkd.nix new file mode 100644 index 0000000..1e67033 --- /dev/null +++ b/nix/networkd.nix @@ -0,0 +1,13 @@ +{ lib, ... }: +{ + # Not really needed. Saves a few bytes and the only service we are running is sshd, which we want to be reachable. + networking.firewall.enable = false; + + networking.useNetworkd = true; + systemd.network.enable = true; + + # mdns + networking.firewall.allowedUDPPorts = [ 5353 ]; + systemd.network.networks."99-ethernet-default-dhcp".networkConfig.MulticastDNS = lib.mkDefault "yes"; + systemd.network.networks."99-wireless-client-dhcp".networkConfig.MulticastDNS = lib.mkDefault "yes"; +} diff --git a/nix/restore-remote-access.nix b/nix/restore-remote-access.nix new file mode 100644 index 0000000..8898e9c --- /dev/null +++ b/nix/restore-remote-access.nix @@ -0,0 +1,17 @@ +{ + # Restore ssh host and user keys if they are available. + # This avoids warnings of unknown ssh keys. + boot.initrd.postMountCommands = '' + mkdir -m 700 -p /mnt-root/root/.ssh + mkdir -m 755 -p /mnt-root/etc/ssh + mkdir -m 755 -p /mnt-root/root/network + if [[ -f ssh/authorized_keys ]]; then + install -m 400 ssh/authorized_keys /mnt-root/root/.ssh + fi + install -m 400 ssh/ssh_host_* /mnt-root/etc/ssh + cp *.json /mnt-root/root/network/ + if [[ -f machine-id ]]; then + cp machine-id /mnt-root/etc/machine-id + fi + ''; +} diff --git a/nix/serial.nix b/nix/serial.nix new file mode 100644 index 0000000..aca1006 --- /dev/null +++ b/nix/serial.nix @@ -0,0 +1,11 @@ +{ pkgs, lib, ... }: +{ + # IPMI SOL console redirection stuff + boot.kernelParams = + [ "console=tty0" ] + ++ (lib.optional ( + pkgs.stdenv.hostPlatform.isAarch32 || pkgs.stdenv.hostPlatform.isAarch64 + ) "console=ttyAMA0,115200") + ++ (lib.optional (pkgs.stdenv.hostPlatform.isRiscV) "console=ttySIF0,115200") + ++ [ "console=ttyS0,115200" ]; +}