- Remove unused flock from stage1 storePaths - Remove duplicate screen package in minimal.nix - Delete docs/superpowers/ planning artifacts (completed work) - Fix stage1 shell scripts to use full Nix store paths consistently (touch, rm, od, tr were bare; add missing ln/rm/touch to storePaths; remove unused echo from storePaths; sort alphabetically) - Remove unreachable else branch in flake.nix inspect app - Fix fragile build check in run-tests.sh (use exit code, not grep) - Rewrite README for current erofs/dm-verity/overlayfs architecture (was still describing old SquashFS/tmpfs extraction approach) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| configs | ||
| lib | ||
| modules | ||
| secureboot-test-keys | ||
| tests | ||
| tools | ||
| CLAUDE.md | ||
| flake.lock | ||
| flake.nix | ||
| README.md | ||
| run-tests.sh | ||
| test-boot.sh | ||
nixos-uki
This repo contains tooling to build a NixOS system configuration into a self-contained Unified Kernel Image that can be directly booted from UEFI. This is useful for example to build a full-featured linux rescue system to recover from boot problems.
Architecture
This project builds self-contained Unified Kernel Images (UKIs) that boot NixOS systems entirely from RAM:
-
UKI Structure: A UKI is a UEFI executable containing:
- Linux kernel
- Initramfs (with archive discovery scripts, tools, and dm-verity metadata)
- Kernel command line
- OS release information
- Optional: Secure Boot signatures
-
System Archive: The complete NixOS system is packaged as an erofs archive with:
- Full
/nix/storewith all dependencies - Nix database for package management
- System profile and configuration
- dm-verity hash tree for cryptographic integrity verification
- 4KiB identifier block for archive matching
- Full
-
Boot Process:
- UEFI firmware loads and executes the UKI
- Kernel boots with systemd in initrd (
boot.initrd.systemd.enable = true) - The
uki-system.serviceruns in initrd:- Phase 1: Checks EFI boot variables to identify the boot partition
- Phase 2: Scans existing block devices for the system archive
- Phase 3: Polls for hotplugged devices (60s timeout)
- Sets up dm-verity for integrity verification
- Mounts erofs as read-only lower layer
- Mounts tmpfs as writable upper layer
- Creates overlayfs root filesystem combining both
- systemd switches root to the overlayfs mount
-
Deployment Modes:
- Embedded Mode (unified): System archive appended to UKI after the PE data (single-file boot)
- Split Mode: UKI + separate
.erofsarchive on FAT filesystem- Enables >2GB systems (UEFI PE binary size limit workaround)
- Archive can be on USB drive, ESP partition, or any FAT volume
- Archives >4GB are automatically split into chunks for FAT32 compatibility
- Hotplug support: insert USB drive after boot starts
-
Features:
- Complete write access via tmpfs overlay on read-only erofs root
- Full Nix package management (nix-env, nix-shell)
- Variable system sizes (minimal rescue to full desktop)
- Hotplug device support when searching for split system archive
- Cryptographic verification of the system archive via dm-verity
- Optional Secure Boot signing
Usage
Building a UKI
Use the Nix flake to build a UKI from a NixOS configuration:
# Build the example minimal rescue system (embedded mode - single file)
nix build .#minimal
# Result: ./result/minimal.efi
# Build in split mode (UKI + separate archive)
nix build .#minimal-split
# Result: ./result/minimal.efi and ./result/minimal.erofs
# Build with Secure Boot signing
nix build .#minimal-signed
# The UKI is created at ./result/
ls -lh ./result/
Custom Configurations
Create your own NixOS configuration and build it as a UKI:
# my-config.nix
{ config, pkgs, ... }:
{
networking.hostName = "my-rescue";
environment.systemPackages = with pkgs; [
vim htop git
];
services.openssh.enable = true;
# ... more configuration
}
Build your custom UKI:
# flake.nix
{
inputs.uefi-rescue.url = "github:yourname/nixos-uki";
outputs = { self, uefi-rescue, nixpkgs }: {
packages.x86_64-linux.my-uki = uefi-rescue.lib.buildUKI {
nixosConfig = ./my-config.nix;
name = "my-rescue";
};
};
}
Build Options
The buildUKI function accepts:
nixosConfig(required): Path to NixOS configuration filename(optional): UKI name, default: "nixos-uki"splitImage(optional): Build split mode (UKI +.erofsfile), default: falseenableSecureBoot(optional): Enable Secure Boot signing, default: falsesecureBootKey(optional): Path to signing key (required if enableSecureBoot)secureBootCert(optional): Path to certificate (required if enableSecureBoot)compression(optional): erofs compression (zstd/lzma/lz4hc/deflate/lz4), default: "zstd"compressionLevel(optional): Compression level, default: max for chosen compressormaxSize(optional): Maximum UKI size in bytes, default: 4294967295 (4 GiB - 1, FAT32 limit)
Example with split mode:
buildUKI {
nixosConfig = ./my-config.nix;
name = "large-rescue";
splitImage = true; # Outputs both .efi and .erofs files
}
Example with Secure Boot:
buildUKI {
nixosConfig = ./my-config.nix;
name = "secure-rescue";
enableSecureBoot = true;
secureBootKey = ./db.key;
secureBootCert = ./db.crt;
}
Included Modules
The build system automatically includes these NixOS modules:
-
uki-system.nix: General NixOS options for all built images
- Enables Nix with flakes support
- Disables documentation (size optimization)
- Configures networking (DHCP, systemd-networkd)
- Serial console support (hvc0) for debugging
-
stage1.nix: Initramfs/stage1 boot logic
- systemd-initrd: Enables systemd in initrd for robust device management
- uki-system.service: Hotplug-aware archive discovery and mounting
- Three-phase search: EFI variables -> existing devices -> hotplug polling
- dm-verity integrity verification
- Overlayfs mount (erofs lower + tmpfs upper)
- Kernel patches for erofs compression and virtio console
find-archiveC utility for locating archives in PE files
-
stage2-activation.nix: Post-switch-root NixOS activation service
-
tmpfs-boot.nix: tmpfs-aware configuration
- Volatile storage for journals
- Persistent machine-id
- tmpfs temporary directories
You can override these settings in your configuration as needed.
Testing
Automated Tests
Run the test suite to verify images build and boot correctly:
./run-tests.sh
This script:
- Builds the minimal rescue image
- Boots it in QEMU using test-boot.sh
- Monitors the serial console for the root login prompt
[root@nixos-rescue:~]# - Automatically stops QEMU once autologin is detected
- Times out after 120 seconds if boot fails
Available tests:
minimal-boot: Verifies the minimal rescue image boots and autologin works
Manual Testing
To boot a UEFI binary interactively (such as the built UKI), use the provided test script:
./test-boot.sh <path-to-efi-binary> [<additional files...>]
The script performs a full UEFI boot process:
- Creates a temporary disk image with a GPT/FAT32 partition
- Copies the UKI to
/EFI/BOOT/bootx64.efi - Boots QEMU with OVMF (UEFI firmware) and KVM acceleration
- Connects the serial console to a UNIX socket and either a logfile or stdout
- Connects the QEMU monitor to a UNIX socket
The serial and monitor socket paths will be printed at launch.
Graphical mode
Same as above, but also opens a GTK window with access to the VGA console:
./test-boot.sh --graphical <path-to-efi-binary> [<additional files...>]
Logging output to file
./test-boot.sh --logfile <log-file-path> <path-to-efi-binary> [<additional files...>]
File Structure
.
├── flake.nix # Main entry point
├── lib/
│ ├── build-uki.nix # UKI orchestration (embedded & split modes)
│ └── system-archive.nix # erofs builder with Nix DB initialization
├── modules/
│ ├── uki-system.nix # Common NixOS options used in all built images
│ ├── stage1.nix # NixOS options specific to our stage1 initrd
│ ├── stage2-activation.nix # Post-switch-root activation service
│ └── tmpfs-boot.nix # tmpfs-aware configuration
├── tools/
│ └── find-archive.c # C utility to locate archives in PE files
├── configs/
│ ├── minimal.nix # Minimal rescue system
│ └── fat.nix # KDE desktop image
├── test-boot.sh # QEMU test script with serial console
├── run-tests.sh # Automated test suite
├── README.md # This file
└── CLAUDE.md # Assistant-specific documentation
Limitations
UEFI Binary Size Limit
UEFI binaries are 32-bit PE executables with a theoretical maximum size of 2GiB. In practice, many UEFI implementations have much smaller limits (often 512MB-1GB).
Solution: Use split mode to work around this limitation:
- The UKI contains only the kernel and minimal initrd
- The system archive is stored separately as a
.erofsfile - Archives >4GB are automatically split into FAT32-compatible chunks
- Both files can reside on the same FAT partition (ESP or USB drive)
This enables building full-featured systems that would exceed UEFI size limits in embedded mode.