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.
Where the file lives
Section titled “Where the file lives”- By default NAILS reads
config/nails.tomlnext to its own binary, after resolving symlinks. A binary at/mnt/data/nailsreads/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, andactivatesays so before it asks for confirmation.nails initwrites 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, sethidden_volume_rootor 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.
Changing it
Section titled “Changing it”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
# inside a hidden session; the configuration directory is root'ssudoedit /mnt/data/config/nails.toml.newsudo /mnt/data/nails config validate /mnt/data/config/nails.toml.newsudo /mnt/data/nails --config /mnt/data/config/nails.toml.new config showThen 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:
sudo mv /mnt/data/config/nails.toml.new /mnt/data/config/nails.tomlChecking it
Section titled “Checking it”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 pathprints the resolved file and whether it exists.nails config showprints every effective value and its source:default,file,cliorderived.nails config show --exampleprints 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.
Path rules
Section titled “Path rules”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_rootis not/and contains no,or:, because overlay mount options use those characters as separators. Spaces are fine.state_file_pathandlog_pathlie insidehidden_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.
Paths and space
Section titled “Paths and space”hidden_volume_rootdefaults 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_pathdefaults tostate.jsonin the hidden volume’s root. It is the activation state file and must lie inside the hidden volume.log_pathdefaults tologsin the hidden volume’s root, the directory ofnails.log. It must lie inside the hidden volume.--no-log-fileturns the log file off for one run.minimum_space_mbdefaults to500. The pre-flight check fails when the hidden volume has less free space than this, with no warning band before it.rebuild_space_mbdefaults to4096, 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.
Decoy posture and reminders
Section titled “Decoy posture and reminders”accept_persistent_decoydefaults tofalse, 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 totrueto accept that risk. The--accept-persistent-decoyflag ofactivateandcheckoverrides it.show_opsec_remindersdefaults totrue. It shows reminders innails statusonce the session has been active for 6, 12 and 24 hours.
nixos_flakeis 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-flakeoverrides it.
When nixos_flake is unset, activation takes the first of these:
- A flake in the hidden volume’s
nixosdirectory, such as/mnt/data/nixos, built for this machine’s hostname. - 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. - The legacy hidden configuration: the decoy’s
/etc/nixos/configuration.nixwith the hidden hardware configuration in the upper layer, which imports the hidden moduleconfig/nixos/configuration.nixon 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 modes
Section titled “Persona modes”persona.modedefaults to"shared-home". The other value is"fresh-home".persona.mask_inheriteddefaults 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 asnails checklists it: the.desktopstem 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 throughsudofor 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 watcher and disks
Section titled “Backend watcher and disks”backend_watch.enableddefaults totrue. It runs the watcher that alerts you when the hidden volume fails.backend_watch.poll_interval_secsdefaults to5, the seconds between the watcher’s probes.backend_watch.auto_emergencydefaults tofalse. Set it totrueto opt in to an automatic emergency when the hidden volume fails.backend_watch.debounce_secsdefaults to15, how long a failure must last before the automatic emergency fires.session_mounts.alertdefaults totrue. It alerts you when a disk outside the hidden volume is mounted read-write during the session.session_mounts.read_onlydefaults tofalse. Set it totrueto 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
Section titled “Sleep policy”sleep_policy.modedefaults 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.blockholds 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 tolock,ignoreorpoweroffis left to logind and keeps working. A laptop kept running with its lid shut can overheat, so still set the lid switch tolock.teardownpowers the machine off when it is about to sleep, within logind’sInhibitDelayMaxSecwindow 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.
Full details
Section titled “Full details”The configuration reference is checked against the code and describes every key, the path rules and the fstab policy in full.