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

Configuration

NAILS reads one strict TOML file, nails.toml. Every key is optional, and an empty file is a valid configuration that uses the defaults. What the session covers is not configurable. It is always the whole decoy root, plus a fixed policy for every other decoy filesystem.

  • By default NAILS reads config/nails.toml next to its own binary, after resolving symlinks. A binary at /mnt/data/nails reads /mnt/data/config/nails.toml, so the configuration always travels with the binary on the hidden volume.
  • --config <PATH> reads another file instead, which must exist. Without a file at the default location NAILS uses the defaults, and activate says so before it asks for confirmation.
  • nails init writes a commented starter file with every setting commented out, readable by root only. It keeps a file that is already there.
  • If you run NAILS from a build directory or /nix/store, set hidden_volume_root or pass --config, because the default location follows the binary.
  • Activation copies the effective configuration and the binary into RAM under /run/overlay-runtime. The backend watcher and the sleep guard read that copy during the session.

Never edit the file on the decoy. An editor leaves its own traces there, such as swap and backup files, ~/.viminfo and recent-file lists. Replace the configuration only while no session is active, and prepare it in one of two ways:

  • on the separate machine, before you copy it to the hidden volume
  • inside a hidden session, as nails.toml.new, which you move into place after the session
Terminal window
# inside a hidden session; the configuration directory is root's
sudoedit /mnt/data/config/nails.toml.new
sudo /mnt/data/nails config validate /mnt/data/config/nails.toml.new
sudo /mnt/data/nails --config /mnt/data/config/nails.toml.new config show

Then end the session. The next time you have mounted the hidden volume on the decoy, and before you activate, move the new file into place from a shell with history off:

Terminal window
sudo mv /mnt/data/config/nails.toml.new /mnt/data/config/nails.toml

Unknown keys, values of the wrong type and syntax errors are rejected with a one-line message that names the file, the line and the column. For a misspelled key it suggests the closest valid one, so a typo never falls back to a default silently.

On an invalid file, activate, check, status, verify, rehearse, reset-upper and nails config exit with code 3. deactivate and recover never refuse. They log one warning and carry on with defaults derived from the binary’s location. emergency reads no configuration at all.

nails init writes the file readable by root only, so run every NAILS command with sudo, the config commands below included. A command run without root that cannot read the file also exits with code 3 and names the full sudo line to run instead.

  • nails config path prints the resolved file and whether it exists.
  • nails config show prints every effective value and its source: default, file, cli or derived.
  • nails config show --example prints a commented example that lists every key.
  • nails config validate [PATH] checks a file strictly without using it.

path, show and validate also accept --json.

Every path is checked when the file is loaded, before anything is mounted.

  • Every path is absolute, with no . or .. segment and no newline.
  • hidden_volume_root is not / and contains no , or :, because overlay mount options use those characters as separators. Spaces are fine.
  • state_file_path and log_path lie inside hidden_volume_root.
  • No existing directory on the way to hidden_volume_root, or to a path inside it, is a symlink, because a symlink could send hidden data to the decoy disk. Configure the resolved path instead.
  • hidden_volume_root defaults to the directory that holds the binary. It is the root of the hidden volume. NAILS writes nothing under it until a volume of its own is mounted there. A folder, a subvolume or bind mount of a filesystem the computer uses, and a volume inside its storage do not count. Leave the key unset so the file keeps working wherever the volume is mounted.
  • state_file_path defaults to state.json in the hidden volume’s root. It is the activation state file and must lie inside the hidden volume.
  • log_path defaults to logs in the hidden volume’s root, the directory of nails.log. It must lie inside the hidden volume. --no-log-file turns the log file off for one run.
  • minimum_space_mb defaults to 500. The pre-flight check fails when the hidden volume has less free space than this, with no warning band before it.
  • rebuild_space_mb defaults to 4096, the free space needed to build a hidden NixOS system. A full build often writes several GB into the upper layer. The first entry also copies the hidden system’s whole closure onto the hidden volume, which is checked against the free space separately.
  • accept_persistent_decoy defaults to false, and activation then refuses a decoy that keeps a persistent journal or runs rsyslog, syslog-ng or auditd. What the decoy logs while it shuts down for the soft-reboot, and while it starts again afterwards, would survive on its disk. Set it to true to accept that risk. The --accept-persistent-decoy flag of activate and check overrides it.
  • show_opsec_reminders defaults to true. It shows reminders in nails status once the session has been active for 6, 12 and 24 hours.
  • nixos_flake is unset by default. It names the hidden NixOS system as a flake reference, such as /mnt/data/nixos#my-host, which is built into the hidden root’s store before the soft-reboot. activate --nixos-flake overrides it.

When nixos_flake is unset, activation takes the first of these:

  1. A flake in the hidden volume’s nixos directory, such as /mnt/data/nixos, built for this machine’s hostname.
  2. The decoy’s own flake in /etc/nixos, which activation refuses, even with --no-preflight. A system built from the decoy’s files would lack the hidden module and the hidden hardware configuration.
  3. The legacy hidden configuration: the decoy’s /etc/nixos/configuration.nix with the hidden hardware configuration in the upper layer, which imports the hidden module config/nixos/configuration.nix on the hidden volume.

A flake includes the hidden module only if it imports it. Write and lock a hidden flake inside a hidden session, which activate --overlay-only enters, never on the decoy. Keep it self-contained. A flake that imports /etc/nixos/configuration.nix reads the decoy’s files and its hardware configuration, swap included. nails check names the choice under “NixOS profile”. The guide to your first session shows how to write a hidden flake and how to apply a change to the running hidden system.

Naming the decoy’s own flake in nixos_flake or with --nixos-flake is refused the same way, whether as /etc/nixos#host, path:/etc/nixos, git+file:///etc/nixos, path:/etc?dir=nixos or through a link to it. So is a nixos directory on the hidden volume that links to it. Other spellings, such as a registry alias, are not recognized, so the refusal does not catch them.

On Debian and Ubuntu, nixos_flake has no effect. The hidden configuration there is whatever the upper layer’s /etc holds.

  • persona.mode defaults to "shared-home". The other value is "fresh-home".
  • persona.mask_inherited defaults to []. It lists decoy autostart entries and user units to hide from the hidden session, or ["*"] for all of them. Each value is a name as nails check lists it: the .desktop stem for an autostart entry, or the full unit name for a user unit.

The hidden session starts from the decoy root, so by default it runs as the decoy user with the decoy home.

  • shared-home (default). The hidden session works with the decoy home seen through the overlay. The decoy’s browser profiles, SSH and GPG keys, password-manager state and shell history are live there. Changes land in the upper layer, so the decoy home on disk is not modified.
  • fresh-home. At the first entry, activation creates an empty home in the upper layer, so nothing of the decoy home shows through. It stays the hidden home at every later entry. Activation must run through sudo for this mode.

A separate hidden account is planned for a later release. Test the mode you choose on your own machine before you rely on it.

[persona]
mode = "fresh-home"
mask_inherited = ["dropbox", "syncthing.service"]

A name that nails check does not list is masked too, as a user unit when it ends in a unit suffix such as .service, and as an autostart entry otherwise. nails check names such names, so a typo does not go unnoticed. Masking hides what runs in the hidden session. It does not roll back anything a shared account or sync client already sent.

Lingering and display-manager autologin are reported but not masked. A display manager set to log an account in automatically logs it in again at every switch, as at boot. The guide to your first session shows how to turn autologin off for the hidden session only.

  • backend_watch.enabled defaults to true. It runs the watcher that alerts you when the hidden volume fails.
  • backend_watch.poll_interval_secs defaults to 5, the seconds between the watcher’s probes.
  • backend_watch.auto_emergency defaults to false. Set it to true to opt in to an automatic emergency when the hidden volume fails.
  • backend_watch.debounce_secs defaults to 15, how long a failure must last before the automatic emergency fires.
  • session_mounts.alert defaults to true. It alerts you when a disk outside the hidden volume is mounted read-write during the session.
  • session_mounts.read_only defaults to false. Set it to true to make every disk the session does not use read-only for the session.

Activation copies the running binary and the effective configuration into RAM under /run/overlay-runtime. If the hidden volume fails during the session, sudo /run/overlay-runtime/control emergency still works, because the emergency needs nothing from the hidden volume.

The watcher runs from that copy and alerts you as soon as one of its probes of the hidden volume fails. It is alert-only by default, so a brief stall can never end a session on its own. With backend_watch.auto_emergency = true, an emergency runs by itself once a failure has lasted backend_watch.debounce_secs seconds, and returns to the decoy like nails emergency.

The alerts go to the account logged in on the machine’s own screen. While nobody is logged in there, no alert can be shown. While the watcher runs, a disk mounted over the hidden root, or over one of its parents, always raises its own alert, “Disk mounted over a folder in use”, whatever session_mounts.alert says. No watcher runs when backend_watch.enabled, session_mounts.alert and session_mounts.read_only are all false.

Turn on session_mounts.read_only unless you need to write to an external disk in the hidden session. A mount then comes up read-only and writes fail, and the way back makes the disks writable again. A root user in the hidden session can still set a disk writable and mount it, and the alert then fires. The setting guards against automounts and mistakes.

  • sleep_policy.mode defaults to "warn". The other values are "block" and "teardown".

Suspending during a session keeps the whole hidden session in RAM, including the open encryption mapping and its key, and on resume the session is still live. Fix the host first. Set the decoy’s lid switch to lock and turn off the desktop’s automatic suspend, as Prepare the decoy describes. The hidden session starts from the decoy’s /etc, so it inherits the fix unless the hidden configuration changes it. The sleep-policy pre-flight check warns when the host would suspend an active session, and when a closed lid is set to ignore, which keeps the session running and unlocked behind the lid.

sleep_policy.mode adds a backstop that runs from the RAM copy for the life of the session:

  • warn (default) only shows the pre-flight warning. Nothing runs during the session, and a closed lid or an idle timeout suspends the session as logind is configured to.
  • block holds an inhibitor, so logind refuses a suspend that is asked for or that an idle timeout would start. When a closed lid would put the machine to sleep, it also takes the lid switch from logind. A closed lid then does nothing at all, neither suspend nor lock the screen. A lid switch set to lock, ignore or poweroff is left to logind and keeps working. A laptop kept running with its lid shut can overheat, so still set the lid switch to lock.
  • teardown powers the machine off when it is about to sleep, within logind’s InhibitDelayMaxSec window of 5 seconds by default, instead of letting it sleep with the session in RAM. This covers a closed lid too.

The inhibitors appear in systemd-inhibit --list under neutral names, and nothing is written to the decoy disk.

The configuration reference is checked against the code and describes every key, the path rules and the fstab policy in full.