Use my wayland-proxy-virtwl fork. Unconditionally requests server-side decorations even if the client doesn't want them. Tested with firefox, works there. |
||
|---|---|---|
| docs/plans | ||
| modules | ||
| rootfs-nixos | ||
| .gitignore | ||
| CLAUDE.md | ||
| flake.lock | ||
| flake.nix | ||
| README.md | ||
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
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, run 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
xfce.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:
Run command in VM (recommended)
vm-run <name> <command>
Example: vm-run banking firefox
This is the primary way to interact with VMs. The command:
- Connects to the VM's socket at
$XDG_RUNTIME_DIR/qubes-lite/<name>.sock - Triggers socket activation to start the VM if not running
- 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