Skip to content
NAILS on GitHub
These docs describe NAILS 0.3, published so far as the release candidate v0.3.0-rc.3. They do not apply to v0.2.0, the latest stable release. Download and verify NAILS 0.3

How it works

NAILS runs the hidden session in one OverlayFS overlay over the whole decoy root, and enters and leaves it with a systemd soft-reboot. This page is the operator’s view of that design. The architecture overview explains why the session takes the whole root and what that costs.

An overlay merges two layers into one directory tree:

  • The lower layer is the decoy root. It stays read-only.
  • The upper layer is on the hidden volume, in its root/upper directory. With the hidden volume at /mnt/data, as in these docs’ examples, that is /mnt/data/root/upper. Changes made in the hidden session land there, from files and logs to shell history and package builds.

The hidden session sees both layers merged. A decoy file it changes is first copied up in full, so the decoy’s own copy stays as it was. The upper persists across sessions, on the hidden volume.

nails activate builds the overlay at /run/nextroot and enters it with systemctl soft-reboot, which needs systemd 254 or newer. systemd stops every decoy program, switches root into the overlay and starts a fresh userspace there. The hidden desktop, its applications and its services all start inside the overlay. After the switch, no program in the session has a path to the decoy’s own filesystem. The few writes that go around the mount layer, to a disk you mount yourself during the session or to EFI variables, are the exception.

The kernel, its modules, the device-mapper mappings and /run survive the switch. That is why the hidden volume has to be one the kernel manages, such as a dm-crypt mapping or a loop device. A FUSE mount would die with the programs at the soft-reboot.

An earlier NAILS design overlaid the running decoy directory by directory. It had to find every process that could still write to the decoy, and a writer it missed went on writing there without any sign. Overlays set up at boot need no such search, but they need a full reboot and leave an enabler on the decoy, such as a package, a boot entry or a kernel command-line flag. With a soft-reboot, every process of the hidden session starts inside the overlay, and nothing is installed on the decoy.

Both transitions log you out. Every program closes, and the hidden session and later the decoy start at their own login screens. NAILS logs nobody in, but a display manager set to log an account in automatically logs it in again after each switch, as at boot. The operator guide shows how to turn autologin off for the hidden session alone.

The root overlay covers every directory on the root filesystem. Every other filesystem the decoy’s fstab names gets a fixed decision before the switch:

Decoy filesystemIn the hidden session
The ESP and /bootBound read-only
A filesystem the decoy mounts read-onlyBound read-only
Any other mounted local filesystem, such as a separate /homeAn overlay of its own, with its upper on the hidden volume
Swap, a local filesystem mounted at boot but not now, a network filesystemMasked, so the hidden session never uses them

The units that write to the ESP and /boot at every start, systemd-boot-random-seed and on Debian and Ubuntu grub-common and grub-initrd-fallback, are masked in the hidden session as well. They run as usual on the decoy. The masks live in /run, so they are gone with the session.

nails check prints the decision for every entry, and the configuration reference has the full table. A disk you mount by hand during the session is outside this policy and is written directly. See Your first session.

On NixOS, the hidden session can run its own declarative NixOS configuration, built into the overlay and entered in the same soft-reboot. The hidden system’s store paths end up on the hidden volume, like everything else the session writes through the decoy’s mounted filesystems. That narrows what an offline examination of the decoy can find. The hidden volume, its encryption and any plausible deniability come from the storage backend you choose, such as a VeraCrypt hidden volume, and NAILS adds nothing to the backend’s claim.

The way back mounts the decoy root again by its device, so the root has to be ext4, xfs, btrfs or f2fs on a disk. A tmpfs root, such as impermanence on /, is refused, and so is a ZFS root. A wiped-on-boot /var or /home on its own tmpfs is fine. Prepare the decoy lists the other settings a NixOS decoy needs.

With a hidden NixOS configuration, activation builds the hidden system into the overlay’s store before the switch. It builds the first of these that applies:

  1. a flake named with --nixos-flake or the nixos_flake key
  2. a flake in nixos/ on the hidden volume, built for the machine’s hostname
  3. the legacy configuration, which is the decoy’s /etc/nixos with the hidden hardware configuration that nails init placed in the upper

nails check names the choice under “NixOS profile”. The legacy configuration imports the hidden module, config/nixos/configuration.nix on the hidden volume, by itself. A flake has to import it, or the hidden system lacks its settings.

The build reads the decoy store through the read-only lower layer and writes every new path to the hidden volume. The first entry also copies the hidden system’s whole closure onto the hidden volume. For a desktop that is several GB, and on a slow or encrypted backend it takes minutes. Later entries copy only new paths, and the decoy can collect its garbage or rebuild between sessions without breaking the hidden system.

No bootloader is installed and the system profile does not change, so the boot default always remains the decoy. The hidden system has to run on the decoy’s kernel. nails activate --overlay-only skips the build and enters with the decoy’s own system plus the hidden /etc and home.

A decoy whose /etc/nixos is a flake needs a hidden flake. Activation refuses to build the decoy’s own flake, --no-preflight included, because the result would lack the hidden module and the hidden hardware configuration. The refusal catches the usual ways of naming that flake. A few spellings, such as a registry alias, are not recognized.

Write and lock the hidden flake inside a session entered with nails activate --overlay-only, never on the decoy. Make it import the hidden module, and keep it self-contained, so that it reads none of the decoy’s files.

Inside the hidden session, sudo nixos-rebuild test applies changes to the legacy configuration. With a hidden flake, name it:

Terminal window
sudo nixos-rebuild test --flake /mnt/data/nixos#<host> --impure

<host> is the decoy’s hostname, even when the network-identity module gives the session another one. For a flake named with nixos_flake, pass that reference instead. On a flake decoy, a plain nixos-rebuild test would build the decoy’s own flake, without the hidden module.

Never use switch or boot. They try to install a bootloader into the read-only ESP and fail. The next activation builds the hidden system again from the hidden module or your flake, so keep that source in step with what you test.

The same root overlay, without a build step. The hidden configuration is whatever the upper’s /etc holds. Change it from inside the hidden session like any system configuration, with apt, edits in /etc or systemctl enable.

On Ubuntu 26.04, chrony and CUPS fail in the hidden session. After the way back, the decoy lists chrony as failed until its next full reboot, which hints that a soft-reboot happened. Reboot fully after a session there. A plain systemctl soft-reboot into another root, without NAILS, breaks chrony the same way.

nails deactivate runs in the hidden session. It mounts the decoy root again at /run/nextroot and makes the decoy’s other mounts again below it, as the decoy had them when the session began. It then marks the hidden volume for closing and soft-reboots. If a mount recorded at entry cannot be made again, deactivate refuses, and nails emergency powers off instead. Some decoy mounts, such as a FUSE mount or a disk mounted by hand, are not made again. As after a plain soft-reboot, they are gone until they are mounted again.

The orderly shutdown stops every hidden program. The last step before the switch:

  • scrubs the top of /run of what the hidden session added, while state deeper down stays, such as NetworkManager’s per-link records
  • deletes the runtime journal, which the decoy shares with the hidden session
  • puts back the decoy’s name-server records below /run/resolvconf
  • gives the kernel back the decoy’s module loader, hotplug helper and firmware path, which a hidden NixOS system points at its own store
  • restores the decoy’s lines in libmount’s utab
  • moves the decoy journal’s kernel-message cursor past the session’s records

The decoy then starts at its own login screen, or at its desktop if its display manager logs an account in automatically. The switch itself unmounts the hidden volume and closes its encryption mapping. Network links, addresses and routes that the session set up stay, as What a soft-reboot leaves explains.

nails emergency takes the same way back but kills every hidden program at once. See Ending a session.

Everything NAILS needs lives on the hidden volume. The session plan, the transition step and the runtime units live in RAM under /run, and the way back removes them. The ESP is read-only in the hidden session, and NAILS never installs a boot entry, an initrd change, a unit or a binary on the decoy. Such an enabler would be evidence in itself, even with the hidden data encrypted.

A line in the decoy’s /etc/crypttab or /etc/fstab that opens or mounts the hidden volume is an enabler too. Activation refuses to run while one exists, and no flag overrides that check.

End-to-end tests hash the ESP’s file listing and capture the EFI variables before activation and after deactivation, and check that nothing changed beyond what a plain reboot changes.

A soft-reboot keeps the kernel running. On the decoy disk it leaves none of the hidden session’s files, with one exception. If a network link the session created is still there at the return, the decoy’s NetworkManager records it, with the time, in /var/lib/NetworkManager/timestamps as soon as the decoy starts. Powering off after the return comes too late for that record.

Until the machine is powered off, RAM also keeps:

  • the kernel ring buffer (dmesg), with the hidden session’s kernel messages
  • the soft-reboot counter, two per session, which systemctl show reports on systemd 256 and newer
  • the same boot ID and continuous uptime across both transitions
  • the links, addresses, routes and VPN tunnels the session set up, with the hidden NetworkManager’s per-link records below /run/NetworkManager
  • a random MAC address from the optional network-identity snippet, which can stay on the decoy’s network device, and on a decoy without /etc/hostname the hidden hostname
  • on a volatile decoy, a journal that starts at the return, while uptime and dmesg still go back to the boot
  • the decoy’s root and its other mounts, made again with new mount IDs that findmnt -o ID shows

Memory the hidden session freed is not wiped either.

Before you deactivate, take down every connection and VPN tunnel you brought up and delete every link you created by hand with ip link delete, since ip link set down keeps it. Ending the session with nails deactivate --poweroff works too, because it powers off before the decoy’s NetworkManager starts. End a session that used the network-identity snippet with --poweroff in any case. Power off after a session whenever RAM residue matters, and leave the machine off for a few minutes.

The decoy’s own shutdown before the session and its start after it write what an ordinary shutdown and boot write, such as the random seed and a wtmp record. On Debian and Ubuntu, the way back keeps the login and boot records reading as one clean power cycle. The login records written after the return still carry process numbers that continue from the decoy’s boot, where a fresh boot starts them low. A session that ends with --poweroff has no return, so it writes none of them.

The residual channels in the threat model list what the overlay cannot cover.

What the hidden session shares with the decoy

Section titled “What the hidden session shares with the decoy”

The overlay separates files, not machines. The hidden session runs on the decoy’s kernel and starts with the decoy’s machine ID, hostname, MAC addresses and network profiles. The kernel keeps network interfaces and addresses across both switches, so what the session sets up there stays in the decoy until power-off, as described above.

By default it also runs as the decoy user, in the decoy home seen through the overlay. The decoy’s browser profiles, SSH and GPG keys and history are live in the hidden session, and changes to them land in the upper. The fresh-home persona gives the hidden session an empty home instead.

Every service the decoy enables starts again in the hidden session unless the hidden configuration turns it off, and in the default persona so do the decoy account’s autostart entries and user services. nails check lists the ones that need review. What the session sends through a shared account or sync client stays on the remote side, and a decoy sync client can pull it back onto the decoy disk later. See persona modes and the threat model.