├── .github └── workflows │ ├── build.yml │ ├── check.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── VERSION ├── flake.lock ├── flake.nix ├── keys ├── KEK.auth ├── PK.auth └── db.auth ├── misc └── local-sysupdate │ ├── 10-uki.transfer │ ├── 20-usr-verity.transfer │ └── 22-usr.transfer ├── modules ├── hardware │ └── generic-pc.nix ├── image │ ├── initrd-repart-expand.nix │ ├── repart-image-compress.nix │ ├── repart-image-verity-store-defaults.nix │ ├── sysupdate-verity-store.nix │ └── update-package.nix └── profiles │ ├── image-based.nix │ ├── minimal.nix │ ├── network.nix │ └── server.nix ├── pkgs ├── composefs.nix ├── linux-firmware.nix ├── openssh.nix ├── qemu.nix ├── systemd-ukify.nix └── systemd.nix ├── scripts ├── pack-release.sh └── sign-release.sh ├── shell.nix └── tests ├── common.nix ├── integration.nix ├── lib.nix └── system-update.nix /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - run: echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns 11 | - uses: actions/checkout@v4 12 | - uses: nixbuild/nix-quick-install-action@v30 13 | - uses: nix-community/cache-nix-action@v6 14 | with: 15 | primary-key: nix-${{ runner.os }}-${{ hashFiles('**/*.nix', '**/flake.lock') }} 16 | restore-prefixes-first-match: nix-${{ runner.os }}- 17 | gc-max-store-size-linux: 1G 18 | - name: Build nixlet-insecure 19 | run: nix build .#nixlet-insecure -o nixlet-insecure-unsigned 20 | - name: Archive nixlet-insecure build artifacts 21 | uses: actions/upload-artifact@v4 22 | with: 23 | name: nixlet-insecure-unsigned 24 | path: nixlet-insecure-unsigned 25 | - name: Build nixlet 26 | run: nix build .#nixlet -o nixlet-unsigned 27 | - name: Archive nixlet build artifacts 28 | uses: actions/upload-artifact@v4 29 | with: 30 | name: nixlet-unsigned 31 | path: nixlet-unsigned 32 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Validate 2 | 3 | on: 4 | workflow_call: 5 | pull_request: 6 | 7 | jobs: 8 | check: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: nixbuild/nix-quick-install-action@v30 13 | - run: echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns 14 | - uses: nix-community/cache-nix-action@v6 15 | with: 16 | primary-key: nix-${{ runner.os }}-${{ hashFiles('**/*.nix', '**/flake.lock') }} 17 | restore-prefixes-first-match: nix-${{ runner.os }}- 18 | gc-max-store-size-linux: 1G 19 | - name: nix flake check 20 | run: nix flake check 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | uses: ./.github/workflows/build.yml 11 | release: 12 | if: github.ref_name == 'main' 13 | needs: build 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: write 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Read version 20 | id: version 21 | run: | 22 | VERSION=$(cat VERSION) 23 | echo "version=$VERSION" >> $GITHUB_ENV 24 | - name: Download artifacts 25 | uses: actions/download-artifact@v4 26 | with: 27 | name: nixlet-unsigned 28 | path: nixlet-unsigned 29 | - name: Sign for Secure Boot 30 | env: 31 | DB_KEY: ${{ secrets.DBKEY }} 32 | DB_CRT: ${{ secrets.DBCRT }} 33 | run: ./scripts/sign-release.sh 34 | - name: Download insecure artifacts 35 | uses: actions/download-artifact@v4 36 | with: 37 | name: nixlet-insecure-unsigned 38 | path: nixlet-insecure-unsigned 39 | - name: Generate release bundle 40 | run: ./scripts/pack-release.sh 41 | - name: Release 42 | uses: ncipollo/release-action@v1.14.0 43 | with: 44 | artifacts: "release/*" 45 | tag: ${{ env.version }} 46 | name: Release ${{ env.version }} 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | result* 2 | nixlet-disk.qcow2 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Peter Marshall 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 | # nixlet 2 | 3 | A minimal, immutable NixOS-based distro with automatic A/B updates. 4 | 5 | - Currently intended for headless servers running Docker containers 6 | - Supports a secure chain of trust using Secure Boot and dm-verity 7 | - Supports automatic boot assessment and unattended rollbacks 8 | - Supports automated provisioning of SSH authorized_keys 9 | - Uses TPM-backed encryption for user data 10 | 11 | ## Included Software 12 | 13 | - openssh 14 | - podman 15 | 16 | ## Usage 17 | 18 | Just write a release image (`nixlet_*.img`) to the system's disk and put your public SSH key in `default-ssh-authorized-keys.txt` on the first partition. The image is expanded on first boot. 19 | 20 | The encrypted user data (home) partition is automatically unlocked via the TPM by default. A password or recovery key can be added via `systemd-cryptenroll`. 21 | 22 | The default username is "admin" with password login disabled. 23 | 24 | ### SSH Key Provisioning 25 | 26 | Authorized keys are provisioned on boot for the "admin" user if `.ssh/authorized_keys` doesn't exist. Keys are read from `default-ssh-authorized-keys.txt` on the ESP. 27 | 28 | ## Partition Layout 29 | 30 | - ESP: vfat (96M) 31 | - Verity A: dm-verity hash (64M) 32 | - System A: erofs (512M) 33 | - Verity B: dm-verity hash (64M) 34 | - System B: erofs (512M) 35 | - Home: btrfs on LUKS (Rest of available space) 36 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.3.2 2 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1749534015, 6 | "narHash": "sha256-tQ81JSorX65STbyJA10TmDVL5Vd3UDfYgp5T1pW/qzI=", 7 | "owner": "NixOS", 8 | "repo": "nixpkgs", 9 | "rev": "03b4f20ad93ed52a80ad55ec88f5eef00279d405", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "NixOS", 14 | "ref": "nixos-unstable-small", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs" 22 | } 23 | } 24 | }, 25 | "root": "root", 26 | "version": 7 27 | } 28 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Minimal image-based NixOS configuration"; 3 | inputs = { 4 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable-small"; 5 | }; 6 | outputs = { self, nixpkgs }: let 7 | pkgs = import nixpkgs { 8 | system = "x86_64-linux"; 9 | }; 10 | updateUrl = "https://github.com/petm5/nixlet/releases/latest/download"; 11 | releaseVersion = nixpkgs.lib.strings.trim (builtins.readFile ./VERSION); 12 | baseConfig = [ 13 | (nixpkgs + "/nixos/modules/image/repart.nix") 14 | ./modules/image/repart-image-verity-store-defaults.nix 15 | ./modules/image/repart-image-compress.nix 16 | ./modules/image/update-package.nix 17 | ./modules/image/initrd-repart-expand.nix 18 | ./modules/image/sysupdate-verity-store.nix 19 | ./modules/profiles/minimal.nix 20 | ./modules/profiles/image-based.nix 21 | ./modules/profiles/server.nix 22 | ./modules/hardware/generic-pc.nix 23 | (nixpkgs + "/nixos/modules/profiles/qemu-guest.nix") 24 | { 25 | nixpkgs.hostPlatform = "x86_64-linux"; 26 | system.stateVersion = "24.05"; 27 | system.image.updates.url = "${updateUrl}"; 28 | system.image.id = "nixlet"; 29 | system.image.version = releaseVersion; 30 | boot.kernelPackages = pkgs.linuxPackages_latest; 31 | } 32 | ]; 33 | in { 34 | nixosSystems.x86_64-linux.nixlet = nixpkgs.lib.nixosSystem { 35 | modules = baseConfig; 36 | }; 37 | nixosSystems.x86_64-linux.nixlet-insecure = nixpkgs.lib.nixosSystem { 38 | modules = baseConfig ++ [ { 39 | system.image.filesystems.encrypt = false; 40 | system.image.id = nixpkgs.lib.mkOverride 0 "nixlet-insecure"; 41 | } ]; 42 | }; 43 | packages.x86_64-linux.nixlet = self.nixosSystems.x86_64-linux.nixlet.config.system.build.updatePackage; 44 | packages.x86_64-linux.nixlet-insecure = self.nixosSystems.x86_64-linux.nixlet-insecure.config.system.build.updatePackage; 45 | checks.x86_64-linux = nixpkgs.lib.listToAttrs (map (test: nixpkgs.lib.nameValuePair "${test}" (import ./tests/${test}.nix { 46 | pkgs = nixpkgs.legacyPackages."x86_64-linux"; 47 | inherit self; 48 | })) [ "integration" "system-update" ]); 49 | apps.x86_64-linux.nixlet-live-test = let 50 | testSystem = self.nixosSystems.x86_64-linux.nixlet-insecure; 51 | script = (import ./tests/common.nix rec { 52 | inherit self; 53 | pkgs = nixpkgs.legacyPackages."x86_64-linux"; 54 | lib = pkgs.lib; 55 | }).makeInteractiveTest { 56 | image = "${testSystem.config.system.build.finalImage}/${testSystem.config.image.fileName}"; 57 | }; 58 | in { 59 | type = "app"; 60 | program = toString script; 61 | }; 62 | devShells.x86_64-linux.default = import ./shell.nix { inherit pkgs; }; 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /keys/KEK.auth: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petm5/nixlet/9a9f11a777ce8fb2f3c3e72faded8f858e51b215/keys/KEK.auth -------------------------------------------------------------------------------- /keys/PK.auth: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petm5/nixlet/9a9f11a777ce8fb2f3c3e72faded8f858e51b215/keys/PK.auth -------------------------------------------------------------------------------- /keys/db.auth: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petm5/nixlet/9a9f11a777ce8fb2f3c3e72faded8f858e51b215/keys/db.auth -------------------------------------------------------------------------------- /misc/local-sysupdate/10-uki.transfer: -------------------------------------------------------------------------------- 1 | [Transfer] 2 | Verify=no 3 | 4 | [Source] 5 | Type=regular-file 6 | Path=nixlet-insecure 7 | MatchPattern=nixlet-insecure_@v.efi 8 | 9 | [Target] 10 | Type=regular-file 11 | Path=/EFI/Linux 12 | PathRelativeTo=esp 13 | MatchPattern=nixlet-insecure_@v+@l-@d.efi nixlet-insecure_@v+@l.efi nixlet-insecure_@v.efi 14 | Mode=0444 15 | TriesLeft=3 16 | TriesDone=0 17 | InstancesMax=2 18 | -------------------------------------------------------------------------------- /misc/local-sysupdate/20-usr-verity.transfer: -------------------------------------------------------------------------------- 1 | [Transfer] 2 | Verify=no 3 | 4 | [Source] 5 | Type=regular-file 6 | Path=nixlet-insecure 7 | MatchPattern=nixlet-insecure_@v_@u.verity 8 | 9 | [Target] 10 | Type=partition 11 | Path=auto 12 | MatchPattern=verity-@v 13 | MatchPartitionType=usr-verity 14 | ReadOnly=1 15 | -------------------------------------------------------------------------------- /misc/local-sysupdate/22-usr.transfer: -------------------------------------------------------------------------------- 1 | [Transfer] 2 | Verify=no 3 | 4 | [Source] 5 | Type=regular-file 6 | Path=nixlet-insecure 7 | MatchPattern=nixlet-insecure_@v_@u.usr 8 | 9 | [Target] 10 | Type=partition 11 | Path=auto 12 | MatchPattern=usr-@v 13 | MatchPartitionType=usr 14 | ReadOnly=1 15 | -------------------------------------------------------------------------------- /modules/hardware/generic-pc.nix: -------------------------------------------------------------------------------- 1 | # Support some generic PC hardware 2 | { 3 | 4 | boot.kernelModules = [ 5 | "usb_storage" "uas" "sd_mod" 6 | "r8169" 7 | "ehci-hcd" "ehci-pci" 8 | "xhci-hcd" "xhci-pci" "xhci-pci-renesas" 9 | "nvme" 10 | "aesni_intel" "crypto_simd" 11 | "kvm" "kvm_intel" "kvm_amd" 12 | ]; 13 | 14 | } 15 | -------------------------------------------------------------------------------- /modules/image/initrd-repart-expand.nix: -------------------------------------------------------------------------------- 1 | { config, lib, ... }: let 2 | cfg = config.system.image.filesystems; 3 | in { 4 | 5 | options.system.image.filesystems = { 6 | encrypt = lib.mkEnableOption "TPM-backed user data encryption" // { 7 | default = true; 8 | }; 9 | }; 10 | 11 | config = lib.mkMerge [({ 12 | 13 | assertions = [ 14 | { assertion = config.boot.initrd.systemd.enable; } 15 | ]; 16 | 17 | boot.initrd.systemd.repart.enable = true; 18 | systemd.repart.partitions = { 19 | "10-esp" = { 20 | Type = "esp"; 21 | Format = "vfat"; 22 | SizeMinBytes = "96M"; 23 | SizeMaxBytes = "96M"; 24 | }; 25 | "20-usr-verity-a" = { 26 | Type = "usr-verity"; 27 | SizeMinBytes = "64M"; 28 | SizeMaxBytes = "64M"; 29 | }; 30 | "22-usr-a" = { 31 | Type = "usr"; 32 | SizeMinBytes = "512M"; 33 | SizeMaxBytes = "512M"; 34 | }; 35 | "30-usr-verity-b" = { 36 | Type = "usr-verity"; 37 | SizeMinBytes = "64M"; 38 | SizeMaxBytes = "64M"; 39 | Label = "_empty"; 40 | ReadOnly = 1; 41 | }; 42 | "32-usr-b" = { 43 | Type = "usr"; 44 | SizeMinBytes = "512M"; 45 | SizeMaxBytes = "512M"; 46 | Label = "_empty"; 47 | ReadOnly = 1; 48 | }; 49 | "40-state" = { 50 | Type = "root"; 51 | Format = "btrfs"; 52 | SizeMinBytes = "16M"; 53 | SizeMaxBytes = "512M"; 54 | Encrypt = lib.optionalString cfg.encrypt "tpm2"; 55 | MakeDirectories = "/usr /etc /root /srv /var"; 56 | }; 57 | "50-home" = { 58 | Type = "home"; 59 | Format = "btrfs"; 60 | SizeMinBytes = "16M"; 61 | Encrypt = lib.optionalString cfg.encrypt "tpm2"; 62 | }; 63 | }; 64 | 65 | boot.initrd.systemd.additionalUpstreamUnits = [ "initrd-usr-fs.target" ]; 66 | 67 | boot.initrd.systemd.services.systemd-repart.after = lib.mkForce [ ]; 68 | 69 | boot.initrd.supportedFilesystems.btrfs = true; 70 | 71 | boot.kernelParams = [ "rootfstype=btrfs" "rootflags=rw" ]; 72 | 73 | boot.initrd.systemd.root = "gpt-auto"; 74 | 75 | # Required to mount the efi partition 76 | boot.kernelModules = [ "vfat" "nls_cp437" "nls_iso8859-1" ]; 77 | 78 | # Don't wait for TPM with encryption disabled 79 | boot.initrd.systemd.tpm2.enable = cfg.encrypt; 80 | systemd.tpm2.enable = cfg.encrypt; 81 | 82 | }) (lib.mkIf cfg.encrypt { 83 | 84 | boot.initrd.luks.forceLuksSupportInInitrd = true; 85 | boot.initrd.kernelModules = [ "dm_mod" "dm_crypt" ] ++ config.boot.initrd.luks.cryptoModules; 86 | 87 | # BUG: mkfs.btrfs hangs when trying to discard an encrypted partition. 88 | boot.initrd.systemd.services.systemd-repart.serviceConfig.Environment = [ 89 | "SYSTEMD_REPART_MKFS_OPTIONS_BTRFS=--nodiscard" 90 | ]; 91 | 92 | # Measure UEFI settings (PCR 3), Secure Boot policy (PCR 7), system extensions (PCR 13) 93 | boot.initrd.systemd.services.systemd-repart.serviceConfig.ExecStart = lib.mkForce [ 94 | " " 95 | '' 96 | ${config.boot.initrd.systemd.package}/bin/systemd-repart \ 97 | --definitions=/etc/repart.d \ 98 | --dry-run=no 99 | --tpm2-pcrs=3,7,13 100 | '' 101 | ]; 102 | 103 | })]; 104 | 105 | } 106 | -------------------------------------------------------------------------------- /modules/image/repart-image-compress.nix: -------------------------------------------------------------------------------- 1 | { 2 | image.repart.mkfsOptions.erofs = [ "-zlz4hc,12" "-C1048576" "-Efragments,dedupe,ztailpacking" ]; 3 | } 4 | -------------------------------------------------------------------------------- /modules/image/repart-image-verity-store-defaults.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, ... }: 2 | let 3 | inherit (pkgs.stdenv.hostPlatform) efiArch; 4 | inherit (config.image.repart.verityStore) partitionIds; 5 | in { 6 | assertions = [ 7 | { assertion = config.boot.initrd.systemd.enable; } 8 | ]; 9 | 10 | fileSystems."/nix/store" = { 11 | device = "/usr/nix/store"; 12 | options = [ "bind" ]; 13 | }; 14 | 15 | boot.kernelParams = [ "mount.usrfstype=erofs" "mount.usrflags=ro" ]; 16 | 17 | image.repart = { 18 | verityStore.enable = true; 19 | 20 | partitions = { 21 | ${partitionIds.esp} = { 22 | contents = { 23 | # Include systemd-boot 24 | "/EFI/BOOT/BOOT${lib.toUpper efiArch}.EFI".source = 25 | "${pkgs.systemdUkify}/lib/systemd/boot/efi/systemd-boot${efiArch}.efi"; 26 | }; 27 | repartConfig = { 28 | Type = "esp"; 29 | Format = "vfat"; 30 | SizeMinBytes = "96M"; 31 | SplitName = "-"; 32 | }; 33 | }; 34 | ${partitionIds.store-verity}.repartConfig = { 35 | SizeMinBytes = "64M"; 36 | SizeMaxBytes = "64M"; 37 | Label = "verity-${config.system.image.version}"; 38 | SplitName = "verity"; 39 | ReadOnly = 1; 40 | }; 41 | ${partitionIds.store}.repartConfig = { 42 | Minimize = "best"; 43 | Label = "usr-${config.system.image.version}"; 44 | SplitName = "usr"; 45 | ReadOnly = 1; 46 | }; 47 | }; 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /modules/image/sysupdate-verity-store.nix: -------------------------------------------------------------------------------- 1 | { config, lib, ... }: { 2 | 3 | options.system.image.updates = { 4 | enable = lib.mkEnableOption "system updates via systemd-sysupdate" // { 5 | default = config.system.image.updates.url != null; 6 | }; 7 | url = lib.mkOption { 8 | type = lib.types.nullOr lib.types.str; 9 | default = null; 10 | }; 11 | }; 12 | 13 | config = lib.mkIf config.system.image.updates.enable { 14 | 15 | assertions = [ 16 | { assertion = config.system.image.updates.url != null; } 17 | ]; 18 | 19 | systemd.sysupdate.enable = true; 20 | systemd.sysupdate.reboot.enable = lib.mkDefault true; 21 | 22 | systemd.sysupdate.transfers = { 23 | "10-uki" = { 24 | Transfer = { 25 | Verify = "no"; 26 | }; 27 | Source = { 28 | Type = "url-file"; 29 | Path = "${config.system.image.updates.url}"; 30 | MatchPattern = "${config.boot.uki.name}_@v.efi"; 31 | }; 32 | Target = { 33 | Type = "regular-file"; 34 | Path = "/EFI/Linux"; 35 | PathRelativeTo = "esp"; 36 | MatchPattern = "${config.boot.uki.name}_@v+@l-@d.efi ${config.boot.uki.name}_@v+@l.efi ${config.boot.uki.name}_@v.efi"; 37 | Mode = "0444"; 38 | TriesLeft = 3; 39 | TriesDone = 0; 40 | InstancesMax = 2; 41 | }; 42 | }; 43 | "20-usr-verity" = { 44 | Transfer = { 45 | Verify = "no"; 46 | }; 47 | Source = { 48 | Type = "url-file"; 49 | Path = "${config.system.image.updates.url}"; 50 | MatchPattern = "${config.system.image.id}_@v_@u.verity"; 51 | }; 52 | Target = { 53 | Type = "partition"; 54 | Path = "auto"; 55 | MatchPattern = "verity-@v"; 56 | MatchPartitionType = "usr-verity"; 57 | ReadOnly = 1; 58 | }; 59 | }; 60 | "22-usr" = { 61 | Transfer = { 62 | Verify = "no"; 63 | }; 64 | Source = { 65 | Type = "url-file"; 66 | Path = "${config.system.image.updates.url}"; 67 | MatchPattern = "${config.system.image.id}_@v_@u.usr"; 68 | }; 69 | Target = { 70 | Type = "partition"; 71 | Path = "auto"; 72 | MatchPattern = "usr-@v"; 73 | MatchPartitionType = "usr"; 74 | ReadOnly = 1; 75 | }; 76 | }; 77 | }; 78 | 79 | systemd.additionalUpstreamSystemUnits = [ 80 | "systemd-bless-boot.service" 81 | "boot-complete.target" 82 | ]; 83 | 84 | }; 85 | 86 | } 87 | -------------------------------------------------------------------------------- /modules/image/update-package.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, ... }: let 2 | finalImage = config.system.build.finalImage.override { 3 | split = true; 4 | }; 5 | 6 | verityImgAttrs = builtins.fromJSON (builtins.readFile "${finalImage}/repart-output.json"); 7 | # HACK: Magic indices are used to select partitions, which is error-prone 8 | usrAttrs = builtins.elemAt verityImgAttrs 2; 9 | verityAttrs = builtins.elemAt verityImgAttrs 1; 10 | 11 | usrUuid = usrAttrs.uuid; 12 | verityUuid = verityAttrs.uuid; 13 | in { 14 | system.build.updatePackage = let 15 | updateFiles = [ 16 | { 17 | name = "${config.system.image.id}_${config.system.image.version}.efi"; 18 | path = "${config.system.build.uki}/${config.system.boot.loader.ukiFile}"; 19 | } 20 | { 21 | name = "${config.system.image.id}_${config.system.image.version}_${verityUuid}.verity"; 22 | path = "${finalImage}/${config.image.baseName}.verity.raw"; 23 | } 24 | { 25 | name = "${config.system.image.id}_${config.system.image.version}_${usrUuid}.usr"; 26 | path = "${finalImage}/${config.image.baseName}.usr.raw"; 27 | } 28 | ]; 29 | createHash = { name, path }: lib.concatStringsSep " " [ (builtins.hashFile "sha256" path) name ]; 30 | in (pkgs.linkFarm "${config.system.build.image.pname}-update-package" (updateFiles ++ [ 31 | { 32 | name = "${config.system.image.id}_${config.system.image.version}.img"; 33 | path = "${finalImage}/${config.image.baseName}.raw"; 34 | } 35 | { 36 | name = "SHA256SUMS"; 37 | path = pkgs.writeText "sha256sums.txt" (lib.concatLines (map createHash updateFiles)); 38 | } 39 | ])); 40 | } 41 | -------------------------------------------------------------------------------- /modules/profiles/image-based.nix: -------------------------------------------------------------------------------- 1 | { lib, ... }: { 2 | 3 | # System cannot be rebuilt 4 | nix.enable = false; 5 | system.switch.enable = false; 6 | 7 | nixpkgs.flake.setNixPath = false; 8 | nixpkgs.flake.setFlakeRegistry = false; 9 | 10 | # Try to avoid interpreters 11 | networking.useNetworkd = true; 12 | systemd.network.wait-online.enable = lib.mkDefault false; 13 | boot.initrd.systemd.enable = true; 14 | 15 | # Use a simple bootloader 16 | boot.loader.grub.enable = false; 17 | boot.loader.systemd-boot.enable = true; 18 | 19 | # The system does not need "human" users 20 | services.userborn.enable = false; 21 | systemd.sysusers.enable = true; 22 | 23 | systemd.services."systemd-oomd".unitConfig.After = "systemd-sysusers.service"; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /modules/profiles/minimal.nix: -------------------------------------------------------------------------------- 1 | { config, pkgs, modulesPath, ... }: { 2 | 3 | imports = [ 4 | (modulesPath + "/profiles/minimal.nix") 5 | (modulesPath + "/profiles/perlless.nix") 6 | ]; 7 | 8 | # Overlays to reduce build time and closure size 9 | nixpkgs.overlays = [(self: super: { 10 | systemdUkify = self.callPackage ../../pkgs/systemd-ukify.nix { inherit super; }; 11 | qemu_tiny = self.callPackage ../../pkgs/qemu.nix { inherit super; }; 12 | composefs = self.callPackage ../../pkgs/composefs.nix { inherit super; }; 13 | })]; 14 | 15 | # Don't include kernel or modules in rootfs 16 | boot.kernel.enable = false; 17 | boot.modprobeConfig.enable = false; 18 | boot.bootspec.enable = false; 19 | system.build = { inherit (config.boot.kernelPackages) kernel; }; 20 | system.modulesTree = [ config.boot.kernelPackages.kernel ] ++ config.boot.extraModulePackages; 21 | 22 | # Modules must be loaded by initrd 23 | boot.initrd.kernelModules = config.boot.kernelModules; 24 | 25 | boot.kernelModules = [ 26 | # Required for systemd SMBIOS credential import 27 | "dmi_sysfs" 28 | ]; 29 | 30 | # Remove foreign language support 31 | i18n.supportedLocales = [ 32 | "en_US.UTF-8/UTF-8" 33 | ]; 34 | 35 | programs.nano.enable = false; 36 | 37 | } 38 | -------------------------------------------------------------------------------- /modules/profiles/network.nix: -------------------------------------------------------------------------------- 1 | { lib, ... }: { 2 | 3 | # Use TCP BBR 4 | boot.kernel.sysctl = { 5 | "net.core.default_qdisc" = "fq"; 6 | "net.ipv4.tcp_congestion_control" = "bbr"; 7 | }; 8 | 9 | # Use nftables 10 | networking.nftables.enable = lib.mkDefault true; 11 | 12 | # Use systemd-networkd 13 | networking.useNetworkd = true; 14 | systemd.network.wait-online.enable = true; 15 | 16 | # Explicitly load networking modules 17 | boot.kernelModules = [ 18 | "ip_tables" 19 | "x_tables" 20 | "nf_tables" 21 | "nft_ct" 22 | "nft_log" 23 | "nf_log_syslog" 24 | "nft_fib" 25 | "nft_fib_inet" 26 | "nft_compat" 27 | "nft_nat" 28 | "nft_chain_nat" 29 | "nft_masq" 30 | "nfnetlink" 31 | "xt_conntrack" 32 | "nf_conntrack" 33 | "nf_log_syslog" 34 | "nf_nat" 35 | "af_packet" 36 | "bridge" 37 | "veth" 38 | "tcp_bbr" 39 | "sch_fq_codel" 40 | "ipt_rpfilter" 41 | "ip6t_rpfilter" 42 | "sch_fq" 43 | "tun" 44 | "tap" 45 | "xt_MASQUERADE" 46 | "xt_mark" 47 | "xt_comment" 48 | "xt_multiport" 49 | "xt_addrtype" 50 | ]; 51 | 52 | } 53 | -------------------------------------------------------------------------------- /modules/profiles/server.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, modulesPath, ... }: { 2 | 3 | imports = [ 4 | ./network.nix 5 | ]; 6 | 7 | # The server is accessed via ssh, passwords are unnecessary 8 | users.allowNoPasswordLogin = true; 9 | 10 | users.mutableUsers = true; 11 | 12 | # Replace sudo with doas 13 | security.sudo.enable = lib.mkDefault false; 14 | security.doas.enable = lib.mkDefault true; 15 | security.doas.wheelNeedsPassword = lib.mkDefault false; 16 | 17 | environment.systemPackages = with pkgs; [ 18 | doas-sudo-shim 19 | iotop 20 | ]; 21 | 22 | # Enable a basic text editor 23 | programs.vim.enable = true; 24 | programs.vim.defaultEditor = lib.mkDefault true; 25 | 26 | services.openssh.enable = true; 27 | 28 | # Disable password auth 29 | services.openssh.settings.PasswordAuthentication = lib.mkDefault false; 30 | 31 | # Disable RSA key generation 32 | services.openssh.hostKeys = [ 33 | { 34 | path = "/etc/ssh/ssh_host_ed25519_key"; 35 | type = "ed25519"; 36 | } 37 | ]; 38 | 39 | virtualisation.podman.enable = true; 40 | 41 | # TODO: Add kubelet? 42 | 43 | # Allow unprivileged ports 44 | boot.kernel.sysctl = { 45 | "net.ipv4.ip_unprivileged_port_start" = 0; 46 | }; 47 | 48 | networking.firewall.enable = false; 49 | 50 | # Avoid conflicts with DNS servers 51 | # services.resolved.extraConfig = '' 52 | # DNSStubListener=no 53 | # ''; 54 | 55 | # Gives a performance boost on low-spec servers 56 | zramSwap.enable = true; 57 | boot.kernelModules = [ "zram" ]; 58 | 59 | time.timeZone = "UTC"; 60 | 61 | # The system should reboot on failure 62 | systemd.watchdog = { 63 | runtimeTime = "10s"; 64 | rebootTime = "30s"; 65 | }; 66 | 67 | boot.kernelParams = [ "panic=30" "boot.panic_on_fail" "quiet" ]; 68 | 69 | # Enable configuration on first boot 70 | systemd.additionalUpstreamSystemUnits = [ 71 | "systemd-firstboot.service" 72 | ]; 73 | 74 | } 75 | -------------------------------------------------------------------------------- /pkgs/composefs.nix: -------------------------------------------------------------------------------- 1 | { super, ... }: 2 | 3 | super.composefs.overrideAttrs (final: prev: { 4 | doCheck = false; 5 | }) 6 | -------------------------------------------------------------------------------- /pkgs/linux-firmware.nix: -------------------------------------------------------------------------------- 1 | { stdenv, lib 2 | , linux-firmware 3 | , fwDirs 4 | }: stdenv.mkDerivation { 5 | pname = "linux-firmware-minimal"; 6 | version = linux-firmware.version; 7 | buildCommand = lib.concatStringsSep "\n" ( 8 | [''mkdir -p "$out/lib/firmware"''] 9 | ++ (map (name: '' 10 | cp -r "${linux-firmware}/lib/firmware/${name}" "$out/lib/firmware/${name}" 11 | '') fwDirs)); 12 | } 13 | -------------------------------------------------------------------------------- /pkgs/openssh.nix: -------------------------------------------------------------------------------- 1 | { super, ... }: 2 | 3 | super.openssh.overrideAttrs (final: prev: { 4 | doCheck = false; 5 | doInstallCheck = false; 6 | dontCheck = true; 7 | }) 8 | -------------------------------------------------------------------------------- /pkgs/qemu.nix: -------------------------------------------------------------------------------- 1 | { super, ... }: 2 | 3 | (super.qemu_test.override { 4 | enableDocs = false; 5 | capstoneSupport = false; 6 | guestAgentSupport = false; 7 | tpmSupport = false; 8 | libiscsiSupport = false; 9 | usbredirSupport = false; 10 | canokeySupport = false; 11 | hostCpuTargets = [ "x86_64-softmmu" ]; 12 | }).overrideDerivation (old: { 13 | postFixup = '' 14 | rm -r $out/share/icons 15 | cp "${pkgs.OVMF.fd + "/FV/OVMF.fd"}" $out/share/qemu/ 16 | ''; 17 | configureFlags = old.configureFlags ++ [ 18 | "--disable-tcg" 19 | "--disable-tcg-interpreter" 20 | "--disable-docs" 21 | "--disable-install-blobs" 22 | "--disable-slirp" 23 | "--disable-virtfs" 24 | "--disable-virtfs-proxy-helper" 25 | "--disable-vhost-user-blk-server" 26 | "--without-default-features" 27 | "--enable-kvm" 28 | "--disable-tools" 29 | ]; 30 | }) 31 | -------------------------------------------------------------------------------- /pkgs/systemd-ukify.nix: -------------------------------------------------------------------------------- 1 | { super, ... }: 2 | 3 | super.systemd.override { 4 | withAcl = false; 5 | withAnalyze = false; 6 | withApparmor = false; 7 | withAudit = false; 8 | withEfi = true; 9 | withCompression = false; 10 | withCoredump = false; 11 | withCryptsetup = false; 12 | withRepart = false; 13 | withDocumentation = false; 14 | withFido2 = false; 15 | withFirstboot = false; 16 | withHomed = false; 17 | withHostnamed = false; 18 | withHwdb = false; 19 | withImportd = false; 20 | withIptables = false; 21 | withKmod = false; 22 | withLibBPF = false; 23 | withLibidn2 = false; 24 | withLocaled = false; 25 | withLogind = false; 26 | withMachined = false; 27 | withNetworkd = false; 28 | withNss = false; 29 | withOomd = false; 30 | withPam = false; 31 | withPasswordQuality = false; 32 | withPCRE2 = false; 33 | withPolkit = false; 34 | withPortabled = false; 35 | withQrencode = false; 36 | withRemote = false; 37 | withResolved = false; 38 | withShellCompletions = false; 39 | withSysusers = false; 40 | withSysupdate = false; 41 | withTimedated = false; 42 | withTimesyncd = false; 43 | withTpm2Tss = false; 44 | withUkify = true; 45 | withUserDb = false; 46 | withUtmp = false; 47 | withVmspawn = false; 48 | } 49 | -------------------------------------------------------------------------------- /pkgs/systemd.nix: -------------------------------------------------------------------------------- 1 | { super, ... }: 2 | 3 | super.systemd.override { 4 | withAcl = false; 5 | withApparmor = false; 6 | withDocumentation = false; 7 | withRemote = false; 8 | withShellCompletions = false; 9 | withVmspawn = false; 10 | } 11 | -------------------------------------------------------------------------------- /scripts/pack-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mkdir release 4 | 5 | cp -L nixlet-signed/* nixlet-insecure-unsigned/* release 6 | 7 | cat nixlet-signed/SHA256SUMS nixlet-insecure-unsigned/SHA256SUMS > release/SHA256SUMS 8 | -------------------------------------------------------------------------------- /scripts/sign-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eux 4 | 5 | mkdir nixlet-signed 6 | cp -L nixlet-unsigned/* nixlet-signed/ 7 | 8 | loopdev=$(sudo losetup -f) 9 | sudo losetup -P "$loopdev" nixlet-signed/*.img 10 | sudo mount "${loopdev}p1" /mnt -t vfat 11 | 12 | echo "$DB_KEY" > db.key 13 | echo "$DB_CRT" > db.crt 14 | 15 | sudo find nixlet-signed/ /mnt/ -name "*.efi" -type f -exec sbsign --key db.key --cert db.crt --output {} {} \; 16 | 17 | sudo mkdir -p /mnt/loader/keys/nixlet 18 | sudo cp keys/*.auth /mnt/loader/keys/nixlet/ 19 | 20 | sudo umount /mnt 21 | sudo losetup -d "$loopdev" 22 | 23 | cd nixlet-signed 24 | rm -f SHA256SUMS 25 | sha256sum *.efi *.usr *.verity > SHA256SUMS 26 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: 2 | pkgs.mkShellNoCC { 3 | packages = with pkgs; [ 4 | sbsigntool 5 | ]; 6 | } 7 | -------------------------------------------------------------------------------- /tests/common.nix: -------------------------------------------------------------------------------- 1 | { self, lib, pkgs, ... }: 2 | 3 | with import (pkgs.path + "/nixos/lib/testing-python.nix") { inherit pkgs; inherit (pkgs.hostPlatform) system; }; 4 | 5 | let 6 | 7 | nixos-lib = import (pkgs.path + "/nixos/lib") {}; 8 | qemu-common = import (pkgs.path + "/nixos/lib/qemu-common.nix") { inherit lib pkgs; }; 9 | 10 | in rec { 11 | 12 | makeSystem = extraConfig: 13 | (import (pkgs.path + "/nixos/lib/eval-config.nix")) { 14 | inherit pkgs lib; 15 | system = null; 16 | modules = [ 17 | (pkgs.path + "/nixos/modules/image/repart.nix") 18 | ../modules/image/repart-image-verity-store-defaults.nix 19 | ../modules/image/update-package.nix 20 | ../modules/image/initrd-repart-expand.nix 21 | ../modules/image/sysupdate-verity-store.nix 22 | ../modules/profiles/minimal.nix 23 | ../modules/profiles/image-based.nix 24 | ../modules/profiles/server.nix 25 | (pkgs.path + "/nixos/modules/profiles/qemu-guest.nix") 26 | { 27 | boot.kernelPackages = pkgs.linuxPackages_latest; 28 | users.allowNoPasswordLogin = true; 29 | system.stateVersion = lib.versions.majorMinor lib.version; 30 | system.image.id = lib.mkDefault "nixos-appliance"; 31 | system.image.version = lib.mkDefault "1"; 32 | networking.hosts."10.0.2.1" = [ "server.test" ]; 33 | boot.kernelParams = [ 34 | "x-systemd.device-timeout=10s" 35 | "console=ttyS0,115200n8" 36 | ]; 37 | # Use weak compression 38 | image.repart.compression.enable = false; 39 | boot.initrd.compressor = "zstd"; 40 | boot.initrd.compressorArgs = [ "-2" ]; 41 | } 42 | (pkgs.path + "/nixos/modules/testing/test-instrumentation.nix") 43 | extraConfig 44 | ]; 45 | }; 46 | 47 | makeImage = extraConfig: let 48 | system = makeSystem extraConfig; 49 | in "${system.config.system.build.finalImage}/${system.config.image.fileName}"; 50 | 51 | makeUpdatePackage = extraConfig: let 52 | system = makeSystem extraConfig; 53 | in "${system.config.system.build.updatePackage}"; 54 | 55 | makeImageTest = { name, image, script, httpRoot ? null, sshAuthorizedKey ? null }: let 56 | qemu = qemu-common.qemuBinary pkgs.qemu_test; 57 | flags = [ 58 | "-machine" "type=q35,accel=kvm,smm=on" 59 | "-m" "512M" 60 | "-drive" "if=pflash,format=raw,unit=0,readonly=on,file=${pkgs.OVMF.firmware}" 61 | "-drive" "if=pflash,format=raw,unit=1,readonly=on,file=${pkgs.OVMF.variables}" 62 | "-drive" "if=virtio,file=${mutableImage}" 63 | "-chardev" "socket,id=chrtpm,path=${tpmFolder}/swtpm-sock" 64 | "-tpmdev" "emulator,id=tpm0,chardev=chrtpm" 65 | "-device" "tpm-tis,tpmdev=tpm0" 66 | "-netdev" ("'user,id=net0" + (lib.optionalString (httpRoot != null) ",guestfwd=tcp:10.0.2.1:80-cmd:${pkgs.micro-httpd}/bin/micro_httpd ${httpRoot}") + "'") 67 | "-device" "virtio-net-pci,netdev=net0" 68 | ] ++ (lib.optionals (sshAuthorizedKey != null) [ 69 | "-smbios" ("'type=11,value=io.systemd.credential:ssh.authorized_keys.root=" + sshAuthorizedKey + "'") 70 | ]); 71 | flagsStr = lib.concatStringsSep " " flags; 72 | startCommand = "${qemu} ${flagsStr}"; 73 | mutableImage = "/tmp/linked-image.qcow2"; 74 | tpmFolder = "/tmp/emulated_tpm"; 75 | indentLines = str: lib.concatLines (map (s: " " + s) (lib.splitString "\n" str)); 76 | in makeTest { 77 | inherit name; 78 | nodes = { }; 79 | testScript = '' 80 | import os 81 | import subprocess 82 | 83 | subprocess.check_call( 84 | [ 85 | "qemu-img", 86 | "create", 87 | "-f", 88 | "qcow2", 89 | "-F", 90 | "raw", 91 | "-b", 92 | "${image}", 93 | "${mutableImage}", 94 | ] 95 | ) 96 | subprocess.check_call(["qemu-img", "resize", "${mutableImage}", "4G"]) 97 | 98 | os.mkdir("${tpmFolder}") 99 | os.mkdir("${tpmFolder}/swtpm") 100 | 101 | def start_tpm(): 102 | subprocess.Popen( 103 | [ 104 | "${pkgs.swtpm}/bin/swtpm", 105 | "socket", 106 | "--tpmstate", "dir=${tpmFolder}/swtpm", 107 | "--ctrl", "type=unixio,path=${tpmFolder}/swtpm-sock", 108 | "--tpm2" 109 | ] 110 | ) 111 | 112 | machine = create_machine("${startCommand}") 113 | 114 | try: 115 | '' + indentLines script + '' 116 | finally: 117 | machine.shutdown() 118 | ''; 119 | }; 120 | 121 | makeInteractiveTest = { image, qemu ? pkgs.qemu_kvm, OVMF ? pkgs.OVMF, runtimeShell ? pkgs.runtimeShell }: let 122 | qemuCommand = qemu-common.qemuBinary qemu; 123 | flags = [ 124 | "-m" "512M" 125 | "-drive" "if=pflash,format=raw,unit=0,readonly=on,file=${OVMF.firmware}" 126 | "-drive" "if=pflash,format=raw,unit=1,readonly=on,file=${OVMF.variables}" 127 | "-drive" "if=virtio,file=${mutableImage}" 128 | "-netdev" "'user,id=net0,hostfwd=tcp:127.0.0.1:2222-:22'" 129 | "-device" "virtio-net-pci,netdev=net0" 130 | "-serial" "stdio" 131 | ]; 132 | flagsStr = lib.concatStringsSep " " flags; 133 | startCommand = "${qemuCommand} ${flagsStr}"; 134 | mutableImage = "nixlet-disk.qcow2"; 135 | qemuImgCommand = "${qemu}/bin/qemu-img"; 136 | imgFlags = [ 137 | "create" 138 | "-f" "qcow2" 139 | "-F" "raw" 140 | "-b" "${image}" 141 | "${mutableImage}" 142 | "2G" 143 | ]; 144 | imgFlagsStr = lib.concatStringsSep " " imgFlags; 145 | imgCommand = "${qemuImgCommand} ${imgFlagsStr}"; 146 | in pkgs.writeScript "qemu-interactive-test" '' 147 | #!${runtimeShell} 148 | if [ ! -e "${mutableImage}" ]; then 149 | echo "Creating mutable image at ${mutableImage}" 150 | ${imgCommand} 151 | fi 152 | ${startCommand} 153 | ''; 154 | 155 | } 156 | -------------------------------------------------------------------------------- /tests/integration.nix: -------------------------------------------------------------------------------- 1 | { pkgs, self }: let 2 | 3 | lib = pkgs.lib; 4 | test-common = import ./common.nix { inherit self lib pkgs; }; 5 | 6 | sshKeys = import (pkgs.path + "/nixos/tests/ssh-keys.nix") pkgs; 7 | 8 | initialImage = test-common.makeImage { 9 | system.extraDependencies = [ sshKeys.snakeOilPrivateKey ]; 10 | }; 11 | 12 | in test-common.makeImageTest { 13 | name = "integration"; 14 | image = initialImage; 15 | sshAuthorizedKey = sshKeys.snakeOilPublicKey; 16 | script = '' 17 | start_tpm() 18 | machine.start() 19 | 20 | machine.wait_for_unit("multi-user.target") 21 | 22 | machine.succeed("systemd-creds --system list > /dev/console") 23 | machine.succeed("systemd-run -p ImportCredential=ssh.authorized_keys.root -P --wait systemd-creds cat ssh.authorized_keys.root") 24 | 25 | # Test SSH key provisioning functionality 26 | 27 | machine.succeed("[ -e /root/.ssh/authorized_keys ]") 28 | 29 | machine.wait_for_open_port(22) 30 | 31 | machine.succeed( 32 | "cat ${sshKeys.snakeOilPrivateKey} > privkey.snakeoil" 33 | ) 34 | machine.succeed("chmod 600 privkey.snakeoil") 35 | 36 | machine.succeed( 37 | "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i privkey.snakeoil root@127.0.0.1 true", 38 | timeout=30 39 | ) 40 | 41 | machine.systemctl("start network-online.target") 42 | machine.wait_for_unit("network-online.target") 43 | 44 | # Test podman functionality 45 | 46 | machine.succeed("tar cv --files-from /dev/null | podman import - scratchimg") 47 | machine.succeed("podman run --rm -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg true") 48 | ''; 49 | } 50 | -------------------------------------------------------------------------------- /tests/lib.nix: -------------------------------------------------------------------------------- 1 | test: 2 | { pkgs, self }: 3 | let nixos-lib = import (pkgs.path + "/nixos/lib") {}; 4 | in (nixos-lib.runTest { 5 | hostPkgs = pkgs; 6 | defaults.documentation.enable = false; 7 | node.specialArgs = { inherit self; }; 8 | imports = [ test ]; 9 | }).config.result 10 | -------------------------------------------------------------------------------- /tests/system-update.nix: -------------------------------------------------------------------------------- 1 | { pkgs, self }: let 2 | 3 | lib = pkgs.lib; 4 | test-common = import ./common.nix { inherit self lib pkgs; }; 5 | 6 | initialImage = test-common.makeImage { 7 | system.image.version = "1"; 8 | system.image.updates.url = "http://server.test/"; 9 | # The default usr-b is too small for uncompressed test images 10 | systemd.repart.partitions."32-usr-b" = { 11 | SizeMinBytes = lib.mkForce "1G"; 12 | SizeMaxBytes = lib.mkForce "1G"; 13 | }; 14 | }; 15 | 16 | updatePackage = test-common.makeUpdatePackage { 17 | system.image.version = "2"; 18 | system.image.updates.url = "http://server.test/"; 19 | }; 20 | 21 | in test-common.makeImageTest { 22 | name = "system-update"; 23 | image = initialImage; 24 | httpRoot = updatePackage; 25 | script = '' 26 | start_tpm() 27 | machine.start() 28 | 29 | machine.systemctl("start network-online.target") 30 | machine.wait_for_unit("network-online.target") 31 | 32 | machine.succeed("/run/current-system/sw/lib/systemd/systemd-sysupdate update") 33 | 34 | machine.shutdown() 35 | 36 | start_tpm() 37 | machine.start() 38 | 39 | machine.wait_for_unit("multi-user.target") 40 | 41 | machine.succeed('. /etc/os-release; [ "$IMAGE_VERSION" == "2" ]') 42 | 43 | machine.wait_for_unit("systemd-bless-boot.service") 44 | ''; 45 | } 46 | --------------------------------------------------------------------------------