├── .github ├── dependabot.yml └── workflows │ ├── auto-merge.yaml │ ├── build.yml │ ├── update-flake-lock.yml │ └── update-nixos-release.yml ├── LICENSE ├── README.md ├── build-images.sh ├── flake.lock ├── flake.nix └── nix ├── image-installer ├── hidden-ssh-announcement.nix ├── module.nix ├── tests.nix └── wifi.nix ├── installer.nix ├── kexec-installer ├── kexec-run.sh ├── local-network-restore-test.sh ├── module.nix ├── restore_routes.py ├── ssh-keys │ ├── id_ed25519 │ └── id_ed25519.pub └── test.nix ├── latest-zfs-kernel.nix ├── log-network-status.nix ├── netboot-installer └── module.nix ├── networkd.nix ├── nix-settings.nix ├── noninteractive.nix ├── noveau-workaround.nix ├── python-minimal.nix ├── restore-remote-access.nix ├── serial.nix └── zfs-minimal.nix /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yaml: -------------------------------------------------------------------------------- 1 | name: Auto Merge Dependency Updates 2 | on: 3 | - pull_request_target 4 | jobs: 5 | auto-merge-dependency-updates: 6 | runs-on: ubuntu-latest 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | concurrency: 11 | group: "auto-merge:${{ github.head_ref }}" 12 | cancel-in-progress: true 13 | steps: 14 | - uses: Mic92/auto-merge@main 15 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: "Build" 2 | on: 3 | repository_dispatch: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | jobs: 9 | images: 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | tag: 14 | - nixos-25.05 15 | - nixos-unstable 16 | os: 17 | - nscloud-ubuntu-22.04-arm64-4x16 18 | - ubuntu-latest 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: cachix/install-nix-action@v31 23 | with: 24 | nix_path: nixpkgs=https://github.com/NixOS/nixpkgs/archive/nixpkgs-unstable.tar.gz 25 | - name: Build image 26 | run: ./build-images.sh "${{ matrix.tag }}" "$(nix eval --raw --impure --expr builtins.currentSystem)" 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | -------------------------------------------------------------------------------- /.github/workflows/update-flake-lock.yml: -------------------------------------------------------------------------------- 1 | name: update-flake-lock 2 | on: 3 | workflow_dispatch: # allows manual triggering 4 | schedule: 5 | - cron: '0 0 * * 1,4' # Run twice a week 6 | 7 | jobs: 8 | lockfile: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout repository 12 | uses: actions/checkout@v4 13 | - name: Install Nix 14 | uses: cachix/install-nix-action@v31 15 | 16 | - name: Generate App Token 17 | uses: actions/create-github-app-token@v2 18 | id: app-token 19 | with: 20 | app-id: ${{ vars.CI_APP_ID }} 21 | private-key: ${{ secrets.CI_APP_PRIVATE_KEY }} 22 | 23 | - name: Update flake.lock 24 | uses: DeterminateSystems/update-flake-lock@v25 25 | with: 26 | token: ${{ steps.app-token.outputs.token }} 27 | pr-labels: dependencies 28 | -------------------------------------------------------------------------------- /.github/workflows/update-nixos-release.yml: -------------------------------------------------------------------------------- 1 | name: Update NixOS Release 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 12 * * 1' 7 | 8 | jobs: 9 | update-nixos: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v4 14 | 15 | - name: Generate App Token 16 | uses: actions/create-github-app-token@v2 17 | id: app-token 18 | with: 19 | app-id: ${{ vars.CI_APP_ID }} 20 | private-key: ${{ secrets.CI_APP_PRIVATE_KEY }} 21 | 22 | - name: Find latest NixOS release 23 | id: find-version 24 | env: 25 | GH_TOKEN: ${{ steps.app-token.outputs.token }} 26 | run: | 27 | LATEST_VERSION=$( 28 | gh api repos/NixOS/nixpkgs/branches --paginate | 29 | jq -r '.[] | select(.name | test("^nixos-[0-9]{2}\\.[0-9]{2}$")) | .name' | 30 | sort -V | 31 | tail -1 32 | ) 33 | if [ -z "$LATEST_VERSION" ]; then 34 | echo "Failed to retrieve latest NixOS version from GitHub" 35 | exit 1 36 | fi 37 | echo "Latest version found: $LATEST_VERSION" 38 | echo "version=$LATEST_VERSION" >> $GITHUB_OUTPUT 39 | 40 | - name: Setup Nix 41 | uses: cachix/install-nix-action@v31 42 | 43 | - name: Update NixOS version 44 | run: | 45 | NEW_VERSION="${{ steps.find-version.outputs.version }}" 46 | sed -i "s/nixos-[0-9][0-9]\.[0-9][0-9]/${NEW_VERSION}/g" flake.nix .github/workflows/build.yml 47 | nix flake update 48 | 49 | - name: Create Pull Request 50 | uses: peter-evans/create-pull-request@v7 51 | with: 52 | title: "update to ${{ steps.find-version.outputs.version }}" 53 | token: ${{ steps.app-token.outputs.token }} 54 | labels: dependencies 55 | body: | 56 | This PR updates the NixOS release to ${{ steps.find-version.outputs.version }}. 57 | 58 | Changes were made automatically by the GitHub Action workflow. 59 | branch: update-nixos-${{ steps.find-version.outputs.version }} 60 | base: main 61 | delete-branch: true 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Nix community projects 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nixos-images 2 | 3 | This project provides automatically updated NixOS images that complement the official images from hydra.nixos.org. New images are built weekly to ensure you always have access to the latest NixOS features and security updates. 4 | 5 | ## Available Image Types 6 | 7 | We currently offer three types of NixOS images: 8 | 9 | - **[ISO Installer Images](#iso-installer-images)**: Bootable USB images for installing NixOS on physical hardware 10 | - **[Kexec Tarballs](#kexec-tarballs)**: For booting NixOS installer from an existing Linux system 11 | - **[Netboot Images](#netboot-images)**: For booting NixOS over the network via PXE/iPXE 12 | 13 | ## ISO Installer Images 14 | 15 | Our ISO installer images allow you to boot NixOS from a USB drive. These images have been optimized for both local and remote installations. 16 | 17 | ### Creating a Bootable NixOS USB Drive 18 | 19 | #### Step 1: Download the ISO image 20 | 21 | Choose the appropriate image for your system architecture: 22 | 23 | **For x86_64 (64-bit Intel/AMD):** 24 | ```bash 25 | wget https://github.com/nix-community/nixos-images/releases/download/nixos-unstable/nixos-installer-x86_64-linux.iso 26 | ``` 27 | 28 | **For aarch64 (64-bit ARM):** 29 | ```bash 30 | wget https://github.com/nix-community/nixos-images/releases/download/nixos-unstable/nixos-installer-aarch64-linux.iso 31 | ``` 32 | 33 | You can also download the images directly from the [releases page](https://github.com/nix-community/nixos-images/releases). 34 | 35 | #### Step 2: Identify your USB drive 36 | 37 | **On Linux:** 38 | ```bash 39 | lsblk 40 | ``` 41 | 42 | **On macOS:** 43 | ```bash 44 | diskutil list 45 | ``` 46 | 47 | Make careful note of the device name (e.g., `/dev/sdb`, `/dev/disk2`, etc.) - **writing to the wrong device can cause data loss!** 48 | 49 | #### Step 4: Write the ISO to the USB drive 50 | 51 | **On Linux:** 52 | ```bash 53 | # Replace /dev/sdX with your USB drive device 54 | sudo dd if=nixos-installer-x86_64-linux.iso of=/dev/sdX bs=4M status=progress conv=fsync 55 | ``` 56 | 57 | **On macOS:** 58 | ```bash 59 | # First unmount the drive (replace N with your disk number) 60 | diskutil unmountDisk /dev/diskN 61 | 62 | # Write the image (replace N with your disk number) 63 | sudo dd if=nixos-installer-x86_64-linux.iso of=/dev/rdiskN bs=1m 64 | ``` 65 | 66 | **On Windows:** 67 | We recommend using tools like [Rufus](https://rufus.ie/), [balenaEtcher](https://www.balena.io/etcher/), or [Ventoy](https://www.ventoy.net/) to write the ISO: 68 | 1. Download and run one of these tools 69 | 2. Select the downloaded ISO file 70 | 3. Select your USB drive (the tool will show available drives) 71 | 4. Start the writing process 72 | 73 | #### Step 5: Boot from the USB drive 74 | 75 | 1. Insert the USB drive into the target computer 76 | 2. Restart the computer 77 | 3. **Disable Secure Boot in BIOS/UEFI** (NixOS installer currently requires Secure Boot to be disabled) 78 | 4. Enter the boot menu (usually by pressing F12, F2, or Del during startup) 79 | 5. Select the USB drive as the boot device 80 | 81 | ### Special Features of the NixOS Installer 82 | 83 | Our installer has been optimized for both local and remote installations (like with [nixos-anywhere](https://github.com/numtide/nixos-anywhere) and [clan](https://docs.clan.lol/getting-started/installer/)): 84 | 85 | * **SSH Access**: OpenSSH server is enabled by default for remote installations 86 | * **Security**: A random root password is generated on each boot 87 | * **Remote Access via Tor**: A Tor hidden SSH service is enabled, allowing access via `torify ssh .onion` 88 | * **Easy Configuration**: A QR code is displayed that contains local addresses and the root password 89 | * **Simplified WiFi Setup**: Includes [IWD](https://wiki.archlinux.org/title/iwd) daemon: 90 | * Run `iwctl` in the terminal for an interactive WiFi setup interface 91 | * Use `iwctl station list` to list WiFi adapters 92 | * Use `iwctl station scan` to scan for networks 93 | * Use `iwctl station connect ` to connect 94 | 95 | ### What's Next? 96 | 97 | After booting the installer, you can: 98 | 1. Use [disko](https://github.com/nix-community/disko) for declarative disk partitioning 99 | 2. Follow the [NixOS manual](https://nixos.org/manual/nixos/stable/) for installation 100 | 3. Use [nixos-anywhere](https://github.com/numtide/nixos-anywhere) for automated installation 101 | 102 | ![Screenshot of the installer](https://github.com/nix-community/nixos-images/releases/download/assets/image-installer-screenshot.jpg) 103 | 104 | 105 | ## Kexec Tarballs 106 | 107 | Kexec tarballs provide a way to boot the NixOS installer directly from an existing Linux system without requiring physical media or rebooting. 108 | 109 | ### What is Kexec? 110 | 111 | Kexec is a mechanism in Linux that allows you to load and boot a new kernel from within a currently running Linux system. This is particularly useful for: 112 | 113 | - **Remote server installations** where you don't have physical access 114 | - **Cloud providers** that don't offer NixOS as an installation option 115 | - **Quick system reinstalls** without needing to create bootable media 116 | 117 | ### Requirements 118 | 119 | - Secure Boot must be disabled in BIOS/UEFI 120 | - At least 1GB of physical RAM (swap does not count) 121 | - Root access on the existing Linux system 122 | 123 | ### Using the Kexec Installer 124 | 125 | #### Step 1: Download and Run the Installer 126 | 127 | Run these commands as root on your existing Linux system: 128 | 129 | ```bash 130 | curl -L https://github.com/nix-community/nixos-images/releases/latest/download/nixos-kexec-installer-noninteractive-x86_64-linux.tar.gz | tar -xzf- -C /root 131 | /root/kexec/run 132 | ``` 133 | 134 | After executing these commands, there will be a short delay (6 seconds) before the kexec process replaces your current kernel with the NixOS installer kernel. This delay allows you to disconnect cleanly if running the commands over SSH. 135 | 136 | #### What Happens Next? 137 | 138 | - Your system will boot into a minimal NixOS installer environment 139 | - The installer runs entirely in RAM, allowing you to reformat all disks 140 | - Your previous system is no longer accessible until you reboot 141 | 142 | ### Special Features 143 | 144 | The kexec installer includes several features to make remote installation easier: 145 | 146 | - **SSH Host Key Preservation**: Reuses SSH host keys from the existing system to prevent breaking `.ssh/known_hosts` on client machines 147 | - **SSH Key Authorization**: Automatically imports authorized keys from: 148 | - `/root/.ssh/authorized_keys` 149 | - `/root/.ssh/authorized_keys2` 150 | - `/etc/ssh/authorized_keys.d/root` 151 | - **Network Configuration Preservation**: Maintains static IP addresses and routes from your previous system 152 | - Interfaces with dynamic addresses are configured to use DHCP 153 | - IPv6 router advertisement is enabled for prefix delegation 154 | 155 | ### Automated Installation 156 | 157 | The kexec installer is designed to work seamlessly with [nixos-anywhere](https://github.com/numtide/nixos-anywhere) for fully automated NixOS installations. 158 | 159 | ## Netboot Images 160 | 161 | Netboot images allow you to boot NixOS over the network without requiring local installation media. 162 | 163 | ### What is Netboot? 164 | 165 | Network booting (netboot) enables computers to boot and load an operating system from the network rather than from local storage. This is useful for: 166 | 167 | - **Diskless workstations** that run entirely from network resources 168 | - **PXE boot environments** in data centers or computer labs 169 | - **Remote installations** where physical media is not available 170 | - **Testing and development** environments that need clean systems 171 | 172 | ### Components 173 | 174 | Our netboot package consists of three main components: 175 | 176 | 1. **iPXE Script**: A configuration file that tells the network boot client what to load 177 | 2. **Kernel Image**: The Linux kernel that will be booted 178 | 3. **Initial RAM Disk (initrd)**: Contains the essential files needed to boot NixOS 179 | 180 | ### Using Netboot Images 181 | 182 | #### Option 1: Direct iPXE Boot 183 | 184 | If you already have an iPXE environment set up, you can use our prepared iPXE script: 185 | 186 | ```bash 187 | # Boot directly using our iPXE script 188 | chain https://github.com/nix-community/nixos-images/releases/download/nixos-unstable/netboot-x86_64-linux.ipxe 189 | ``` 190 | 191 | #### Option 2: Manual Configuration 192 | 193 | If you're setting up your own PXE/TFTP server, you'll need: 194 | 195 | 1. **Kernel**: [bzImage-x86_64-linux](https://github.com/nix-community/nixos-images/releases/download/nixos-unstable/bzImage-x86_64-linux) 196 | 2. **Initial RAM Disk**: [initrd-x86_64-linux](https://github.com/nix-community/nixos-images/releases/download/nixos-unstable/initrd-x86_64-linux) 197 | 198 | Configure your DHCP server to point to your TFTP server, and configure the TFTP server to serve these files. 199 | 200 | ### Server Configuration Example 201 | 202 | Here's a basic example for setting up a TFTP/PXE server with dnsmasq: 203 | 204 | ```bash 205 | # Create a temporary environment with dnsmasq 206 | nix-shell -p dnsmasq 207 | 208 | # Create a configuration file 209 | cat > dnsmasq.conf << EOF 210 | interface=eth0 211 | dhcp-range=192.168.1.100,192.168.1.150,12h 212 | dhcp-boot=pxelinux.0 213 | enable-tftp 214 | tftp-root=/srv/tftp 215 | EOF 216 | 217 | # Create the TFTP directory 218 | mkdir -p /srv/tftp/nixos 219 | 220 | # Download the netboot files 221 | curl -o /srv/tftp/nixos/bzImage https://github.com/nix-community/nixos-images/releases/download/nixos-unstable/bzImage-x86_64-linux 222 | curl -o /srv/tftp/nixos/initrd https://github.com/nix-community/nixos-images/releases/download/nixos-unstable/initrd-x86_64-linux 223 | 224 | # Run dnsmasq 225 | dnsmasq --conf-file=dnsmasq.conf --no-daemon 226 | ``` 227 | 228 | ### Further Resources 229 | 230 | For more detailed information on network booting: 231 | 232 | - [NixOS Netboot Documentation](https://wiki.nixos.org/wiki/Netboot) 233 | - [iPXE Documentation](https://ipxe.org/start) 234 | -------------------------------------------------------------------------------- /build-images.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nix-shell 2 | #!nix-shell -p nix -p coreutils -p bash -p gh -i bash 3 | # shellcheck shell=bash 4 | set -xeuo pipefail 5 | shopt -s lastpipe 6 | 7 | build_netboot_image() { 8 | declare -r tag=$1 channel=$2 arch=$3 tmp=$4 9 | img=$(nix build --print-out-paths --option accept-flake-config true -L ".#packages.${arch}.netboot-nixos-${channel//./}") 10 | kernel=$(echo "$img"/*Image) 11 | kernelName=$(basename "$kernel") 12 | ln -s "$kernel" "$tmp/$kernelName-$arch" 13 | echo "$tmp/$kernelName-$arch" 14 | ln -s "$img/initrd" "$tmp/initrd-$arch" 15 | echo "$tmp/initrd-$arch" 16 | sed -e "s!^kernel $kernelName!kernel https://github.com/nix-community/nixos-images/releases/download/${tag}/$kernelName-${arch}!" \ 17 | -e "s!^initrd initrd!initrd https://github.com/nix-community/nixos-images/releases/download/${tag}/initrd-${arch}!" \ 18 | -e "s!initrd=initrd!initrd=initrd-${arch}!" \ 19 | <"$img/netboot.ipxe" \ 20 | >"$tmp/netboot-$arch.ipxe" 21 | echo "$tmp/netboot-$arch.ipxe" 22 | } 23 | 24 | build_kexec_installer() { 25 | declare -r channel=$1 arch=$2 tmp=$3 variant=$4 26 | out=$(nix build --print-out-paths --option accept-flake-config true -L ".#packages.${arch}.kexec-installer-nixos-${channel}${variant}") 27 | echo "$out/nixos-kexec-installer${variant}-$arch.tar.gz" 28 | } 29 | 30 | build_image_installer() { 31 | declare -r channel=$1 arch=$2 tmp=$3 32 | out=$(nix build --print-out-paths --option accept-flake-config true -L ".#packages.${arch}.image-installer-nixos-${channel//./}") 33 | echo "$out/iso/nixos-installer-${arch}.iso" 34 | } 35 | 36 | main() { 37 | declare -r tag=${1:-nixos-unstable} arch=${2:-x86_64-linux} 38 | tmp="$(mktemp -d)" 39 | trap 'rm -rf -- "$tmp"' EXIT 40 | ( 41 | channel=$(if [[ "$tag" == nixos-unstable ]]; then echo "unstable"; else echo "stable"; fi) 42 | build_kexec_installer "$channel" "$arch" "$tmp" "" 43 | build_kexec_installer "$channel" "$arch" "$tmp" "-noninteractive" 44 | build_netboot_image "$tag" "$channel" "$arch" "$tmp" 45 | build_image_installer "$channel" "$arch" "$tmp" 46 | ) | readarray -t assets 47 | for asset in "${assets[@]}"; do 48 | pushd "$(dirname "$asset")" 49 | popd 50 | done 51 | 52 | if ! gh release view "$tag"; then 53 | gh release create --title "$tag (build $(date +"%Y-%m-%d"))" "$tag" 54 | fi 55 | gh release upload --clobber "$tag" "${assets[@]}" 56 | } 57 | 58 | main "$@" 59 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixos-stable": { 4 | "locked": { 5 | "lastModified": 1749024892, 6 | "narHash": "sha256-OGcDEz60TXQC+gVz5sdtgGJdKVYr6rwdzQKuZAJQpCA=", 7 | "ref": "nixos-25.05", 8 | "rev": "8f1b52b04f2cb6e5ead50bd28d76528a2f0380ef", 9 | "shallow": true, 10 | "type": "git", 11 | "url": "https://github.com/NixOS/nixpkgs" 12 | }, 13 | "original": { 14 | "ref": "nixos-25.05", 15 | "shallow": true, 16 | "type": "git", 17 | "url": "https://github.com/NixOS/nixpkgs" 18 | } 19 | }, 20 | "nixos-unstable": { 21 | "locked": { 22 | "lastModified": 1748856973, 23 | "narHash": "sha256-RlTsJUvvr8ErjPBsiwrGbbHYW8XbB/oek0Gi78XdWKg=", 24 | "ref": "nixpkgs-unstable", 25 | "rev": "e4b09e47ace7d87de083786b404bf232eb6c89d8", 26 | "shallow": true, 27 | "type": "git", 28 | "url": "https://github.com/NixOS/nixpkgs" 29 | }, 30 | "original": { 31 | "ref": "nixpkgs-unstable", 32 | "shallow": true, 33 | "type": "git", 34 | "url": "https://github.com/NixOS/nixpkgs" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "nixos-stable": "nixos-stable", 40 | "nixos-unstable": "nixos-unstable" 41 | } 42 | } 43 | }, 44 | "root": "root", 45 | "version": 7 46 | } 47 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "NixOS images"; 3 | 4 | inputs.nixos-unstable.url = "git+https://github.com/NixOS/nixpkgs?shallow=1&ref=nixpkgs-unstable"; 5 | inputs.nixos-stable.url = "git+https://github.com/NixOS/nixpkgs?shallow=1&ref=nixos-25.05"; 6 | 7 | nixConfig.extra-substituters = [ "https://nix-community.cachix.org" ]; 8 | nixConfig.extra-trusted-public-keys = [ "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=" ]; 9 | 10 | outputs = { self, nixos-unstable, nixos-stable }: 11 | let 12 | supportedSystems = [ "aarch64-linux" "x86_64-linux" ]; 13 | forAllSystems = nixos-unstable.lib.genAttrs supportedSystems; 14 | in 15 | { 16 | packages = forAllSystems (system: 17 | let 18 | netboot = nixpkgs: (import (nixpkgs + "/nixos/release.nix") { }).netboot.${system}; 19 | kexec-installer = nixpkgs: module: (nixpkgs.legacyPackages.${system}.nixos [ module self.nixosModules.kexec-installer ]).config.system.build.kexecInstallerTarball; 20 | netboot-installer = nixpkgs: (nixpkgs.legacyPackages.${system}.nixos [ self.nixosModules.netboot-installer ]).config.system.build.netboot; 21 | image-installer = nixpkgs: (nixpkgs.legacyPackages.${system}.nixos [ self.nixosModules.image-installer ]).config.system.build.isoImage; 22 | in 23 | { 24 | netboot-nixos-unstable = netboot nixos-unstable; 25 | netboot-nixos-stable = netboot nixos-stable; 26 | kexec-installer-nixos-unstable = kexec-installer nixos-unstable {}; 27 | kexec-installer-nixos-stable = kexec-installer nixos-stable {}; 28 | 29 | image-installer-nixos-unstable = image-installer nixos-unstable; 30 | image-installer-nixos-stable = image-installer nixos-stable; 31 | 32 | kexec-installer-nixos-unstable-noninteractive = kexec-installer nixos-unstable { 33 | _file = __curPos.file; 34 | system.kexec-installer.name = "nixos-kexec-installer-noninteractive"; 35 | imports = [ 36 | self.nixosModules.noninteractive 37 | ]; 38 | }; 39 | kexec-installer-nixos-stable-noninteractive = kexec-installer nixos-stable { 40 | _file = __curPos.file; 41 | system.kexec-installer.name = "nixos-kexec-installer-noninteractive"; 42 | imports = [ 43 | self.nixosModules.noninteractive 44 | ]; 45 | }; 46 | 47 | netboot-installer-nixos-unstable = netboot-installer nixos-unstable; 48 | netboot-installer-nixos-stable = netboot-installer nixos-stable; 49 | }); 50 | nixosModules = { 51 | kexec-installer = ./nix/kexec-installer/module.nix; 52 | noninteractive = ./nix/noninteractive.nix; 53 | # TODO: also add a test here once we have https://github.com/NixOS/nixpkgs/pull/228346 merged 54 | netboot-installer = ./nix/netboot-installer/module.nix; 55 | image-installer = ./nix/image-installer/module.nix; 56 | }; 57 | checks = 58 | let 59 | # re-export the packages as checks 60 | packages = forAllSystems (system: nixos-unstable.lib.mapAttrs' (n: nixos-unstable.lib.nameValuePair "package-${n}") self.packages.${system}); 61 | checks = 62 | let 63 | pkgsUnstable = nixos-unstable.legacyPackages.x86_64-linux; 64 | pkgsStable = nixos-stable.legacyPackages.x86_64-linux; 65 | 66 | bootTests = pkgs: channel: suffix: pkgs.lib.mapAttrs' (name: pkgs.lib.nameValuePair "${name}${suffix}") (pkgs.callPackages ./nix/image-installer/tests.nix { 67 | nixpkgs = channel; 68 | nixosModules = self.nixosModules; 69 | }); 70 | in 71 | { 72 | kexec-installer-unstable = pkgsUnstable.callPackage ./nix/kexec-installer/test.nix { 73 | kexecInstallerTarball = self.packages.x86_64-linux.kexec-installer-nixos-unstable-noninteractive; 74 | }; 75 | 76 | kexec-installer-stable = nixos-stable.legacyPackages.x86_64-linux.callPackage ./nix/kexec-installer/test.nix { 77 | kexecInstallerTarball = self.packages.x86_64-linux.kexec-installer-nixos-stable-noninteractive; 78 | }; 79 | shellcheck = pkgsUnstable.runCommand "shellcheck" 80 | { 81 | nativeBuildInputs = [ pkgsUnstable.shellcheck ]; 82 | } '' 83 | shellcheck ${(pkgsUnstable.nixos [self.nixosModules.kexec-installer]).config.system.build.kexecRun} 84 | touch $out 85 | ''; 86 | } // (bootTests pkgsUnstable nixos-unstable "-nixos-unstable") 87 | // (bootTests pkgsStable nixos-stable "-nixos-stable"); 88 | in 89 | nixos-unstable.lib.recursiveUpdate packages { x86_64-linux = checks; }; 90 | }; 91 | } 92 | -------------------------------------------------------------------------------- /nix/image-installer/hidden-ssh-announcement.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | lib, 4 | pkgs, 5 | ... 6 | }: 7 | { 8 | options.hidden-ssh-announce = { 9 | enable = lib.mkEnableOption "hidden-ssh-announce"; 10 | script = lib.mkOption { 11 | type = lib.types.package; 12 | default = pkgs.writers.writeDash "test-output" "echo $1"; 13 | description = '' 14 | script to run when the hidden tor service was started and they hostname is known. 15 | takes the hostname as $1 16 | ''; 17 | }; 18 | }; 19 | 20 | config = lib.mkIf config.hidden-ssh-announce.enable { 21 | services.openssh.enable = true; 22 | services.tor = { 23 | enable = true; 24 | relay.onionServices.hidden-ssh = { 25 | version = 3; 26 | map = [ 27 | { 28 | port = 22; 29 | target.port = 22; 30 | } 31 | ]; 32 | }; 33 | client.enable = true; 34 | }; 35 | systemd.services.hidden-ssh-announce = { 36 | description = "announce hidden ssh"; 37 | after = [ 38 | "tor.service" 39 | "network-online.target" 40 | ]; 41 | wants = [ 42 | "tor.service" 43 | "network-online.target" 44 | ]; 45 | wantedBy = [ "multi-user.target" ]; 46 | serviceConfig = { 47 | # ${pkgs.tor}/bin/torify 48 | ExecStart = pkgs.writeShellScript "announce-hidden-service" '' 49 | set -efu 50 | until test -e ${config.services.tor.settings.DataDirectory}/onion/hidden-ssh/hostname; do 51 | echo "still waiting for ${config.services.tor.settings.DataDirectory}/onion/hidden-ssh/hostname" 52 | sleep 1 53 | done 54 | 55 | ${config.hidden-ssh-announce.script} "$(cat ${config.services.tor.settings.DataDirectory}/onion/hidden-ssh/hostname)" 56 | ''; 57 | PrivateTmp = "true"; 58 | User = "tor"; 59 | Type = "oneshot"; 60 | }; 61 | }; 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /nix/image-installer/module.nix: -------------------------------------------------------------------------------- 1 | { 2 | lib, 3 | pkgs, 4 | modulesPath, 5 | ... 6 | }: 7 | let 8 | network-status = pkgs.writeShellScriptBin "network-status" '' 9 | export PATH=${ 10 | lib.makeBinPath ( 11 | with pkgs; 12 | [ 13 | iproute2 14 | coreutils 15 | gnugrep 16 | nettools 17 | gum 18 | ] 19 | ) 20 | } 21 | set -efu -o pipefail 22 | msgs=() 23 | if [[ -e /var/shared/qrcode.utf8 ]]; then 24 | qrcode=$(gum style --border-foreground 240 --border normal "$(< /var/shared/qrcode.utf8)") 25 | msgs+=("$qrcode") 26 | fi 27 | network_status="Root password: $(cat /var/shared/root-password) 28 | Local network addresses: 29 | $(ip -brief -color addr | grep -v 127.0.0.1) 30 | $([[ -e /var/shared/onion-hostname ]] && echo "Onion address: $(cat /var/shared/onion-hostname)" || echo "Onion address: Waiting for tor network to be ready...") 31 | Multicast DNS: $(hostname).local" 32 | network_status=$(gum style --border-foreground 240 --border normal "$network_status") 33 | msgs+=("$network_status") 34 | msgs+=("Press 'Ctrl-C' for console access") 35 | 36 | gum join --vertical "''${msgs[@]}" 37 | ''; 38 | in 39 | { 40 | imports = [ 41 | (modulesPath + "/installer/cd-dvd/installation-cd-base.nix") 42 | ../installer.nix 43 | ../noveau-workaround.nix 44 | ./hidden-ssh-announcement.nix 45 | ./wifi.nix 46 | ]; 47 | systemd.tmpfiles.rules = [ "d /var/shared 0777 root root - -" ]; 48 | services.openssh.settings.PermitRootLogin = "yes"; 49 | system.activationScripts.root-password = '' 50 | mkdir -p /var/shared 51 | ${pkgs.xkcdpass}/bin/xkcdpass --numwords 3 --delimiter - --count 1 > /var/shared/root-password 52 | echo "root:$(cat /var/shared/root-password)" | chpasswd 53 | ''; 54 | hidden-ssh-announce = { 55 | enable = true; 56 | script = pkgs.writeShellScript "write-hostname" '' 57 | set -efu 58 | export PATH=${ 59 | lib.makeBinPath ( 60 | with pkgs; 61 | [ 62 | iproute2 63 | coreutils 64 | jq 65 | qrencode 66 | ] 67 | ) 68 | } 69 | 70 | mkdir -p /var/shared 71 | echo "$1" > /var/shared/onion-hostname 72 | local_addrs=$(ip -json addr | jq '[map(.addr_info) | flatten | .[] | select(.scope == "global") | .local]') 73 | jq -nc \ 74 | --arg password "$(cat /var/shared/root-password)" \ 75 | --arg onion_address "$(cat /var/shared/onion-hostname)" \ 76 | --argjson local_addrs "$local_addrs" \ 77 | '{ pass: $password, tor: $onion_address, addrs: $local_addrs }' \ 78 | > /var/shared/login.json 79 | cat /var/shared/login.json | qrencode -s 2 -m 2 -t utf8 -o /var/shared/qrcode.utf8 80 | ''; 81 | }; 82 | 83 | services.getty.autologinUser = lib.mkForce "root"; 84 | 85 | console.earlySetup = true; 86 | console.font = lib.mkDefault "${pkgs.terminus_font}/share/consolefonts/ter-u22n.psf.gz"; 87 | 88 | environment.systemPackages = [ network-status ]; 89 | 90 | # Less ipv6 addresses to reduce the noise 91 | networking.tempAddresses = "disabled"; 92 | 93 | # Tango theme: https://yayachiken.net/en/posts/tango-colors-in-terminal/ 94 | console.colors = lib.mkDefault [ 95 | "000000" 96 | "CC0000" 97 | "4E9A06" 98 | "C4A000" 99 | "3465A4" 100 | "75507B" 101 | "06989A" 102 | "D3D7CF" 103 | "555753" 104 | "EF2929" 105 | "8AE234" 106 | "FCE94F" 107 | "739FCF" 108 | "AD7FA8" 109 | "34E2E2" 110 | "EEEEEC" 111 | ]; 112 | 113 | programs.bash.interactiveShellInit = '' 114 | if [[ "$(tty)" =~ /dev/(tty1|hvc0|ttyS0)$ ]]; then 115 | # workaround for https://github.com/NixOS/nixpkgs/issues/219239 116 | systemctl restart systemd-vconsole-setup.service 117 | 118 | watch --no-title --color ${network-status}/bin/network-status 119 | fi 120 | ''; 121 | 122 | # No one got time for xz compression. 123 | isoImage.squashfsCompression = "zstd"; 124 | } // (if lib.versionAtLeast lib.version "25.03pre" then { 125 | image.baseName = lib.mkForce "nixos-installer-${pkgs.system}"; 126 | } else { 127 | isoImage.isoName = lib.mkForce "nixos-installer-${pkgs.system}.iso"; 128 | }) 129 | -------------------------------------------------------------------------------- /nix/image-installer/tests.nix: -------------------------------------------------------------------------------- 1 | { 2 | pkgs, 3 | lib, 4 | nixpkgs, 5 | nixos, 6 | nixosModules, 7 | }: 8 | 9 | let 10 | testConfig = ( 11 | nixos [ 12 | ( 13 | { modulesPath, ... }: 14 | { 15 | imports = [ 16 | nixosModules.image-installer 17 | "${modulesPath}/testing/test-instrumentation.nix" 18 | ]; 19 | } 20 | ) 21 | ] 22 | ); 23 | iso = testConfig.config.system.build.isoImage; 24 | mkStartCommand = 25 | { 26 | memory ? 2048, 27 | cdrom ? null, 28 | usb ? null, 29 | uefi ? false, 30 | extraFlags ? [ ], 31 | }: 32 | let 33 | qemu-common = import (nixpkgs + "/nixos/lib/qemu-common.nix") { inherit lib pkgs; }; 34 | qemu = qemu-common.qemuBinary pkgs.qemu_test; 35 | 36 | flags = 37 | [ 38 | "-m" 39 | (toString memory) 40 | "-netdev" 41 | "user,id=net0" 42 | "-device" 43 | "virtio-net-pci,netdev=net0" 44 | ] 45 | ++ lib.optionals (cdrom != null) [ 46 | "-cdrom" 47 | cdrom 48 | ] 49 | ++ lib.optionals (usb != null) [ 50 | "-device" 51 | "usb-ehci" 52 | "-drive" 53 | "id=usbdisk,file=${usb},if=none,readonly" 54 | "-device" 55 | "usb-storage,drive=usbdisk" 56 | ] 57 | ++ lib.optionals uefi [ 58 | "-drive" 59 | "if=pflash,format=raw,unit=0,readonly=on,file=${pkgs.OVMF.firmware}" 60 | "-drive" 61 | "if=pflash,format=raw,unit=1,readonly=on,file=${pkgs.OVMF.variables}" 62 | ] 63 | ++ extraFlags; 64 | 65 | flagsStr = lib.concatStringsSep " " flags; 66 | in 67 | "${qemu} ${flagsStr}"; 68 | 69 | makeBootTest = 70 | name: config: 71 | let 72 | startCommand = mkStartCommand config; 73 | in 74 | pkgs.testers.runNixOSTest { 75 | name = "boot-${name}"; 76 | nodes = { }; 77 | testScript = '' 78 | machine = create_machine("${startCommand}") 79 | machine.start() 80 | machine.wait_for_unit("multi-user.target") 81 | machine.succeed("nix store verify --no-trust -r --option experimental-features nix-command /run/current-system") 82 | 83 | machine.shutdown() 84 | ''; 85 | }; 86 | in 87 | { 88 | uefi-cdrom = makeBootTest "uefi-cdrom" { 89 | uefi = true; 90 | cdrom = "${iso}/iso/nixos-installer-${pkgs.hostPlatform.system}.iso"; 91 | }; 92 | 93 | uefi-usb = makeBootTest "uefi-usb" { 94 | uefi = true; 95 | usb = "${iso}/iso/nixos-installer-${pkgs.hostPlatform.system}.iso"; 96 | }; 97 | 98 | bios-cdrom = makeBootTest "bios-cdrom" { 99 | cdrom = "${iso}/iso/nixos-installer-${pkgs.hostPlatform.system}.iso"; 100 | }; 101 | 102 | bios-usb = makeBootTest "bios-usb" { 103 | usb = "${iso}/iso/nixos-installer-${pkgs.hostPlatform.system}.iso"; 104 | }; 105 | } 106 | -------------------------------------------------------------------------------- /nix/image-installer/wifi.nix: -------------------------------------------------------------------------------- 1 | { 2 | imports = [ ../networkd.nix ]; 3 | # use iwd instead of wpa_supplicant 4 | networking.wireless.enable = false; 5 | 6 | # Use iwd instead of wpa_supplicant. It has a user friendly CLI 7 | networking.wireless.iwd = { 8 | enable = true; 9 | settings = { 10 | Network = { 11 | EnableIPv6 = true; 12 | RoutePriorityOffset = 300; 13 | }; 14 | Settings.AutoConnect = true; 15 | }; 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /nix/installer.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | lib, 4 | pkgs, 5 | ... 6 | }: 7 | { 8 | imports = [ 9 | ./latest-zfs-kernel.nix 10 | ./nix-settings.nix 11 | ]; 12 | # more descriptive hostname than just "nixos" 13 | networking.hostName = lib.mkDefault "nixos-installer"; 14 | 15 | # We are stateless, so just default to latest. 16 | system.stateVersion = config.system.nixos.release; 17 | 18 | # Enable bcachefs support 19 | boot.supportedFilesystems.bcachefs = lib.mkDefault true; 20 | 21 | # use latest kernel we can support to get more hardware support 22 | boot.zfs.package = pkgs.zfsUnstable; 23 | 24 | documentation.enable = false; 25 | documentation.man.man-db.enable = false; 26 | 27 | # make it easier to debug boot failures 28 | boot.initrd.systemd.emergencyAccess = true; 29 | 30 | environment.systemPackages = [ 31 | pkgs.nixos-install-tools 32 | # for zapping of disko 33 | pkgs.jq 34 | # for copying extra files of nixos-anywhere 35 | pkgs.rsync 36 | # alternative to nixos-generate-config 37 | # TODO: use nixpkgs again after next nixos release 38 | pkgs.nixos-facter 39 | 40 | pkgs.disko 41 | ]; 42 | 43 | # enable zswap to help with low memory systems 44 | boot.kernelParams = [ 45 | "zswap.enabled=1" 46 | "zswap.max_pool_percent=50" 47 | "zswap.compressor=zstd" 48 | # recommended for systems with little memory 49 | "zswap.zpool=zsmalloc" 50 | ]; 51 | 52 | # Don't add nixpkgs to the image to save space, for our intended use case we don't need it 53 | system.installer.channel.enable = false; 54 | } 55 | -------------------------------------------------------------------------------- /nix/kexec-installer/kexec-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Set pipefail if the shell supports it. 4 | if set -o | grep -q pipefail; then 5 | # shellcheck disable=SC3040 6 | set -o pipefail 7 | fi 8 | set -eux 9 | 10 | 11 | kexec_extra_flags="" 12 | 13 | while [ $# -gt 0 ]; do 14 | case "$1" in 15 | --kexec-extra-flags) 16 | kexec_extra_flags="$2" 17 | shift 18 | ;; 19 | esac 20 | shift 21 | done 22 | 23 | # provided by nix 24 | init="@init@" 25 | kernelParams="@kernelParams@" 26 | 27 | SCRIPT_DIR=$(dirname "$(readlink -f "$0")") 28 | INITRD_TMP=$(TMPDIR=$SCRIPT_DIR mktemp -d) 29 | 30 | cd "$INITRD_TMP" 31 | cleanup() { 32 | rm -rf "$INITRD_TMP" 33 | } 34 | trap cleanup EXIT 35 | mkdir -p ssh 36 | 37 | extractPubKeys() { 38 | home="$1" 39 | for file in .ssh/authorized_keys .ssh/authorized_keys2; do 40 | key="$home/$file" 41 | if test -e "$key"; then 42 | # workaround for debian shenanigans 43 | grep -o '\(\(ssh\|ecdsa\|sk\)-[^ ]* .*\)' "$key" >> ssh/authorized_keys || true 44 | fi 45 | done 46 | } 47 | extractPubKeys /root 48 | 49 | if test -n "${DOAS_USER-}"; then 50 | SUDO_USER="$DOAS_USER" 51 | fi 52 | 53 | if test -n "${SUDO_USER-}"; then 54 | sudo_home=$(sh -c "echo ~$SUDO_USER") 55 | extractPubKeys "$sudo_home" 56 | fi 57 | 58 | # Typically for NixOS 59 | if test -e /etc/ssh/authorized_keys.d/root; then 60 | cat /etc/ssh/authorized_keys.d/root >> ssh/authorized_keys 61 | fi 62 | if test -n "${SUDO_USER-}" && test -e "/etc/ssh/authorized_keys.d/$SUDO_USER"; then 63 | cat "/etc/ssh/authorized_keys.d/$SUDO_USER" >> ssh/authorized_keys 64 | fi 65 | for p in /etc/ssh/ssh_host_*; do 66 | test -e "$p" || continue 67 | cp -a "$p" ssh 68 | done 69 | 70 | # save the networking config for later use 71 | "$SCRIPT_DIR/ip" --json addr > addrs.json 72 | 73 | "$SCRIPT_DIR/ip" -4 --json route > routes-v4.json 74 | "$SCRIPT_DIR/ip" -6 --json route > routes-v6.json 75 | 76 | [ -f /etc/machine-id ] && cp /etc/machine-id machine-id 77 | 78 | find . | cpio -o -H newc | gzip -9 >> "$SCRIPT_DIR/initrd" 79 | 80 | kexecSyscallFlags="" 81 | # only do kexec-syscall-auto on kernels newer than 6.0. 82 | # On older kernel we often get errors like: https://github.com/nix-community/nixos-anywhere/issues/264 83 | if printf "%s\n" "6.1" "$(uname -r)" | sort -c -V 2>&1; then 84 | kexecSyscallFlags="--kexec-syscall-auto" 85 | fi 86 | 87 | if ! sh -c "'$SCRIPT_DIR/kexec' --load '$SCRIPT_DIR/bzImage' \ 88 | $kexecSyscallFlags \ 89 | $kexec_extra_flags \ 90 | --initrd='$SCRIPT_DIR/initrd' --no-checks \ 91 | --command-line 'init=$init $kernelParams'" 92 | then 93 | echo "kexec failed, dumping dmesg" 94 | dmesg | tail -n 100 95 | exit 1 96 | fi 97 | 98 | # Disconnect our background kexec from the terminal 99 | echo "machine will boot into nixos in 6s..." 100 | if test -e /dev/kmsg; then 101 | # this makes logging visible in `dmesg`, or the system console or tools like journald 102 | exec > /dev/kmsg 2>&1 103 | else 104 | exec > /dev/null 2>&1 105 | fi 106 | # We will kexec in background so we can cleanly finish the script before the hosts go down. 107 | # This makes integration with tools like terraform easier. 108 | nohup sh -c "sleep 6 && '$SCRIPT_DIR/kexec' -e ${kexec_extra_flags}" & 109 | -------------------------------------------------------------------------------- /nix/kexec-installer/local-network-restore-test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S nix shell --inputs-from .# nixos-unstable#bash nixos-unstable#iproute2 nixos-unstable#findutils nixos-unstable#coreutils nixos-unstable#python3 nixos-unstable#jq --command bash 2 | 3 | set -eu 4 | SCRIPT_DIR=$(dirname "$(readlink -f "$0")") 5 | 6 | # This script can be used to see what network configuration would be restored by the restore_routes.py script for the current system. 7 | 8 | tmp=$(mktemp -d) 9 | trap "rm -rf $tmp" EXIT 10 | ip --json address >"$tmp/addrs.json" 11 | ip -6 --json route >"$tmp/routes-v6.json" 12 | ip -4 --json route >"$tmp/routes-v4.json" 13 | python3 "$SCRIPT_DIR/restore_routes.py" "$tmp/addrs.json" "$tmp/routes-v4.json" "$tmp/routes-v6.json" "$tmp" 14 | ls -la "$tmp" 15 | 16 | find "$tmp" -type f -name "*.json" -print0 | while IFS= read -r -d '' file; do 17 | echo -e "\033[0;31m$(basename "$file")\033[0m" 18 | jq . "$file" 19 | echo "" 20 | done 21 | 22 | find "$tmp" -type f -name "*.network" -print0 | while IFS= read -r -d '' file; do 23 | echo -e "\033[0;31m$(basename "$file")\033[0m" 24 | cat "$file" 25 | echo "" 26 | done 27 | -------------------------------------------------------------------------------- /nix/kexec-installer/module.nix: -------------------------------------------------------------------------------- 1 | { config, lib, modulesPath, pkgs, ... }: 2 | let 3 | writePython3 = pkgs.writers.makePythonWriter 4 | pkgs.python3Minimal pkgs.python3Packages pkgs.buildPackages.python3Packages; 5 | 6 | # writePython3Bin takes the same arguments as writePython3 but outputs a directory (like writeScriptBin) 7 | writePython3Bin = name: writePython3 "/bin/${name}"; 8 | 9 | restore-network = writePython3Bin "restore-network" { 10 | flakeIgnore = [ "E501" ]; 11 | } ./restore_routes.py; 12 | 13 | # does not link with iptables enabled 14 | iprouteStatic = pkgs.pkgsStatic.iproute2.override { iptables = null; }; 15 | in 16 | { 17 | imports = [ 18 | (modulesPath + "/installer/netboot/netboot-minimal.nix") 19 | ../installer.nix 20 | ../networkd.nix 21 | ../serial.nix 22 | ../restore-remote-access.nix 23 | ]; 24 | 25 | options = { 26 | system.kexec-installer.name = lib.mkOption { 27 | type = lib.types.str; 28 | default = "nixos-kexec-installer"; 29 | description = '' 30 | The variant of the kexec installer to use. 31 | ''; 32 | }; 33 | }; 34 | 35 | config = { 36 | boot.initrd.compressor = "xz"; 37 | # This is a variant of the upstream kexecScript that also allows embedding 38 | # a ssh key. 39 | system.build.kexecRun = pkgs.runCommand "kexec-run" { } '' 40 | install -D -m 0755 ${./kexec-run.sh} $out 41 | 42 | sed -i \ 43 | -e 's|@init@|${config.system.build.toplevel}/init|' \ 44 | -e 's|@kernelParams@|${lib.escapeShellArgs config.boot.kernelParams}|' \ 45 | $out 46 | 47 | ${pkgs.shellcheck}/bin/shellcheck $out 48 | ''; 49 | 50 | system.build.kexecInstallerTarball = pkgs.runCommand "kexec-tarball" { } '' 51 | mkdir kexec $out 52 | cp "${config.system.build.netbootRamdisk}/initrd" kexec/initrd 53 | cp "${config.system.build.kernel}/${config.system.boot.loader.kernelFile}" kexec/bzImage 54 | cp "${config.system.build.kexecRun}" kexec/run 55 | cp "${pkgs.pkgsStatic.kexec-tools}/bin/kexec" kexec/kexec 56 | cp "${iprouteStatic}/bin/ip" kexec/ip 57 | ${lib.optionalString (pkgs.hostPlatform == pkgs.buildPlatform) '' 58 | kexec/ip -V 59 | kexec/kexec --version 60 | ''} 61 | tar -czvf $out/${config.system.kexec-installer.name}-${pkgs.stdenv.hostPlatform.system}.tar.gz kexec 62 | ''; 63 | 64 | systemd.services.restore-network = { 65 | before = [ "network-pre.target" ]; 66 | wants = [ "network-pre.target" ]; 67 | wantedBy = [ "multi-user.target" ]; 68 | 69 | serviceConfig = { 70 | Type = "oneshot"; 71 | RemainAfterExit = true; 72 | ExecStart = [ 73 | "${restore-network}/bin/restore-network /root/network/addrs.json /root/network/routes-v4.json /root/network/routes-v6.json /etc/systemd/network" 74 | ]; 75 | }; 76 | 77 | unitConfig.ConditionPathExists = [ 78 | "/root/network/addrs.json" 79 | "/root/network/routes-v4.json" 80 | "/root/network/routes-v6.json" 81 | ]; 82 | }; 83 | }; 84 | } 85 | -------------------------------------------------------------------------------- /nix/kexec-installer/restore_routes.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | from pathlib import Path 4 | from typing import Any, Iterator 5 | from dataclasses import dataclass 6 | 7 | 8 | @dataclass 9 | class Address: 10 | address: str 11 | family: str 12 | prefixlen: int 13 | preferred_life_time: int = 0 14 | valid_life_time: int = 0 15 | 16 | 17 | @dataclass 18 | class Interface: 19 | name: str 20 | ifname: str | None 21 | mac_address: str 22 | dynamic_addresses: list[Address] 23 | static_addresses: list[Address] 24 | static_routes: list[dict[str, Any]] 25 | 26 | 27 | def filter_interfaces(network: list[dict[str, Any]]) -> list[Interface]: 28 | interfaces = [] 29 | for net in network: 30 | if net.get("link_type") == "loopback": 31 | continue 32 | if not (mac_address := net.get("address")): 33 | # We need a mac address to match devices reliable 34 | continue 35 | static_addresses = [] 36 | dynamic_addresses = [] 37 | for info in net.get("addr_info", []): 38 | # no link-local ipv4/ipv6 39 | if info.get("scope") == "link": 40 | continue 41 | if (preferred_life_time := info.get("preferred_life_time")) is None: 42 | continue 43 | if (valid_life_time := info.get("valid_life_time")) is None: 44 | continue 45 | if (prefixlen := info.get("prefixlen")) is None: 46 | continue 47 | if (family := info.get("family")) not in ["inet", "inet6"]: 48 | continue 49 | if (local := info.get("local")) is None: 50 | continue 51 | if (dynamic := info.get("dynamic", False)) is None: 52 | continue 53 | 54 | address = Address( 55 | address=local, 56 | family=family, 57 | prefixlen=prefixlen, 58 | preferred_life_time=preferred_life_time, 59 | valid_life_time=valid_life_time, 60 | ) 61 | 62 | if dynamic: 63 | dynamic_addresses.append(address) 64 | else: 65 | static_addresses.append(address) 66 | interfaces.append( 67 | Interface( 68 | name=net.get("ifname", mac_address.replace(":", "-")), 69 | ifname=net.get("ifname"), 70 | mac_address=mac_address, 71 | dynamic_addresses=dynamic_addresses, 72 | static_addresses=static_addresses, 73 | static_routes=[], 74 | ) 75 | ) 76 | 77 | return interfaces 78 | 79 | 80 | def filter_routes(routes: list[dict[str, Any]]) -> list[dict[str, Any]]: 81 | filtered = [] 82 | for route in routes: 83 | # Filter out routes set by addresses with subnets, dhcp and router advertisement 84 | if route.get("protocol") in ["dhcp", "kernel", "ra"]: 85 | continue 86 | filtered.append(route) 87 | 88 | return filtered 89 | 90 | 91 | def find_most_recent_v4_lease(addresses: list[Address]) -> Address | None: 92 | most_recent_address = None 93 | most_recent_lifetime = -1 94 | for addr in addresses: 95 | if addr.family == "inet6": 96 | continue 97 | lifetime = max(addr.preferred_life_time, addr.valid_life_time) 98 | if lifetime > most_recent_lifetime: 99 | most_recent_lifetime = lifetime 100 | most_recent_address = addr 101 | return most_recent_address 102 | 103 | 104 | def generate_routes( 105 | interface: Interface, routes: list[dict[str, Any]] 106 | ) -> Iterator[str]: 107 | for route in routes: 108 | if interface.ifname is None or route.get("dev") != interface.ifname: 109 | continue 110 | 111 | yield "[Route]" 112 | if route.get("dst") != "default": 113 | # can be skipped for default routes 114 | yield f"Destination = {route['dst']}" 115 | gateway = route.get("gateway") 116 | # route v4 via v6 117 | route_via = route.get("via") 118 | if route_via and route_via.get("family") == "inet6": 119 | gateway = route_via.get("host") 120 | if route.get("dst") == "default": 121 | yield "Destination = 0.0.0.0/0" 122 | if gateway: 123 | yield f"Gateway = {gateway}" 124 | flags = route.get("flags", []) 125 | if "onlink" in flags: 126 | yield "GatewayOnLink = yes" 127 | 128 | 129 | def generate_networkd_units( 130 | interfaces: list[Interface], routes: list[dict[str, Any]], directory: Path 131 | ) -> None: 132 | directory.mkdir(exist_ok=True) 133 | for interface in interfaces: 134 | # FIXME in some networks we might not want to trust dhcp or router advertisements 135 | unit_sections = [ 136 | f""" 137 | [Match] 138 | MACAddress = {interface.mac_address} 139 | 140 | [Network] 141 | # both ipv4 and ipv6 142 | DHCP = yes 143 | # lets us discover the switch port we're connected to 144 | LLDP = yes 145 | # ipv6 router advertisements 146 | IPv6AcceptRA = yes 147 | # allows us to ping "nixos.local" 148 | MulticastDNS = yes""" 149 | ] 150 | unit_sections.extend( 151 | f"Address = {addr.address}/{addr.prefixlen}" 152 | for addr in interface.static_addresses 153 | ) 154 | unit_sections.extend(generate_routes(interface, routes)) 155 | most_recent_v4_lease = find_most_recent_v4_lease(interface.dynamic_addresses) 156 | if most_recent_v4_lease: 157 | unit_sections.append("[DHCPv4]") 158 | unit_sections.append(f"RequestAddress = {most_recent_v4_lease.address}") 159 | 160 | # trailing newline at the end 161 | unit_sections.append("") 162 | 163 | (directory / f"00-{interface.name}.network").write_text( 164 | "\n".join(unit_sections) 165 | ) 166 | 167 | 168 | def main() -> None: 169 | if len(sys.argv) < 5: 170 | print( 171 | f"USAGE: {sys.argv[0]} addresses routes-v4 routes-v6 networkd-directory", 172 | file=sys.stderr, 173 | ) 174 | sys.exit(1) 175 | 176 | with open(sys.argv[1]) as f: 177 | addresses = json.load(f) 178 | with open(sys.argv[2]) as f: 179 | v4_routes = json.load(f) 180 | with open(sys.argv[3]) as f: 181 | v6_routes = json.load(f) 182 | 183 | networkd_directory = Path(sys.argv[4]) 184 | 185 | relevant_interfaces = filter_interfaces(addresses) 186 | relevant_routes = filter_routes(v4_routes) + filter_routes(v6_routes) 187 | 188 | generate_networkd_units(relevant_interfaces, relevant_routes, networkd_directory) 189 | 190 | 191 | if __name__ == "__main__": 192 | main() 193 | -------------------------------------------------------------------------------- /nix/kexec-installer/ssh-keys/id_ed25519: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 3 | QyNTUxOQAAACA8wk9uIqPk7FZFhRs0ZQ4Q/b0Rd//Rpq2i9e3v33+WwgAAAJjeXdO33l3T 4 | twAAAAtzc2gtZWQyNTUxOQAAACA8wk9uIqPk7FZFhRs0ZQ4Q/b0Rd//Rpq2i9e3v33+Wwg 5 | AAAEBiNUp5mUe87gWrXbjd36dqt/6waDLdoYV1woR8in4ehDzCT24io+TsVkWFGzRlDhD9 6 | vRF3/9GmraL17e/ff5bCAAAAE2pvZXJnQHR1cmluZ21hY2hpbmUBAg== 7 | -----END OPENSSH PRIVATE KEY----- 8 | -------------------------------------------------------------------------------- /nix/kexec-installer/ssh-keys/id_ed25519.pub: -------------------------------------------------------------------------------- 1 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDzCT24io+TsVkWFGzRlDhD9vRF3/9GmraL17e/ff5bC joerg@turingmachine 2 | -------------------------------------------------------------------------------- /nix/kexec-installer/test.nix: -------------------------------------------------------------------------------- 1 | { pkgs 2 | , lib 3 | , kexecInstallerTarball 4 | }: 5 | 6 | pkgs.testers.runNixOSTest { 7 | name = "kexec-installer"; 8 | meta = with pkgs.lib.maintainers; { 9 | maintainers = [ mic92 ]; 10 | }; 11 | 12 | nodes = { 13 | node1 = { modulesPath, pkgs, ... }: { 14 | virtualisation.vlans = [ ]; 15 | imports = [ 16 | (modulesPath + "/profiles/minimal.nix") 17 | ]; 18 | 19 | system.extraDependencies = [ kexecInstallerTarball ]; 20 | virtualisation.memorySize = 1 * 1024; 21 | virtualisation.diskSize = 4 * 1024; 22 | virtualisation.forwardPorts = [{ 23 | host.port = 2222; 24 | guest.port = 22; 25 | }]; 26 | 27 | services.openssh.enable = true; 28 | 29 | networking.useNetworkd = true; 30 | networking.useDHCP = false; 31 | 32 | users.users.root.openssh.authorizedKeys.keyFiles = [ ./ssh-keys/id_ed25519.pub ]; 33 | 34 | systemd.network = { 35 | networks = { 36 | # systemd-networkd will load the first network unit file 37 | # that matches, ordered lexiographically by filename. 38 | # /etc/systemd/network/{40-eth1,99-main}.network already 39 | # exists. This network unit must be loaded for the test, 40 | # however, hence why this network is named such. 41 | 42 | "01-eth0" = { 43 | name = "eth0"; 44 | address = [ 45 | # Some static addresses that we want to see in the kexeced image 46 | "192.168.42.1/24" 47 | "42::1/64" 48 | ]; 49 | routes = if pkgs.lib.versionAtLeast lib.version "24.11" then [ 50 | { Destination = "192.168.43.0/24"; } 51 | { Destination = "192.168.44.0/24"; Gateway = "192.168.43.1"; } 52 | { Destination = "192.168.45.0/24"; Gateway = "43::1"; } 53 | { Destination = "43::0/64"; } 54 | { Destination = "44::1/64"; Gateway = "43::1"; } 55 | ] else [ 56 | # Some static routes that we want to see in the kexeced image 57 | { routeConfig = { Destination = "192.168.43.0/24"; }; } 58 | { routeConfig = { Destination = "192.168.44.0/24"; Gateway = "192.168.43.1"; }; } 59 | { routeConfig = { Destination = "192.168.45.0/24"; Gateway = "43::1"; }; } 60 | { routeConfig = { Destination = "43::0/64"; }; } 61 | { routeConfig = { Destination = "44::1/64"; Gateway = "43::1"; }; } 62 | ]; 63 | networkConfig = { DHCP = "yes"; IPv6AcceptRA = true; }; 64 | }; 65 | }; 66 | }; 67 | } // lib.optionalAttrs (lib.versionOlder lib.version "24.11pre") { 68 | # avoid second overlay 69 | environment.noXlibs = false; 70 | }; 71 | }; 72 | 73 | testScript = /*python*/ '' 74 | import json 75 | import time 76 | import subprocess 77 | import socket 78 | import http.server 79 | from threading import Thread 80 | from typing import Optional 81 | 82 | start_all() 83 | 84 | class DualStackServer(http.server.HTTPServer): 85 | def server_bind(self): 86 | self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) 87 | return super().server_bind() 88 | DualStackServer.address_family = socket.AF_INET6 89 | httpd = DualStackServer(("::", 0), http.server.SimpleHTTPRequestHandler) 90 | 91 | http.server.HTTPServer.address_family = socket.AF_INET6 92 | port = httpd.server_port 93 | def serve_forever(httpd): 94 | with httpd: 95 | httpd.serve_forever() 96 | thread = Thread(target=serve_forever, args=(httpd, )) 97 | thread.setDaemon(True) 98 | thread.start() 99 | 100 | node1.wait_until_succeeds(f"curl -v -I http://10.0.2.2:{port}") 101 | node1.wait_until_succeeds(f"curl -v -I http://[fec0::2]:{port}") 102 | 103 | node1.succeed("ip addr >&2") 104 | node1.succeed("ip route >&2") 105 | node1.succeed("ip -6 route >&2") 106 | node1.succeed("networkctl status eth0 >&2") 107 | 108 | def ssh(cmd: list[str], check: bool = True, stdout: Optional[int] = None) -> subprocess.CompletedProcess[str]: 109 | ssh_cmd = [ 110 | "${pkgs.openssh}/bin/ssh", 111 | "-o", "StrictHostKeyChecking=no", 112 | "-o", "ConnectTimeout=1", 113 | "-i", "${./ssh-keys/id_ed25519}", 114 | "-p", "2222", 115 | "root@127.0.0.1", 116 | "--" 117 | ] + cmd 118 | print(" ".join(ssh_cmd)) 119 | return subprocess.run(ssh_cmd, 120 | text=True, 121 | check=check, 122 | stdout=stdout) 123 | 124 | 125 | while not ssh(["true"], check=False).returncode == 0: 126 | time.sleep(1) 127 | ssh(["cp", "--version"]) 128 | 129 | host_ed25519_before = node1.succeed("cat /etc/ssh/ssh_host_ed25519_key.pub").strip() 130 | node1.succeed('ssh-keygen -t ed25519 -f /root/.ssh/id_ed25519 -q -N ""') 131 | root_ed25519_before = node1.succeed('tee /root/.ssh/authorized_keys < /root/.ssh/id_ed25519.pub').strip() 132 | 133 | # Kexec node1 to the toplevel of node2 via the kexec-boot script 134 | node1.succeed('touch /run/foo') 135 | old_machine_id = node1.succeed("cat /etc/machine-id").strip() 136 | node1.fail('parted --version >&2') 137 | node1.succeed('tar -xf ${kexecInstallerTarball}/nixos-kexec-installer-noninteractive-${pkgs.system}.tar.gz -C /root') 138 | node1.succeed('/root/kexec/ip -V >&2') 139 | node1.succeed('/root/kexec/kexec --version >&2') 140 | # test with dash here to make sure we don't introduce bashisms 141 | node1.succeed('${pkgs.dash}/bin/dash /root/kexec/run >&2') 142 | 143 | # wait for kexec to finish 144 | while ssh(["true"], check=False).returncode == 0: 145 | print("Waiting for kexec to finish...") 146 | time.sleep(1) 147 | 148 | while ssh(["true"], check=False).returncode != 0: 149 | print("Waiting for node2 to come up...") 150 | time.sleep(1) 151 | 152 | while ssh(["systemctl is-active restore-network"], check=False).returncode != 0: 153 | print("Waiting for network to be restored...") 154 | time.sleep(1) 155 | ssh(["systemctl", "status", "restore-network"]) 156 | 157 | print(ssh(["ip", "addr"])) 158 | print(ssh(["ip", "route"])) 159 | print(ssh(["ip", "-6", "route"])) 160 | print(ssh(["networkctl", "status"])) 161 | 162 | new_machine_id = ssh(["cat", "/etc/machine-id"], stdout=subprocess.PIPE).stdout.strip() 163 | assert old_machine_id == new_machine_id, f"{old_machine_id} != {new_machine_id}, machine-id changed" 164 | 165 | assert ssh(["ls", "-la", "/run/foo"], check=False).returncode != 0, "kexeced node1 still has /run/foo" 166 | print(ssh(["parted", "--version"])) 167 | host = ssh(["hostname"], stdout=subprocess.PIPE).stdout.strip() 168 | assert host == "nixos-installer", f"hostname is {host}, not nixos-installer" 169 | 170 | data = json.loads(ssh(["nixos-facter"], stdout=subprocess.PIPE).stdout) 171 | assert data["virtualisation"] == "kvm", f"virtualisation is {data['virtualisation']}, not kvm" 172 | 173 | host_ed25519_after = ssh(["cat", "/etc/ssh/ssh_host_ed25519_key.pub"], stdout=subprocess.PIPE).stdout.strip() 174 | assert host_ed25519_before == host_ed25519_after, f"'{host_ed25519_before}' != '{host_ed25519_after}'" 175 | 176 | root_ed25519_after = ssh(["cat", "/root/.ssh/authorized_keys"], stdout=subprocess.PIPE).stdout.strip() 177 | assert root_ed25519_before in root_ed25519_after, f"'{root_ed25519_before}' not included in '{root_ed25519_after}'" 178 | 179 | print(ssh(["cat", "/etc/systemd/network/00-eth0.network"])) 180 | ssh(["curl", "-v", "-I", f"http://10.0.2.2:{port}"]) 181 | ssh(["curl", "-v", "-I", f"http://[fec0::2]:{port}"]) 182 | 183 | ## Check if static addresses have been restored 184 | ssh(["ping", "-c1", "42::1"]) 185 | ssh(["ping", "-c1", "192.168.42.1"]) 186 | 187 | out = ssh(["ip", "route", "get", "192.168.43.2"], stdout=subprocess.PIPE).stdout 188 | print(out) 189 | assert "192.168.43.2 dev" in out, f"route to `192.168.43.2 dev` not found: {out}" 190 | 191 | out = ssh(["ip", "route", "get", "192.168.44.2"], stdout=subprocess.PIPE).stdout 192 | print(out) 193 | assert "192.168.44.2 via 192.168.43.1" in out, f"route to `192.168.44.2 via 192.168.43.1` not found: {out}" 194 | 195 | out = ssh(["ip", "route", "get", "192.168.45.2"], stdout=subprocess.PIPE).stdout 196 | print(out) 197 | assert "192.168.45.2 via inet6 43::1" in out, f"route to `192.168.45.2 via inet6 43::1` not found: {out}" 198 | 199 | out = ssh(["ip", "route", "get", "43::2"], stdout=subprocess.PIPE).stdout 200 | print(out) 201 | assert "43::2 from :: dev" in out, f"route `43::2 from dev` not found: {out}" 202 | 203 | out = ssh(["ip", "route", "get", "44::2"], stdout=subprocess.PIPE).stdout 204 | print(out) 205 | assert "44::2 from :: via 43::1" in out, f"route to `44::2 from :: via 43::1` not found: {out}" 206 | 207 | node1.crash() 208 | ''; 209 | } 210 | -------------------------------------------------------------------------------- /nix/latest-zfs-kernel.nix: -------------------------------------------------------------------------------- 1 | { 2 | lib, 3 | pkgs, 4 | config, 5 | ... 6 | }: 7 | 8 | let 9 | isUnstable = config.boot.zfs.package == pkgs.zfsUnstable; 10 | zfsCompatibleKernelPackages = lib.filterAttrs ( 11 | name: kernelPackages: 12 | (builtins.match "linux_[0-9]+_[0-9]+" name) != null 13 | && (builtins.tryEval kernelPackages).success 14 | && ( 15 | (!isUnstable && !kernelPackages.zfs.meta.broken) 16 | || (isUnstable && !kernelPackages.zfs_unstable.meta.broken) 17 | ) 18 | ) pkgs.linuxKernel.packages; 19 | latestKernelPackage = lib.last ( 20 | lib.sort (a: b: (lib.versionOlder a.kernel.version b.kernel.version)) (builtins.attrValues zfsCompatibleKernelPackages) 21 | ); 22 | in 23 | { 24 | # Note this might jump back and worth as kernel get added or removed. 25 | boot.kernelPackages = latestKernelPackage; 26 | } 27 | -------------------------------------------------------------------------------- /nix/log-network-status.nix: -------------------------------------------------------------------------------- 1 | { pkgs, ... }: 2 | { 3 | systemd.services.log-network-status = { 4 | wantedBy = [ "multi-user.target" ]; 5 | # No point in restarting this. We just need this after boot 6 | restartIfChanged = false; 7 | 8 | serviceConfig = { 9 | Type = "oneshot"; 10 | StandardOutput = "journal+console"; 11 | ExecStart = [ 12 | # Allow failures, so it still prints what interfaces we have even if we 13 | # not get online 14 | "-${pkgs.systemd}/lib/systemd/systemd-networkd-wait-online" 15 | "${pkgs.iproute2}/bin/ip -c addr" 16 | "${pkgs.iproute2}/bin/ip -c -6 route" 17 | "${pkgs.iproute2}/bin/ip -c -4 route" 18 | "${pkgs.systemd}/bin/networkctl status" 19 | ]; 20 | }; 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /nix/netboot-installer/module.nix: -------------------------------------------------------------------------------- 1 | { config, lib, modulesPath, pkgs, ... }: 2 | { 3 | imports = [ 4 | (modulesPath + "/installer/netboot/netboot-minimal.nix") 5 | ../installer.nix 6 | ../networkd.nix 7 | ../serial.nix 8 | ../restore-remote-access.nix 9 | ]; 10 | 11 | # We are stateless, so just default to latest. 12 | system.stateVersion = config.system.nixos.release; 13 | 14 | system.build.netboot = pkgs.symlinkJoin { 15 | name = "netboot"; 16 | paths = with config.system.build; [ 17 | netbootRamdisk 18 | kernel 19 | (pkgs.runCommand "kernel-params" { } '' 20 | mkdir -p $out 21 | ln -s "${config.system.build.toplevel}/kernel-params" $out/kernel-params 22 | ln -s "${config.system.build.toplevel}/init" $out/init 23 | '') 24 | ]; 25 | preferLocalBuild = true; 26 | }; 27 | systemd.network.networks."10-uplink" = { 28 | matchConfig.Type = "ether"; 29 | networkConfig = { 30 | DHCP = "yes"; 31 | EmitLLDP = "yes"; 32 | IPv6AcceptRA = "yes"; 33 | MulticastDNS = "yes"; 34 | LinkLocalAddressing = "yes"; 35 | LLDP = "yes"; 36 | }; 37 | 38 | dhcpV4Config = { 39 | UseHostname = false; 40 | ClientIdentifier = "mac"; 41 | }; 42 | }; 43 | 44 | networking.hostName = ""; 45 | # overrides normal activation script for setting hostname 46 | system.activationScripts.hostname = lib.mkForce '' 47 | # apply hostname from cmdline 48 | for o in $(< /proc/cmdline); do 49 | case $o in 50 | hostname=*) 51 | IFS== read -r -a hostParam <<< "$o" 52 | ;; 53 | esac 54 | done 55 | hostname "''${hostParam[1]:-nixos}" 56 | ''; 57 | } 58 | -------------------------------------------------------------------------------- /nix/networkd.nix: -------------------------------------------------------------------------------- 1 | { lib, ... }: 2 | { 3 | # Not really needed. Saves a few bytes and the only service we are running is sshd, which we want to be reachable. 4 | networking.firewall.enable = false; 5 | 6 | networking.useNetworkd = true; 7 | systemd.network.enable = true; 8 | 9 | # mdns 10 | networking.firewall.allowedUDPPorts = [ 5353 ]; 11 | systemd.network.networks."99-ethernet-default-dhcp".networkConfig.MulticastDNS = lib.mkDefault "yes"; 12 | systemd.network.networks."99-wireless-client-dhcp".networkConfig.MulticastDNS = lib.mkDefault "yes"; 13 | } 14 | -------------------------------------------------------------------------------- /nix/nix-settings.nix: -------------------------------------------------------------------------------- 1 | # take from srvos 2 | { lib, ... }: 3 | { 4 | # Fallback quickly if substituters are not available. 5 | nix.settings.connect-timeout = 5; 6 | 7 | # Enable flakes 8 | nix.settings.extra-experimental-features = [ "nix-command" "flakes" ]; 9 | nix.settings.extra-substituters = [ "/" ]; 10 | 11 | # The default at 10 is rarely enough. 12 | nix.settings.log-lines = lib.mkDefault 25; 13 | 14 | # Avoid disk full issues 15 | nix.settings.max-free = lib.mkDefault (3000 * 1024 * 1024); 16 | nix.settings.min-free = lib.mkDefault (512 * 1024 * 1024); 17 | 18 | # TODO: cargo culted. 19 | nix.daemonCPUSchedPolicy = lib.mkDefault "batch"; 20 | nix.daemonIOSchedClass = lib.mkDefault "idle"; 21 | nix.daemonIOSchedPriority = lib.mkDefault 7; 22 | 23 | # Avoid copying unnecessary stuff over SSH 24 | nix.settings.builders-use-substitutes = true; 25 | } 26 | -------------------------------------------------------------------------------- /nix/noninteractive.nix: -------------------------------------------------------------------------------- 1 | # This module optimizes for non-interactive deployments by remove some store paths 2 | # which are primarily useful for interactive installations. 3 | 4 | { lib, pkgs, modulesPath, options, ... }: 5 | { 6 | disabledModules = [ 7 | # This module adds values to multiple lists (systemPackages, supportedFilesystems) 8 | # which are impossible/unpractical to remove, so we disable the entire module. 9 | "profiles/base.nix" 10 | ]; 11 | 12 | imports = [ 13 | ./zfs-minimal.nix 14 | ./python-minimal.nix 15 | ./noveau-workaround.nix 16 | # reduce closure size by removing perl 17 | "${modulesPath}/profiles/perlless.nix" 18 | # FIXME: we still are left with nixos-generate-config due to nixos-install-tools 19 | { system.forbiddenDependenciesRegexes = lib.mkForce []; } 20 | ]; 21 | 22 | config = { 23 | # nixos-option is mainly useful for interactive installations 24 | system.tools.nixos-option.enable = false; 25 | 26 | # among others, this prevents carrying a stdenv with gcc in the image 27 | system.extraDependencies = lib.mkForce [ ]; 28 | 29 | # prevents shipping nixpkgs, unnecessary if system is evaluated externally 30 | nix.registry = lib.mkForce { }; 31 | 32 | # would pull in nano 33 | programs.nano.enable = false; 34 | 35 | # prevents strace 36 | environment.defaultPackages = lib.mkForce [ 37 | pkgs.parted 38 | pkgs.gptfdisk 39 | pkgs.e2fsprogs 40 | ]; 41 | 42 | # included in systemd anyway 43 | systemd.sysusers.enable = true; 44 | services.userborn.enable = false; 45 | 46 | # normal users are not allowed with sys-users 47 | # see https://github.com/NixOS/nixpkgs/pull/328926 48 | users.users.nixos = { 49 | isSystemUser = true; 50 | isNormalUser = lib.mkForce false; 51 | shell = "/run/current-system/sw/bin/bash"; 52 | group = "nixos"; 53 | }; 54 | users.groups.nixos = {}; 55 | 56 | # we have still run0 from systemd and most of the time we just use root 57 | security.sudo.enable = false; 58 | security.polkit.enable = lib.mkForce false; 59 | 60 | documentation.man.enable = false; 61 | 62 | # no dependency on x11 63 | services.dbus.implementation = "broker"; 64 | 65 | # introduces x11 dependencies 66 | security.pam.services.su.forwardXAuth = lib.mkForce false; 67 | 68 | # Don't install the /lib/ld-linux.so.2 stub. This saves one instance of nixpkgs. 69 | environment.ldso32 = null; 70 | 71 | # we prefer root as this is also what we use in nixos-anywhere 72 | services.getty.autologinUser = lib.mkForce "root"; 73 | 74 | # we are missing this from base.nix 75 | boot.supportedFilesystems = [ 76 | "ext4" 77 | "btrfs" 78 | ## quiet huge dependency closure 79 | #"cifs" 80 | "f2fs" 81 | ## anyone still using this over ext4? 82 | #"jfs" 83 | "ntfs" 84 | ## no longer seems to be maintained, anyone still using it? 85 | #"reiserfs" 86 | "vfat" 87 | "xfs" 88 | ]; 89 | boot.kernelModules = [ 90 | # we have to explicitly enable this, otherwise it is not loaded even when creating a raid: 91 | # https://github.com/nix-community/nixos-anywhere/issues/249 92 | "dm-raid" 93 | ]; 94 | } // lib.optionalAttrs (options.hardware ? firmwareCompression) { 95 | hardware.firmwareCompression = "xz"; 96 | }; 97 | } 98 | -------------------------------------------------------------------------------- /nix/noveau-workaround.nix: -------------------------------------------------------------------------------- 1 | { 2 | # fixes blank screen on boot for some cards 3 | boot.kernelParams = [ "nouveau.modeset=0" ]; 4 | } 5 | -------------------------------------------------------------------------------- /nix/python-minimal.nix: -------------------------------------------------------------------------------- 1 | { 2 | nixpkgs.overlays = [ 3 | (final: prev: { 4 | bcachefs-tools = prev.bcachefs-tools.override { python3 = final.python3Minimal; }; 5 | nfs-utils = prev.nfs-utils.override { python3 = final.python3Minimal; }; 6 | }) 7 | ]; 8 | } 9 | -------------------------------------------------------------------------------- /nix/restore-remote-access.nix: -------------------------------------------------------------------------------- 1 | { 2 | # We have a bug in 23.11 in combination with netboot. 3 | boot.initrd.systemd.enable = true; 4 | boot.initrd.systemd.services.restore-state-from-initrd = { 5 | unitConfig = { 6 | DefaultDependencies = false; 7 | RequiresMountsFor = "/sysroot /dev"; 8 | }; 9 | wantedBy = [ "initrd.target" ]; 10 | requiredBy = [ "rw-etc.service" ]; 11 | before = [ "rw-etc.service" ]; 12 | serviceConfig.Type = "oneshot"; 13 | # Restore ssh host and user keys if they are available. 14 | # This avoids warnings of unknown ssh keys. 15 | script = '' 16 | mkdir -m 700 -p /sysroot/root/.ssh 17 | mkdir -m 755 -p /sysroot/etc/ssh 18 | mkdir -m 755 -p /sysroot/root/network 19 | if [[ -f ssh/authorized_keys ]]; then 20 | install -m 400 ssh/authorized_keys /sysroot/root/.ssh 21 | fi 22 | install -m 400 ssh/ssh_host_* /sysroot/etc/ssh 23 | cp *.json /sysroot/root/network/ 24 | if [[ -f machine-id ]]; then 25 | cp machine-id /sysroot/etc/machine-id 26 | fi 27 | ''; 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /nix/serial.nix: -------------------------------------------------------------------------------- 1 | { pkgs, lib, ... }: 2 | { 3 | # IPMI SOL console redirection stuff 4 | boot.kernelParams = 5 | [ "console=tty0" ] 6 | ++ (lib.optional ( 7 | pkgs.stdenv.hostPlatform.isAarch32 || pkgs.stdenv.hostPlatform.isAarch64 8 | ) "console=ttyAMA0,115200") 9 | ++ (lib.optional (pkgs.stdenv.hostPlatform.isRiscV) "console=ttySIF0,115200") 10 | ++ [ "console=ttyS0,115200" ]; 11 | } 12 | -------------------------------------------------------------------------------- /nix/zfs-minimal.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, ... }: 2 | # incorperate a space-optimized version of zfs 3 | let 4 | zfs = pkgs.zfsUnstable.override { 5 | # this overrides saves 10MB 6 | samba = pkgs.coreutils; 7 | 8 | python3 = pkgs.python3Minimal; 9 | }; 10 | in 11 | { 12 | services.udev.packages = [ zfs ]; # to hook zvol naming, etc. 13 | # unsure if need this, but in future udev rules could potentially point to systemd services. 14 | systemd.packages = [ zfs ]; 15 | environment.defaultPackages = lib.mkForce [ zfs ]; # this merges with outer noninteractive module. 16 | 17 | boot.kernelModules = [ "zfs" ]; 18 | boot.extraModulePackages = [ config.boot.kernelPackages.zfs_unstable ]; 19 | 20 | networking.hostId = lib.mkDefault "8425e349"; 21 | } 22 | --------------------------------------------------------------------------------