No description
  • Nix 83.9%
  • Shell 11%
  • Awk 5.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-08-18 17:13:27 -04:00
checks Order the database reset so every crash residue recovers 2026-07-30 12:52:41 +00:00
docs initial public release 2026-07-09 18:21:20 -04:00
hooks initial public release 2026-07-09 18:21:20 -04:00
modules Order the database reset so every crash residue recovers 2026-07-30 12:52:41 +00:00
.gitignore initial public release 2026-07-09 18:21:20 -04:00
disk.nix initial public release 2026-07-09 18:21:20 -04:00
flake.lock flake.lock: update nixpkgs-unstable, profiles 2026-08-18 17:13:27 -04:00
flake.nix Fix the store replay database reset and termination guarantee 2026-07-30 08:03:59 +00:00
LICENSE Add LICENSE (GPL-3.0-or-later) 2026-07-17 05:40:41 +00:00
README.md Split the guest modules and add the microvm runtime 2026-07-29 20:01:03 +00:00
setup.sh initial public release 2026-07-09 18:21:20 -04:00

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 (profiles repo)
  • per-VM secrets and SSH keys (profiles repo)
  • host NixOS config and provisioning scripts (nexus repo)
  • 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.hostPlatform defaults to x86_64-linux
  • systemd-boot + EFI
  • networkmanager (hostname-mode=none)
  • fix-home-ownership activation 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 passwordless
  • nix.settings.experimental-features, nix.settings.trusted-users, nix.extraOptions = "netrc-file = /etc/nix/netrc"
  • /etc/gitconfig credential store helper (lets root fetch private HTTPS flake inputs)
  • system.stateVersion = "25.11"
  • the allod.vm.guestRuntimes exclusivity assertion

nixosModules.microvmGuest

Imports the pinned upstream microvm.nixosModules.microvm plus the shared base, and bounds it:

  • microvm.hypervisor = "qemu"; microvm.qemu.machine stays at the upstream platform default, and a non-QEMU runner with a non-empty microvm.credentialFiles map 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.kernelModules gains qemu_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.shares must be empty, microvm.storeOnDisk must resolve true, and the runner carries no bin/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 = true
  • systemd.user.tmpfiles.rules — creates the %t/gnupg socket dir at login (sshd's RemoteForward target)
  • programs.ssh.enable = true + enableDefaultConfig = false
  • profiles — consumer VM configs and profile modules (consumes this repo as a flake input)
  • nexus — host NixOS config and the host-side provisioning scripts
  • secrets — identity and credential-schema examples consumed by profiles

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.
  • profiles pins all flake inputs through vm via follows.

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>:

  1. creates the libvirt VM (new-vm, booting the installer ISO);
  2. runs nixos-anywhere (disko + install), injecting the pre-generated VM SSH host key via --extra-files so agenix can decrypt secrets on first boot;
  3. 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.