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

Prepare the decoy

The decoy is the installed system, which should stay mundane and plausible. NAILS needs it to be an amnesic decoy: one that keeps no durable record of the hidden session. That means a volatile journal, no syslog or audit daemon, no disk swap and core dumps kept off the disk.

The decoy runs before the soft-reboot into the hidden session and again after the way back. A decoy that writes logs, audit records, crash dumps or swap to its own disk records the activation command, its own shutdown and start around each session, and what it imports from the kernel ring buffer. The overlay keeps the hidden session off the decoy only if the decoy itself keeps no durable record of it.

Before activation, the decoy-persistence check inspects the decoy. It only reads. It never rotates or deletes the decoy’s own logs, because destroyed log history is itself a tell.

Activation is refused when the decoy has any of these:

  • a persistent systemd journal
  • a classic syslog daemon, rsyslog or syslog-ng
  • the audit daemon, auditd

--accept-persistent-decoy on activate and check, or accept_persistent_decoy = true in nails.toml, overrides these three refusals. Use it only if you accept that what the decoy logs around each soft-reboot may survive on its disk. It never covers the swap check, which refuses active disk swap. RAM-backed zram is allowed.

Activation is also refused while a line in the decoy’s /etc/crypttab or /etc/fstab opens or mounts the hidden volume or a device it is stored on. No flag overrides this. Open the hidden volume by hand each time, and remove such a line long before the next use, since the decoy’s configuration history can keep it. On NixOS, activation is refused as well while a .nix file in /etc/nixos names the hidden volume or NAILS.

These are reported as warnings and never block activation:

  • a root filesystem mounted without noatime or relatime, which records read access
  • crash dump channels that can write memory to the decoy disk or to firmware storage: a kernel.core_pattern that writes to the disk, systemd-coredump without both Storage=none and ProcessSizeMax=0, pstore records or an armed kdump
  • nixos-upgrade.timer on NixOS, which rebuilds the decoy on its own schedule
  • sudo’s own log file or I/O logging
  • lid or idle settings that would suspend an active session, and a closed lid set to ignore, which leaves the session running and unlocked

Set these in the decoy configuration, not in the hidden one.

services.journald.storage = "volatile";
fileSystems."/".options = [ "noatime" ];
boot.kernelParams = [ "init_on_free=1" "page_poison=1" ];
systemd.coredump.settings.Coredump = {
Storage = "none";
ProcessSizeMax = 0;
};
services.logind.settings.Login = {
HandleLidSwitch = "lock";
HandleLidSwitchExternalPower = "lock";
HandleLidSwitchDocked = "ignore";
IdleAction = "ignore";
};
  • Keep the root on a disk. A tmpfs root, such as impermanence on /, is refused. A wiped-on-boot /var or /home on its own tmpfs is fine.
  • Leave swapDevices empty, including in hardware-configuration.nix, and do not configure boot.resumeDevice or a resume kernel parameter.
  • Do not enable services.rsyslogd, services.syslog-ng or security.auditd.
  • If your nixpkgs does not install soft-reboot.target and systemd-soft-reboot.service, declare them with the module nails check prints. The hidden configuration needs them too.
  • Keep /etc/nixos free of anything that names the hidden volume. See What /etc/nixos must not name.

NAILS reads the journald, logind and systemd-coredump settings the way systemd does, from the main file and every drop-in, so a drop-in that sets Storage=persistent is caught too.

Where systemd-coredump is installed, keep it running with both settings rather than turning it off. Storage=none alone still writes each core to /var/lib/systemd/coredump on the decoy disk while it is processed. Without systemd-coredump, the kernel writes a core file into the crashing program’s working directory.

init_on_free=1 page_poison=1 make the kernel wipe freed memory. The hidden session runs on the same kernel, so this covers it too. The memory-wipe check reports whether the booted command line sets init_on_free=1.

On NixOS, the suspicious-nails-reference check reads every .nix file in /etc/nixos, comments included. It refuses activation, naming the file, the line and the text it found, when a line holds:

  • the hidden volume’s mount point as a whole path, such as /mnt/data, or the units systemd names after it, such as mnt-data.mount
  • its device, such as /dev/mapper/data, or the unit systemd names after it
  • the storage it is on, by device path, udev link, UUID or label, unless the decoy’s own volumes sit on it too
  • its mapping name where a line opens the volume, such as a boot.initrd.luks.devices entry, a crypttab line or a cryptsetup open command, in a comment as in code
  • text NAILS writes during a session, or the word nails

Ordinary configuration passes, such as the mapping name used as an ordinary word or a directory like /var/lib/postgresql/data.

The check reads text and does not evaluate Nix, so it misses a name built with a let binding or an interpolation. Keep such lines out yourself. It also reads only .nix files. Keep notes, scripts and a README in /etc/nixos free of these names too. Where /etc/nixos is a git repository, as a flake often is, its history counts as well. A line removed from a file stays in the commit that added it.

Run nixos-generate-config only while the hidden volume is closed. It writes an entry for every mounted filesystem.

If /etc/nixos holds a flake, activation refuses to build it, --no-preflight included, because that build would leave out the hidden module and the hidden hardware configuration. The refusal does not recognize every way of naming that flake, such as a registry alias, so never name it with nixos_flake or --nixos-flake. Write a self-contained hidden flake on the hidden volume instead, one that imports the hidden module and reads none of the decoy’s files. Write and lock it inside a hidden session, never on the decoy. nails activate --overlay-only enters one with the decoy’s own system, without building the hidden configuration. See the nixos_flake setting.

Suspending during a session keeps the whole hidden session in RAM, including the open encryption mapping and its key, behind the hidden session’s lock screen. Whoever unlocks that screen is in the live hidden session. By default logind suspends when the lid closes, and desktops add their own idle suspend.

  • Set the lid switch to lock, as above. ignore also keeps the host awake, but it leaves the session unlocked behind a closed lid, and the sleep-policy check warns about it.
  • Turn off automatic suspend in the desktop’s power settings too, such as GNOME’s or KDE’s, since logind does not govern it.
  • The hidden session starts from the decoy’s /etc, so it keeps these settings unless the hidden configuration changes them.
  • Consider sleep_policy.mode in nails.toml as a backstop. block prevents sleep during a session. When a closed lid would sleep, it also takes the lid switch from logind, so a closed lid then does nothing, not even lock the screen. A lid set to lock stays with logind and keeps locking. teardown powers the machine off when it is about to sleep. See the sleep policy settings.

Neither mode replaces the lid switch setting. Keep it at lock, so a closed lid locks the screen. A laptop kept running with its lid shut can overheat.

Running sudo is itself logged, with the invoking user, the command and its full path. On an amnesic decoy that record stays in RAM. Two kinds of sudo settings write to the disk on their own. Remove both from /etc/sudoers and /etc/sudoers.d/:

  • Defaults logfile=, which writes every sudo command line to a file
  • every I/O logging option, such as log_input, log_output and the LOG_INPUT and LOG_OUTPUT tags, which record whole terminal sessions, keystrokes and output included

NAILS cannot suppress sudo’s own logging without tampering with the decoy, which would itself be a tell. Keep the binary at a neutral path on the hidden volume.

The unlock and mount commands must be typed on the decoy, before NAILS runs. Run them, and nails activate, in a shell with history off:

Terminal window
set +o history

Activation kills the decoy’s interactive shells as its last step, so none of them can write the activation command to a history file on exit. A shell that writes history after every command has already written it. The incremental-history-flush check warns about that, for zsh’s INC_APPEND_HISTORY or history -a in bash’s PROMPT_COMMAND.

NAILS does not configure the decoy’s shells. To give an interactive shell nowhere to write, set HISTSIZE=0 and HISTFILESIZE=0 in the decoy’s shell profile and point the history files at /dev/null. In the shell you unlock from, do not turn history back on or run history -a or history -w. Each of them writes the commands you typed to the decoy disk.

On an amnesic decoy, the decoy’s records from around each switch stay in RAM: the sudo line of the activation, the hidden build’s progress, the decoy’s shutdown before the switch and its start after the way back. They are in the runtime journal below /run/log/journal, which the hidden session’s journald writes to as well.

The way back deletes that journal whole, the decoy’s own records from before the session included. The decoy’s journal then starts at the return, without the kernel’s boot messages, while uptime and dmesg still go back to the boot. Until the machine is powered off, anyone with a shell can see that the journal starts mid-uptime. Power off after a session.

With a persistent journal, accepted with --accept-persistent-decoy, these records are on the decoy disk and stay there, with a gap where the hidden session ran.

Use the decoy normally between sessions, so its logs account for the hours the machine ran. Otherwise some records point to use the logs do not explain. The clock synchronization stamp and timer stamps can date a session, and the drive counts its own power-on hours, which no software can change. A /var on a wiped-on-boot tmpfs loses the two stamps at every boot. nails verify reports them as notes, so you can see what an examiner would see.