- Nix 83.9%
- Shell 11%
- Awk 5.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| checks | ||
| docs | ||
| hooks | ||
| modules | ||
| .gitignore | ||
| disk.nix | ||
| flake.lock | ||
| flake.nix | ||
| LICENSE | ||
| README.md | ||
| setup.sh | ||
vm
NixOS framework for dev VMs. Exports the shared guest modules for both
supported runtimes, the shared Home Manager module, the re-exported microvm.nix
host module, and the agenix app. Per-VM profiles (host configs, secrets, keys)
live in the companion profiles repo.
Repo purpose
This repo owns:
- the shared guest base (
modules/guest-base.nix) — account, sudo, sshd, Nix and git settings - the disk-installed libvirt guest (
nixosModules.qemuGuest) — disk layout + boot - the microvm.nix guest (
nixosModules.microvmGuest) — tmpfs root, no bootloader, writable Nix store - the sole microvm.nix pin, re-exported for the host as
nixosModules.microvmHost - shared Home Manager module (
homeModules.base) — GPG and SSH base config - the agenix app re-export (
apps.x86_64-linux.agenix)
This repo does not own:
- per-VM NixOS configs (
profilesrepo) - per-VM secrets and SSH keys (
profilesrepo) - host NixOS config and provisioning scripts (
nexusrepo) - which runtime a machine uses, its credentials, volume placement, or addressing (
inventory,archetypes,nexus) - editor and other user preferences supplied by a consumer profile
Exported outputs
| Output | Type | Description |
|---|---|---|
nixosModules.qemuGuest |
NixOS module | Disk-installed libvirt guest: QEMU guest profile, disko disk layout, systemd-boot, NetworkManager, plus the shared base |
nixosModules.microvmGuest |
NixOS module | microvm.nix guest: upstream tmpfs root, no disko, no bootloader, writable Nix store on /nix/var/nix, plus the shared base |
nixosModules.microvmHost |
NixOS module | The pinned upstream microvm.nixosModules.host, re-exported so the host repo needs no microvm.nix input |
homeModules.base |
Home Manager module | GPG base config, gnupg socket tmpfile, SSH enable |
apps.x86_64-linux.agenix |
app | re-exports the agenix CLI |
checks.<system>.guest-module-contracts |
derivation | Reads both guests' generated artefacts and rejects every contract mutation |
checks.<system>.microvm-store-reconcile |
derivation | Runs the Nix-database reconciliation pass against synthetic registration data |
Runtimes
Both runtimes are first-class and a machine selects exactly one. qemuGuest is
unchanged: the disko GPT layout, systemd-boot, nixos-anywhere provisioning and
guest-local agenix all keep working. microvmGuest is the new alternative and
does not compose disko, disk.nix, the nixpkgs QEMU disk profile, or a
bootloader.
Composing both is an evaluation error rather than a precedence accident: each
runtime module appends its own name to the internal
allod.vm.guestRuntimes list, and the shared base asserts that exactly one is
present.
Which runtime a machine gets is not decided here. inventory owns the runtime
fact and archetypes selects the module from it.
nixosModules.qemuGuest
Includes:
disko.nixosModules.disko+./disk.nix— GPT layout, 512M EF00 ESP on/boot, ext4 on/modules/guest-base.nix(below)- QEMU guest kernel modules;
nixpkgs.hostPlatformdefaults tox86_64-linux systemd-boot+ EFInetworkmanager(hostname-mode=none)fix-home-ownershipactivation script — re-chowns the home dir after the nixos-anywhere bootstrap
modules/guest-base.nix
Shared by both runtimes, and not exported on its own:
openssh(StreamLocalBindUnlink, no password auth)users.${username}with host SSH key(s) authorized, sudo wheel passwordlessnix.settings.experimental-features,nix.settings.trusted-users,nix.extraOptions = "netrc-file = /etc/nix/netrc"/etc/gitconfigcredential store helper (lets root fetch private HTTPS flake inputs)system.stateVersion = "25.11"- the
allod.vm.guestRuntimesexclusivity assertion
nixosModules.microvmGuest
Imports the pinned upstream microvm.nixosModules.microvm plus the shared base,
and bounds it:
microvm.hypervisor = "qemu";microvm.qemu.machinestays at the upstream platform default, and a non-QEMU runner with a non-emptymicrovm.credentialFilesmap fails evaluation, because the-fw_cfg name=opt/io.systemd.credentials/...transport is QEMU-only- the upstream tmpfs root: no partition or disk device resolves
/or/boot, no EFI or systemd-boot installer, no disko device boot.initrd.kernelModulesgainsqemu_fw_cfg, which upstream does not load, so an initrd-time credential consumer can read its credential; blacklisting it is rejected- zero shared folders:
microvm.sharesmust be empty,microvm.storeOnDiskmust resolve true, and the runner carries nobin/virtiofsd-run - one coherent writable Nix store, described below
Machine facts stay outside this module. It declares no address, no credential value, no volume image path, and no host path.
The microvm writable Nix store
At the pinned revision, upstream mounts /nix/store straight from the
read-only erofs store disk and disables nix-daemon and its socket whenever
microvm.writableStoreOverlay is null, so a dev guest built that way boots
unable to build anything. modules/microvm-store.nix therefore requires one
persistent /nix/var/nix volume and sets
microvm.writableStoreOverlay = "/nix/var/nix", which puts the overlay
upper/work directories in the same volume as Nix's database, profiles and
gcroots so they move and roll back together.
Upstream microvm.registerClosure is deliberately off. Its registration file
is an extra path on the read-only store disk that is not registered by its own
contents, so guest garbage collection hides it behind a persistent overlay
whiteout and a later rollback can no longer read it. In its place the runner
carries allod.regInfo=<store-disk registration> on the kernel command line —
with the string context discarded, as upstream discards it for init=, so the
closure-info reference graph does not enter every host runner closure — and
modules/microvm-store-replay.sh runs from boot.postBootCommands, before
systemd, to cache that registration under /nix/var/nix, reconcile the
persistent database against the current merged store, load the registration and
verify the running toplevel. Stage 2 throws that child's exit status away in
both initrd flavours, so the script terminates the boot itself on failure.
This is an empirical specification. Its acceptance evidence is the nested store-lifecycle boots owned by the archetypes milestone that first selects a dev microvm; where a measurement contradicts the module, the measurement wins.
Neither guest module enables unfree packages. Consumer profiles that ship
unfree tools (e.g. claude-code, codex) set nixpkgs.config.allowUnfree = true
themselves.
Checks
nix flake check --print-build-logs
nix build .#checks.x86_64-linux.guest-module-contracts --print-build-logs
guest-module-contracts reads generated artefacts rather than module source:
both guests' /etc/fstab and bootspec, their boot-loader installers, disko's
partitioning script, the microvm guest's generated initrd module list and
modprobe blacklist, the generated boot.postBootCommands file and its position
in stage 2, and the built QEMU runner's own command line. Every assertion and
scanner it adds is paired with a sabotage fixture that must fail, including a
deliberate both-runtime-modules composition.
homeModules.base
Includes:
programs.gpg.enable = true+settings.no-autostart = truesystemd.user.tmpfiles.rules— creates the%t/gnupgsocket dir at login (sshd's RemoteForward target)programs.ssh.enable = true+enableDefaultConfig = false
Related repos
profiles— consumer VM configs and profile modules (consumes this repo as a flake input)nexus— host NixOS config and the host-side provisioning scriptssecrets— identity and credential-schema examples consumed byprofiles
Cloning
git clone https://forge.anarch.diy/allod/vm.git
Architecture
- The host machine runs libvirt/QEMU. Dev VMs are the main development environments.
- VMs are accessed over SSH from the host. No shared folders.
- All shared VM configuration (disk layout, system boilerplate, home boilerplate) lives here.
- Per-VM configuration (packages, forge identity, secrets) lives in
profiles. profilespins all flake inputs throughvmviafollows.
Provisioning
Provisioning is host-side and owned by nexus. Its commands are built into the
nexus provisioning-scripts package and installed on the host through
nexus.homeModules.host, so they run from the Nix store — editing a checkout
does not change the installed command until the host is rebuilt.
At a high level, provision-vm-from-host <vm-name>:
- creates the libvirt VM (
new-vm, booting the installer ISO); - runs
nixos-anywhere(disko + install), injecting the pre-generated VM SSH host key via--extra-filesso agenix can decrypt secrets on first boot; - boots the VM from disk and bootstraps it — clones the workspace and runs
nixos-rebuild switch --flake .../profiles#<vm-name>.
VM specs (sizing, networking, repo lists, rebuild policy) come from inventory.
See the nexus docs for the full provisioning and host-key/token lifecycle.
SSH keys
| Key | Location | Purpose |
|---|---|---|
host |
~/.ssh/host on host |
Host identity: SSH to VMs; age decryption of all secrets |
<vm-forge-key> |
~/.ssh/<vm-forge-key> on host |
Dev VM forge SSH key; copied to VM during bootstrap |
<vm-name> host key |
/etc/ssh/<vm-name> on VM |
VM SSH host key; pre-generated, injected by nixos-anywhere before first boot |
Adding a new VM
See the profiles README for the full guide. At the framework level, adding a
new VM requires zero changes to this repo — all per-VM configuration goes in
profiles.