/security
What's protected, and what isn't.
Ephemeral Nix sets careful defaults on every machine and documents each one together with what it doesn't cover. Nothing here makes a machine invulnerable. This page describes the config as released; each control links to the file that sets it.
01Implemented
On every machine, in every profile, unless marked.
full full profile only option off unless you turn it on
Storage
| Full-disk encryption | LUKS2 with argon2id (5 s iteration time) over everything but the ESP; the swap file is inside it. | modules/_disko-layout.nix |
|---|---|---|
| /boot permissions | The ESP is mounted with umask=0077, readable by root only. | modules/_disko-layout.nix |
| Ephemeral root | / is recreated from an empty snapshot at every boot, so files dropped outside the kept paths don’t survive it. | modules/ephemeral-root.nix |
| Snapshots are root-onlyfull | Old snapshots hold old password hashes and keys, so /persist/.snapshots is mode 750, owned by root. | modules/snapshots.nix |
Accounts and privilege
| Declared users | mutableUsers = false: users come from the config. One user, in wheel. | modules/users.nix |
|---|---|---|
| No root password | Root’s password is locked. sudo is for wheel only, with a password, and only runs from wheel members. | modules/users.nix |
| Nix daemon | allowed-users is @wheel; only root is a trusted user. | modules/nix.nix |
| Password hash | Only a yescrypt hash, in /persist/passwords/<user>, mode 600, owned by root; permissions set again at every boot. | modules/users.nix |
| Safe password changes | ephemeral-passwd asks twice and only replaces the hash once it has a valid one, so a typo can’t lock you out. | scripts/ephemeral-passwd.sh |
Network
| Firewall | On, with no port opened unless a setting asks for it. | modules/networking.nix |
|---|---|---|
| SSH | Off. Turned on, it still keeps port 22 closed, allows keys only, refuses root logins, and turns off agent and X11 forwarding. | modules/ssh.nix |
| OpenSnitchfull | Default action deny for outbound connections; it asks the first time each program connects. | modules/opensnitch.nix |
| Wi-Fi MAC address | A different address on each network, stable for that network. | modules/networking.nix |
| Name resolution | LLMNR off. mDNS discovery (Avahi) is off on purpose. | modules/networking.nix |
| DNS over TLSoption | Optional, through Control D, with no fallback servers. The resolver id is kept root-only, never in the Nix store. | modules/controld.nix |
| SMB mountsoption | SMB 3.1.1 with encryption (seal); passwords root-only in /persist/etc/smb. | modules/smb.nix |
Kernel
| Kernel image protection | security.protectKernelImage, which rules out kexec and hibernation. | modules/hardening.nix |
|---|---|---|
| IOMMU | intel_iommu=on, iommu.passthrough=0, iommu.strict=1: each device is limited to the memory it’s given. | modules/hardening.nix |
| Memory | init_on_free=1, slab_nomerge, page_alloc.shuffle=1, vsyscall=none, debugfs=off. | modules/hardening.nix |
| sysctls | kptr_restrict=2, dmesg_restrict, unprivileged BPF off, BPF JIT hardening, ptrace_scope=1, no suid core dumps, protected FIFOs and regular files, no line-discipline autoload, no ICMP redirects or source routing. | modules/hardening.nix |
| Unused protocols | dccp, sctp, rds and tipc are blacklisted. | modules/hardening.nix |
Services and secrets
| Systemd sandbox | The config’s own root services run with a read-only system, no network, no capabilities and restricted syscalls: Control D, Flathub, the user picture, root-reset-check and the snapshot setup. | modules/sandbox.nix |
|---|---|---|
| No secrets in the store | ephemeral-update and ephemeral-setup refuse to rebuild when the config folder holds files that look like keys or passwords. | scripts/ephemeral-update.sh |
| Typed settings | Every setting that reaches a script or a path has a type that refuses what would break it; settings-reject checks the refusals. | modules/settings/_schema.nix |
| Unfree allowlist | Nothing unfree builds unless it’s listed by name. | modules/unfree.nix |
| Pinned Flathub | Flathub is added from a pinned copy of its repo file, so its signing key can’t be swapped. | modules/flathub.nix |
The installer
| Passphrase handling | The disk passphrase goes to partitioning only on standard input; the password only as a hash. Neither is in the environment or a log. | ARCHITECTURE.md |
|---|---|---|
| Disk checks | Nothing is erased before every check passes, and the disk’s identity is checked again right before erasing. | scripts/common.sh |
02OpenSnitch
Outgoing connections are blocked until you allow them.
On the full profile, OpenSnitch asks the first time each program connects somewhere. If an app can't connect, a missed or denied prompt is the likely cause: open OpenSnitch and check its rules. Options that need the network, such as Tailscale and the Proton Pass SSH agent, add their own rule. "useOpenSnitch": false in settings.json turns it off.
03Not covered
Limits to know before you rely on it.
Boot integrity
/boot is not encrypted and Secure Boot is not set up; installing needs it turned off. Someone with the laptop in hand could replace the kernel or initrd with one that records the passphrase. There is no measured boot and no TPM: the disk unlocks with the passphrase only.
Before the kernel
The IOMMU makes DMA attacks over Thunderbolt and USB4 much harder once the kernel is running. It doesn't cover the firmware and boot loader before that.
TRIM
Discards pass through the encryption for SSD performance. That shows which blocks are in use, not what's in them.
Root-equivalent choices
Once ephemeral-setup makes /persist/etc/nixos yours, anything running as you can change what the next rebuild installs as root. Docker and virt-manager add you to docker or libvirtd, which has the same effect. Each is your choice, and each says so when you make it.
Convenience defaults
On the full profile nix-ld lets prebuilt Linux programs run unpatched. smartd's disk warnings go over the system bus, so any local process can show you a notification. With fingerprint login on, a finger also works for sudo. The security notes say how to turn each off.
Visible on the network
With Control D, the resolver id is part of the server name TLS sends unencrypted, so your network can see it. It tells Control D which settings to use; it doesn't log you in.
04Threat model
Who it's built against.
A single-user laptop or desktop, used day to day. The aim is to protect data at rest, keep the network surface small, make unexpected outbound traffic visible, and keep system state from silently accumulating, without a setup that's tiring to live with. It is not built for a physically targeted user or a hostile multi-user machine.
| Threat | Addressed | How, and the gap |
|---|---|---|
| A powered-off laptop is lost or stolen | yes | Everything but /boot is LUKS2-encrypted with argon2id. The passphrase is the only key; choose a strong one. |
| Someone tampers with the laptop while you’re away, then you unlock it | no | /boot is not encrypted and Secure Boot isn’t set up, so a modified kernel or initrd could record the passphrase. |
| A running, unlocked machine is taken | partly | The screen locks when idle (Noctalia after 10 minutes on niri and Umbriel; the desktop’s own settings on GNOME and Plasma). Disk encryption doesn’t help once unlocked. |
| DMA over Thunderbolt or USB4 | partly | The IOMMU limits devices once the kernel runs. It doesn’t cover the firmware and boot loader before that. Devices need approving once. |
| A connection in from the network | yes | Firewall on, SSH off, no open ports by default. |
| A program calling home or exfiltrating data | partly | On the full profile OpenSnitch denies outbound connections you haven’t allowed. It’s only as strict as the rules you approve. |
| Malware that persists in system directories | partly | Anything written to / outside the kept paths is gone at the next boot. Your home folder, /persist and a user-owned config folder are kept, so it isn’t a defence on its own. |
| A local process becoming root | partly | Kernel hardening, sandboxed root services and wheel-only sudo raise the bar. Docker and libvirtd group membership, or a config folder you own, are root-equivalent. |
| Secrets leaking into the Nix store | yes | Rebuilds refuse when the config folder holds key- or password-like files; secrets live in root-only files read at runtime. |
| A compromised upstream input | partly | Inputs are pinned and releases immutable; only nixpkgs updates merge on their own after tests, others are read by a person first. Nothing can verify every upstream line. |
05Supply chain
What you install can be checked.
- Releases are immutable: once published, a tag and its files can't change.
- Installer ISOs are built by CI and attested;
gh attestation verifyproves which workflow built one, andgh release verify-assetthat it's the one the release went out with. - Release tags cut by hand are signed. Weekly releases tagged by CI aren't, but GitHub signs a record of each release's tag and files, which
gh release verifychecks. - Machines only build a pinned, released tag, never a branch.
ephemeral-updateonly offers a newer, well-formed tag. - Only nixpkgs updates merge on their own, after CI and the boot tests. Other inputs are read by a person first.
- CI pins every action by commit, asks for permissions per job, keeps tokens out of the checkout, lints workflows with actionlint and zizmor, scans for secrets, and runs
govulncheckon the installer.
sha256sum -c SHA256SUMS
gh attestation verify ephemeral-nix-*.iso -R the-wittch/ephemeral-nixos
gh release verify-asset ephemeral-nix-*.iso -R the-wittch/ephemeral-nixosMore on the install page.
06Planned
No security changes are announced.
The repositories don't announce Secure Boot, TPM unlocking or measured boot, and the docs make no promise to add them. The config's rule is that kernel hardening only gets stronger: turning anything off needs a changelog entry saying how to undo it. Changes are listed in the changelog as they ship.
07Reporting