A NixOS-based Qubes-like app isolation system based on VMs
Find a file
Davíð Steinn Geirsson eb4e2ffcaa Add VM configuration options and migrate to --block
- Rename `disks` to `additionalDisks` with structured format
  (path, readOnly, enableDiscard, blockSize, devIdentifier, useDirect)
- Add custom boot options: rootDisk, kernel, initramfs, rootDiskReadonly
- Add kernelParams for extra kernel command line options
- Add gpu option (default: "context-types=cross-domain:virgl2")
- Add sharedDirectories for crosvm --shared-dir
- Add global crosvmLogLevel option (default: "info")
- Add --name argument to crosvm set to VM name
- Migrate deprecated --disk/--rwdisk to --block format
- Switch flake to nixos-unstable channel

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 19:09:51 +00:00
modules Add VM configuration options and migrate to --block 2026-02-04 19:09:51 +00:00
rootfs-nixos Move disposable/idleTimeout to runtime kernel parameters 2026-02-04 18:16:15 +00:00
.gitignore Revert server-side decoration forcing 2026-02-04 16:03:00 +00:00
CLAUDE.md Add VM configuration options and migrate to --block 2026-02-04 19:09:51 +00:00
flake.lock Add VM configuration options and migrate to --block 2026-02-04 19:09:51 +00:00
flake.nix Add VM configuration options and migrate to --block 2026-02-04 19:09:51 +00:00
README.md Update documentation 2026-02-04 17:59:20 +00:00

qubes-lite

A lightweight virtualization system inspired by Qubes OS. Runs isolated VMs using crosvm (Chrome OS VMM) with different security domains.

All credit goes to Thomas Leonard (@talex5), who wrote the wayland proxy and got all this stuff working: https://gitlab.com/talex5/qubes-lite

This is a hacky side project. If you need a serious and secure operating system, use Qubes.

Comparison to Qubes

The main benefits compared to Qubes are:

  • Fast, modern graphics. Wayland calls are proxied to the host.
  • Better power management. Qubes is based on Xen, and its support for modern laptop power management is significantly worse than linux.
  • NixOS-based declarative VM config.

The cost for that is security. Qubes is laser-focused on security and hard compartmentalisation. This makes it by far the most secure general-purpose operating system there is.

Ways in which we are less secure than Qubes (list is not even remotely exhaustive):

  • The host system is not isolated from the network at all. The user needs to use discipline to not access untrusted network resources from the host. Even if they do, handling VM network traffic makes the host attack surface much larger.
  • There is no attempt to isolate the host system from hardware peripherals. Qubes segregates USB and network into VMs.
  • Currently clipboard is shared between host and all VMs. This will be fixed at some point, the plan is to implement a two-level clipboard like Qubes.
  • Proxying wayland calls means the attack surface from VM to host is way larger than Qubes' raw framebuffer copy approach.
  • Probably a million other things.

If you are trying to defend against a determined, well-resourced attacker targeting you specifically then you should be running Qubes.

Quick Start

Add to your flake inputs:

{
  inputs.qubes-lite.url = "git+https://git.dsg.is/davidlowsec/qubes-lite.git";
}

Import the module and configure VMs in your NixOS configuration:

{ config, pkgs, ... }: {
  imports = [ inputs.qubes-lite.nixosModules.default ];

  programs.qubes-lite = {
    enable = true;
    user = "david";
    vmNetwork = "172.16.200.0/24";
    natEnable = true;
    natInterface = "eth0";

    guestPrograms = with pkgs; [
      firefox
      xfce4-terminal
    ];

    nixosVms = [
      {
        id = 3;
        name = "banking";
        memory = 4096;
        cpus = 4;
        disposable = true;  # Auto-shutdown when idle
        idleTimeout = 120;  # 2 minutes
      }
      {
        id = 5;
        name = "shopping";
        memory = 2048;
        cpus = 2;
        disposable = true;
      }
      {
        id = 7;
        name = "personal";
        memory = 4096;
        cpus = 4;
        network = false;  # Offline VM
      }
    ];
  };
}

Configuration Options

programs.qubes-lite

Option Type Default Description
enable bool false Enable qubes-lite VM management
user string required User who owns TAP interfaces and runs VMs
vmNetwork string "172.16.200.0/24" Network CIDR for VM networking
natEnable bool false Enable NAT for VM internet access
natInterface string "" External interface for NAT (required if natEnable)
guestPrograms list of packages [] Packages included in all VM rootfs images
guestConfig attrs {} NixOS configuration applied to all VM rootfs images
nixosVms list of VM configs [] List of NixOS-based VMs to create

VM Configuration (nixosVms items)

Option Type Default Description
id int required VM ID (odd number 3-255). Used for IP and vsock CID
name string required VM name for scripts and TAP interface
memory int 1024 Memory allocation in MB
cpus int 2 Number of virtual CPUs
network bool true Enable networking for this VM
disposable bool false Auto-shutdown when idle (after idleTimeout seconds)
idleTimeout int 60 Seconds to wait before shutdown (when disposable=true)
disks list of strings [] Additional disk paths (qcow2 or block devices)
guestPrograms list of packages [] VM-specific packages
guestConfig attrs {} VM-specific NixOS configuration

Commands

After rebuilding NixOS, the following commands are available:

vm-run <name> <command>

Example: vm-run banking firefox

This is the primary way to interact with VMs. The command:

  1. Connects to the VM's socket at $XDG_RUNTIME_DIR/qubes-lite/<name>.sock
  2. Triggers socket activation to start the VM if not running
  3. Sends the command to the guest

Start VM for debugging

vm-start-debug <name>

Starts crosvm directly in the foreground, bypassing socket activation. Useful for debugging VM boot issues since crosvm output is visible.

Stop a VM

vm-stop <name>

Sends systemctl poweroff to the guest for graceful shutdown.

Socket activation

VMs start automatically on first access via systemd socket activation:

# Check socket status
systemctl --user status qubes-lite-banking.socket

# Check VM service status
systemctl --user status qubes-lite-banking-vm.service

Sockets are enabled by default and start on login.

Network Architecture

IP Addressing

VMs use /31 point-to-point links:

  • VM IP: <network-base>.<id> (e.g., 172.16.200.3)
  • Host TAP IP: <network-base>.<id-1> (e.g., 172.16.200.2)

The host TAP IP acts as the gateway for the VM.

ID Requirements

VM IDs must be:

  • Odd numbers (3, 5, 7, 9, ...)
  • In range 3-255
  • Unique across all VMs

This ensures non-overlapping /31 networks and valid vsock CIDs.

NAT

When natEnable = true, the module configures:

  • IP forwarding (net.ipv4.ip_forward = 1)
  • NAT masquerading on natInterface
  • Internal IPs set to VM IPs

Advanced Configuration

Global and Per-VM NixOS Config

Use guestConfig at the top level for configuration shared by all VMs, and per-VM guestConfig for overrides:

programs.qubes-lite = {
  # Applied to all VMs
  guestConfig = {
    services.openssh.enable = true;
    users.users.user.extraGroups = [ "wheel" ];
  };

  nixosVms = [
    {
      id = 3;
      name = "dev";
      # Per-VM config overrides global
      guestConfig = {
        # Mount persistent home directory
        fileSystems."/home/user" = {
          device = "/dev/vdb";
          fsType = "ext4";
        };
      };
      disks = [ "/dev/mapper/main-dev-home" ];
    }
    {
      id = 5;
      name = "banking";
      # This VM gets global guestConfig but disables SSH
      guestConfig = {
        services.openssh.enable = false;
      };
    }
  ];
};

Per-VM guestConfig is deep-merged with global guestConfig, with per-VM values taking precedence on conflicts.

Offline VMs

For sensitive data that should never touch the network:

{
  id = 13;
  name = "vault";
  network = false;
  memory = 2048;
}

Disposable VMs

VMs that auto-shutdown when idle to save memory:

{
  id = 9;
  name = "untrusted";
  memory = 4096;
  disposable = true;   # Enable auto-shutdown
  idleTimeout = 60;    # Shutdown 60 seconds after last command exits
}

The guest runs an idle watchdog that monitors for active command sessions. When no commands are running and idleTimeout seconds have passed since the last activity, the VM shuts down cleanly.

Building

# Build the default rootfs image
nix build .#rootfs-nixos

Architecture

Each NixOS VM gets:

  • A dedicated qcow2 rootfs image with packages baked in
  • Overlayfs root (read-only ext4 lower + tmpfs upper)
  • wayland-proxy-virtwl for GPU passthrough
  • Socket-activated command listener (vsock-cmd.socket + vsock-cmd@.service)
  • Optional idle watchdog for disposable VMs
  • Systemd-based init

The host provides:

  • Persistent TAP interfaces via NixOS networking
  • NAT for internet access (optional)
  • Socket activation (qubes-lite-<name>.socket)
  • Proxy services that wait for guest vsock and forward commands
  • CLI tools: vm-run, vm-start-debug, vm-stop