Transfer Nix builds to an air-gapped machine.
Air-gapped Nix has three workflows:
| Workflow | Imports | Airgap builds |
|---|---|---|
nix-airgap |
CA paths and trusted outputs | Some |
| Trusted import | Whole closure | Little or none |
| Source/FOD-only (“rebuild the world”) | Inputs only | Almost everything |
Use nix-airgap from a connected client to transfer cached outputs and build only
cache misses on the air-gapped host. The cache.nixos.org signing key is trusted by
default. Select the cache list per transfer with repeated --trusted-cache flags or
TRUSTED_CACHES; either replaces the default cache list.
The SSH account need not be a Nix trusted-user: nix-airgap transfers only paths a
non-trusted account can add—CA paths such as FODs and literal source inputs (e.g. ./.), the evaluated .drv graph, and
outputs from explicitly selected trusted caches.
If the importing user is trusted by the air-gapped daemon, build normally and copy
the complete closure to removable media, for example with a file:// binary cache:
nix build .#whatever
nix copy --to file:///mnt/usb/cache .#whatever
# On the air-gapped machine:
nix copy --from file:///mnt/usb/cache --no-check-sigs /nix/store/...-resultThis trust model makes nix-airgap unnecessary.
Transfer flake inputs and FOD/CA dependencies, then build the remaining graph with substituters disabled. Bootstrap material remains necessary; see the FOD-only copy script.
Build the CLI with Nix:
nix run .#default -- INSTALLABLE SSH_HOSTExample:
SSH_CONFIG=./vm/ssh_config \
nix run .#default -- .#demo airgapINSTALLABLE may be a flake installable or a .drv path. SSH_HOST is resolved by
OpenSSH. Set SSH_CONFIG when a custom SSH config is needed.
Remote builds do not create a result link by default. Persist one explicitly:
nix run .#default -- .#demo airgap \
--remote-out-link /home/test/resultThe command prints resulting store paths with --print-out-paths.
Use --dry-run to plan locally without SSH, transfers, or builds:
nix run .#default -- .#demo airgap --dry-runThe plan reports trusted-cache, FOD, and remote-build frontiers:
--show-cache-frontierlists each path and source cache.--show-fod-frontierlists each FOD withvalid caormissing cametadata.--show-build-frontierlists derivations left for the remote machine.
Use them together when needed:
nix run .#default -- .#demo airgap --dry-run \
--show-cache-frontier --show-fod-frontier --show-build-frontierConfigure caches with either repeated flags or an environment variable:
nix run .#default -- .#demo airgap \
--trusted-cache https://cache.nixos.org \
--trusted-cache https://example-cache.invalid
TRUSTED_CACHES='https://cache.nixos.org https://example-cache.invalid' \
nix run .#default -- .#demo airgapThe planner walks the derivation graph and checks configured caches via asynchronous, bounded HTTP narinfo probes.
- Trusted-cache outputs and healthy FODs (
ca != null) transfer in batch withnix copy. - Broken FOD metadata (
ca = null) uses bounded concurrentnix store addoperations, reconstructing CA registration from.drvmetadata and verifying each output path. - The evaluated
.drvgraph and sources for remote-built DRVs transfer withnix copy; remote Nix builds the output.
The FOD workaround avoids forwarding untrusted cache signatures. It addresses the Cachix metadata issue documented in cachix/cachix#740.
SSH_HOST is passed to both OpenSSH and the Nix ssh-ng store URL. Set
SSH_CONFIG to an OpenSSH config file when the host needs custom connection settings;
the tool passes it to ssh and exports it through NIX_SSHOPTS for Nix store operations.
SSH_CONFIG=/absolute/path/to/ssh_config nix run .#default -- .#demo airgapEnter the flake development shell, then use uv:
nix develop
uv lock
uv run nix-airgap --helpThe production package uses uv2nix and pyproject-nix. The default package is
also available as .#airgap.
The included vm/ and .#demo are development fixtures for testing the transfer flow.
To exercise the demo from the client VM:
ssh -F ssh_config client
cd /work/vm
SSH_CONFIG=./ssh_config nix run .#default -- .#demo airgap --remote-out-link /tmp/demoThen inspect the result on the air-gapped VM:
ssh -F ssh_config airgap
ls -l /tmp/demoChecks:
nix flake check
python3 -m py_compile src/airgap/cli.pynix/vm.nix defines two VMs:
airgap: hostnameairgap, isolated network, SSH forwarded to host port13964.client: hostnameclient, full internet access, SSH forwarded to host port13965.
Prepare shared SSH key from repository root:
chmod 600 vm/vm-keyStart each VM from vm/, in separate terminals. The VM launcher changes into a
temporary directory, so the config uses $OLDPWD/.. to preserve the live repository share:
cd vm
nix run ..#nixosConfigurations.airgap.config.system.build.vmcd vm
nix run ..#nixosConfigurations.client.config.system.build.vmBoth VMs use useBootLoader = true and a 20 GiB root image. boot.growPartition
expands the backing image's root partition and filesystem on first boot. Restart existing
VMs with the new build; delete old qcow files only if they predate the 20 GiB image.
The repository working tree is shared from vm/.. through a raw 9p mount at
/run/shared-raw, then exposed at /work through bindfs as test:users.
Host ownership remains determined by the user launching the VM; no host UID is configured.
From vm/, connect using aliases in ssh_config:
ssh -F ssh_config airgap
ssh -F ssh_config clientThe shared airgap SSH alias works from both host and client: host connections use
127.0.0.1:13964; client connections use the host gateway at 10.0.2.2:13964.