Skip to content

Configuration Propagation

This guide explains how configuration values flow from the host to VMs in Ghaf using the globalConfig and hostConfig patterns via specialArgs.

In earlier versions of Ghaf, VMs accessed host configuration directly:

# OLD PATTERN - DON'T USE
{ configHost }:
{
config = {
# Direct reference to host config
ghaf.profiles.debug.enable = configHost.ghaf.profiles.debug.enable;
networking.hostName = configHost.ghaf.networking.vms.gui-vm.name;
};
}

This caused several problems:

  1. Tight coupling: VM modules depended on specific host config structure
  2. Circular dependencies: Host needs VM config, VM needs host config
  3. Hard to test: Can’t evaluate VM modules without full host config
  4. Difficult downstream: Hard to override or extend
  5. Unclear data flow: Where does each value come from?

Ghaf now uses specialArgs to inject configuration into VM modules:

# NEW PATTERN
{ config, lib, globalConfig, hostConfig, ... }:
{
config = {
# Clear separation of concerns
ghaf.profiles.debug.enable = globalConfig.debug.enable;
networking.hostName = hostConfig.vmName;
};
}

When evaluating NixOS modules with evalModules, specialArgs provides values that are automatically available to all modules:

lib.nixosSystem {
specialArgs = {
# These become function parameters in ALL modules
globalConfig = { /* ... */ };
hostConfig = { /* ... */ };
inputs = { /* ... */ };
};
modules = [ ... ];
}

Unlike regular module arguments, specialArgs:

  • Don’t need to be explicitly passed between modules
  • Are available everywhere in the module tree
  • Don’t create dependencies between modules

globalConfig contains settings that should be identical across all VMs.

CategoryExamplesWhy Global?
Debug settingsdebug.enableSame debug state everywhere
Developmentssh.daemon.enable, debug.tools.enableConsistent dev experience
Logginglogging.enable, logging.listenerCentralized log collection
GIVCgivc.enable, givc.debugInter-VM communication config
Securitysecurity.audit.enableConsistent security posture
Featuresfeatures.fprint, features.wifiHardware feature assignments

In VM modules, globalConfig is available as a function parameter:

modules/microvm/sysvms/guivm-base.nix
{
config,
lib,
pkgs,
globalConfig, # Injected via specialArgs
...
}:
{
config = {
# Access global settings
ghaf.profiles.debug.enable = globalConfig.debug.enable;
ghaf.development = {
ssh.daemon.enable = globalConfig.development.ssh.daemon.enable;
debug.tools.enable = globalConfig.development.debug.tools.enable;
};
logging = {
inherit (globalConfig.logging) enable listener;
client.enable = globalConfig.logging.enable;
};
ghaf.givc = {
enable = globalConfig.givc.enable;
debug = globalConfig.givc.debug;
};
};
}

At the host level, set ghaf.global-config:

# Using a predefined profile
{ lib, ... }:
{
ghaf.global-config = lib.ghaf.profiles.debug;
}
# Or customize
{ lib, ... }:
{
ghaf.global-config = lib.ghaf.mkGlobalConfig "debug" {
storage.encryption.enable = true;
features.bluetooth.enable = true;
};
}
# Or set directly
{
ghaf.global-config = {
debug.enable = true;
development.ssh.daemon.enable = true;
logging.enable = true;
givc.enable = true;
features = {
wifi.enable = true;
wifi.targetVms = [ "net-vm" ];
};
};
}
globalConfig = {
# Debug mode
debug.enable = false;
# Development settings
development = {
ssh.daemon.enable = false;
debug.tools.enable = false;
nix-setup.enable = false;
};
# Logging
logging = {
enable = false;
listener = {
address = ""; # Auto-populated from admin-vm IP
port = 9999;
};
server.endpoint = "";
};
# Security
security.audit.enable = false;
# GIVC
givc = {
enable = false;
debug = false;
};
# Services
services = {
power-manager.enable = false;
performance.enable = false;
};
# Storage
storage = {
encryption.enable = false;
storeOnDisk = false;
};
# Shared memory
shm = {
enable = false;
serverSocketPath = "";
};
# IDS VM
idsvm.mitmproxy.enable = false;
# Platform (auto-populated from host)
platform = {
buildSystem = "x86_64-linux";
hostSystem = "x86_64-linux";
timeZone = "UTC";
};
# Features (hardware feature assignments)
features = {
fprint = { enable = true; targetVms = [ "gui-vm" ]; };
yubikey = { enable = true; targetVms = [ "gui-vm" ]; };
brightness = { enable = true; targetVms = [ "gui-vm" ]; };
wifi = { enable = true; targetVms = [ "net-vm" ]; };
audio = { enable = true; targetVms = [ "audio-vm" ]; };
bluetooth = { enable = true; targetVms = [ "audio-vm" ]; };
};
};

hostConfig contains settings that are different for each VM but derived from the host configuration.

CategoryExamplesWhy Per-VM?
IdentityvmName, vmTypeEach VM has unique name
Networkingipv4, mac, hostsDifferent IPs per VM
Usersuser configurationMay vary by VM role
Hardwarekernel, passthroughDevice-specific
GIVCcliArgs, enableTlsVM-specific IPC config

In VM modules, hostConfig is available as a function parameter:

modules/microvm/sysvms/netvm-base.nix
{
config,
lib,
hostConfig, # Injected via specialArgs
...
}:
{
config = {
# VM identity
networking.hostName = hostConfig.vmName;
# Network settings specific to this VM
networking.interfaces.ethint0.ipv4.addresses = [{
address = hostConfig.networking.thisVm.ipv4;
prefixLength = 24;
}];
# User configuration (may differ per VM)
users.users = hostConfig.users.managed;
# Reference other VMs' network info
networking.hosts = hostConfig.networking.hosts;
};
}

Use lib.ghaf.vm.mkHostConfig to create hostConfig for a VM:

# In profile or target
hostConfig = lib.ghaf.vm.mkHostConfig {
config = config; # Host NixOS config
vmName = "gui-vm";
extraConfig = {
# Additional settings if needed
customOption = "value";
};
};
hostConfig = {
# VM identity
vmName = "gui-vm";
vmType = "guivm"; # vmName with dashes removed
# Kernel configuration (if VM type has one)
kernel = { /* ghaf.kernel.<vmType> or null */ };
# QEMU configuration (if VM type has one)
qemu = { /* ghaf.qemu.<vmType> or null */ };
# Hardware passthrough
passthrough = {
qemuExtraArgs = [ /* extra QEMU args for this VM */ ];
vmUdevExtraRules = "..."; # Udev rules for passthrough
};
# Host filesystem paths
sharedVmDirectory = "/...";
# Boot configuration
microvmBoot = {
enable = bool;
};
# Hardware devices
hardware = {
devices = { /* ghaf.hardware.devices */ };
};
# Common namespace (killswitch, etc.)
common = { /* ghaf.common config from host */ };
# User configuration
users = { /* ghaf.users config from host */ };
# Reference services
reference = {
services = { /* ghaf.reference.services */ };
};
# Networking
networking = {
# All VM network configs (for /etc/hosts, etc.)
hosts = {
"gui-vm" = { ipv4 = "192.168.100.3"; mac = "..."; };
"net-vm" = { ipv4 = "192.168.100.1"; mac = "..."; };
# ...
};
# This VM's specific config
thisVm = {
ipv4 = "192.168.100.3";
mac = "02:00:00:00:00:03";
# ...
};
};
# GIVC configuration
givc = {
cliArgs = "--addr ...";
enableTls = bool;
};
# Security settings
security = {
sshKeys = { /* SSH key configuration */ };
};
# AppVM configurations (for launcher generation)
appvms = { /* enabledVms with derived values */ };
# GUI VM applications
guivm = {
applications = [ /* GUI VM local applications */ ];
};
};

Profiles create the specialArgs when defining VM bases:

modules/profiles/laptop-x86.nix
{ config, lib, inputs, ... }:
{
options.ghaf.profiles.laptop-x86.guivmBase = lib.mkOption {
type = lib.types.unspecified;
readOnly = true;
default = lib.nixosSystem {
# This is where specialArgs are created
specialArgs = lib.ghaf.vm.mkSpecialArgs {
inherit lib inputs;
globalConfig = config.ghaf.global-config;
hostConfig = lib.ghaf.vm.mkHostConfig {
inherit config;
vmName = "gui-vm";
};
};
modules = [
inputs.self.nixosModules.guivm-base
];
};
};
}

This helper creates the specialArgs attrset:

# Usage
specialArgs = lib.ghaf.vm.mkSpecialArgs {
lib = extendedLib; # Required: lib with ghaf functions
inputs = flakeInputs; # Required: flake inputs
globalConfig = config.ghaf.global-config; # Required
hostConfig = hostConfigValue; # Optional: from mkHostConfig
extraArgs = { myArg = value; }; # Optional: additional args
};
# Result
{
lib = extendedLib;
inputs = flakeInputs;
globalConfig = { /* ... */ };
hostConfig = { /* ... */ }; # If provided
myArg = value; # If extraArgs provided
}
# Module received entire host config
{ configHost }:
{
# Had to navigate host config structure
ghaf.profiles.debug.enable = configHost.ghaf.profiles.debug.enable;
# Tight coupling to host config layout
networking.hostName = configHost.ghaf.networking.vms.gui-vm.name;
# Repeated everywhere
ghaf.development.ssh.daemon.enable =
configHost.ghaf.development.ssh.daemon.enable;
}

Problems:

  • Module depends on full host config structure
  • Changes to host config break VM modules
  • Hard to test VM modules in isolation
  • Downstream can’t easily override values
# Module receives only what it needs
{ globalConfig, hostConfig, ... }:
{
# Clear, direct access
ghaf.profiles.debug.enable = globalConfig.debug.enable;
# VM-specific from hostConfig
networking.hostName = hostConfig.vmName;
# Same pattern everywhere
ghaf.development.ssh.daemon.enable =
globalConfig.development.ssh.daemon.enable;
}

Benefits:

  • Module only depends on documented interface
  • Host config changes don’t break VM modules
  • Easy to test with mock globalConfig/hostConfig
  • Downstream can customize at well-defined points
# GOOD: Setting applies to all VMs
ghaf.logging.enable = globalConfig.logging.enable;
# BAD: Hardcoded per-VM
ghaf.logging.enable = true;

2. Use hostConfig for VM-Specific Settings

Section titled “2. Use hostConfig for VM-Specific Settings”
# GOOD: Each VM gets its own value
networking.hostName = hostConfig.vmName;
# BAD: Hardcoded
networking.hostName = "gui-vm";
# GOOD: Centrally managed, can be reassigned
config = lib.mkIf (lib.ghaf.features.isEnabledFor globalConfig "fprint" "gui-vm") {
services.fprintd.enable = true;
};
# BAD: Hardcoded in one VM
config = {
services.fprintd.enable = true; # What if we want it elsewhere?
};
# GOOD: Safe access with fallback
networking.timeZone = globalConfig.platform.timeZone or "UTC";
# RISKY: Might fail if not set
networking.timeZone = globalConfig.platform.timeZone;
modules/microvm/sysvms/my-vm-base.nix
{
config,
lib,
pkgs,
globalConfig, # Uses: debug.enable, logging.*, givc.*
hostConfig, # Uses: vmName, networking.thisVm
...
}:
ConceptPurposeScopeAccess
globalConfigSettings same for all VMsGlobalglobalConfig.setting
hostConfigSettings different per VMPer-VMhostConfig.setting
specialArgsInjection mechanismAll modulesFunction parameters

The separation provides:

  • Clear data flow: Know where each value comes from
  • Loose coupling: Modules don’t depend on host structure
  • Testability: Mock specialArgs for testing
  • Extensibility: Override at well-defined points

For more details: