Architecture & security model
This document is the blueprint: the threat model, the storage/crypto/boot layout, and the data-security, network, data-minimization, and supply-chain design. Be honest about limits is a design rule here — every protection notes what it does not stop.
1. Threat model
Section titled “1. Threat model”Who this is for: someone doing sensitive work under a high-surveillance environment (the operator currently lives in [region], routes through [overseas] VPN exits, and may relocate to [region]). The design goal: if the device is lost or confiscated or the operator is detained, data and communications stay confidential, and the system stays auditable and easy to re-harden. Same design space as Tails / Qubes / GrapheneOS.
In scope (defended):
- Device loss or confiscation while powered off → full-disk encryption; detached header makes the disk look blank.
- Evil-maid tampering of the boot chain → Secure Boot (signed UKIs) + firmware password.
- Network surveillance / censorship → VPN kill-switch, encrypted DNS, MAC randomization, Tor for anonymity.
- Forensic trace accumulation → ephemeral root (impermanence), volatile logs, no hibernation image.
- Malicious USB / peripherals → USBGuard, IOMMU/DMA protection.
- Supply-chain / package poisoning → pinned inputs, signature discipline, reproducible builds.
Out of scope (NOT defended — stated plainly):
- A live, privileged remote attacker who already has code execution as you.
- Coercion — no software defeats a wrench; the mitigation is not carrying the data.
- Cold-boot / RAM remnant attacks while powered on or suspended (→ full shutdown when at risk).
- Hardware implants / firmware backdoors below the OS.
- Metadata at the endpoints — messengers can’t stop the other side keeping copies or screenshots.
- Plausible deniability as a legal guarantee — see §7; it’s a tradeoff, not a shield.
2. Storage layout
Section titled “2. Storage layout”One internal NVMe + one USB “key” stick. The USB holds everything needed to boot and the LUKS header, so the internal disk is inert without it.
USB key stick (GPT, declared in hosts/cobalt/disko.nix — only two partitions are Linux-only) ├─ p1 ESP FAT32 1G → /boot systemd-boot + lanzaboote-signed UKIs, + installer boot entry [all OSes] ├─ p2 raw 32M detached LUKS2 header (no filesystem; GPT label "reserved") [Linux] ├─ p3 raw 16G installer ISO image (iso9660, volume id COBALT_INSTALL; closure embedded) [Linux] └─ p4 exFAT rest interop files + a VeraCrypt container (cross-OS encrypted) [all OSes] macOS shows an "Initialize?" dialog for p2/p3 — always choose Ignore.
Internal NVMe (referenced by /dev/disk/by-id/…, from my/private.nix) — NO partition table, NO label, NO LUKS header: the whole device is the LUKS2 data area (header DETACHED → on the USB). Nothing on the device identifies it as encrypted; what stays visible is the split between written extents and the TRIMmed zeros of unused space (discards are allowed, see §7). └─ LUKS2 container (whole device) └─ LVM VG "vg" ├─ lv-swap inside the LUKS container (no hibernation) └─ lv-root btrfs ├─ @ → / (rolled back to blank each boot) ├─ @home → /home (persists) ├─ @nix → /nix (persists; the store) └─ @persist → /persist (impermanence allowlist: machine-id, NM connections, etc.)- Why LVM-on-LUKS: satisfies the explicit LVM requirement; a single FIDO2 unlock exposes root + swap + home; encrypted swap comes for free.
- Why btrfs on the root LV: subvolumes enable the “erase your darlings” rollback (§5) and cheap snapshots; the detached-header guide we follow allows substituting btrfs for ext4.
- Why the header is detached onto the USB, and why no partition table: removes all LUKS metadata and
every label from the internal disk. Cost: lose the stick and the disk is
unrecoverable → keep ≥2 cloned sticks + an offline
cryptsetup luksHeaderBackup. - Device identity: the NVMe is addressed by its hardware id (
/dev/disk/by-id/…), the stick’s partitions by GPT label — addclone of the stick keeps labels, partuuids and filesystem uuids but not the USB serial, so by-id would break the spare sticks. Filesystem UUIDs cannot be used for the root device: a headerless LUKS area has none. - Mount hardening:
noatime,compress=zstdeverywhere;nodev,nosuid,noexecon/boot,/var/log,/.snapshots,/persistwhere safe (ported from the Arch scripts, cited in-module).
3. Boot chain + FDE (what the terms mean)
Section titled “3. Boot chain + FDE (what the terms mean)”The boot chain is the ordered sequence that turns a powered-off machine into a login prompt:
power on → UEFI firmware → systemd-boot / lanzaboote → kernel + initrd → systemd → login (firmware pw) (Secure Boot: signed UKI) (FIDO2 unlock in initrd)- Secure Boot (lanzaboote) signs the bootloader + kernel + initrd (as a Unified Kernel Image). Each stage verifies the next, so an attacker can’t silently swap in a tampered kernel while the laptop is unattended (boot-tampering). It needs a firmware supervisor password, or an attacker just turns Secure Boot off.
- FDE (Full Disk Encryption) encrypts the whole root container (LUKS2). The key is released only after the
YubiKey FIDO2 tap (PIN + touch) during initrd. So:
- Secure Boot protects the code that boots (tamper-evidence while off).
- FDE protects the data (confidentiality at rest).
- No
/booton the internal disk: kernels/initrd live on the USB ESP. Pull the stick → firmware finds no bootloader and there’s no LUKS header → the machine can’t boot and the disk can’t be read. That “no stick = nothing” property is inherent to the layout, not a toggle.
4. Data-security model
Section titled “4. Data-security model”- At rest: everything is inside LUKS2 (aes-xts-plain64 / argon2id, pinned). Secrets in the repo are
sops-encrypted (age via YubiKey PIV) — the Nix store is world-readable, so plaintext secrets never go in it;
they’re decrypted at activation into
/run/secrets(tmpfs). - In transit: VPN (kill-switched) as the outer tunnel; DNS-over-TLS; app content E2EE (Signal/Element); Tor Browser for anonymous browsing.
- File/system permissions (#19): umask 077,
0700homes,fs.protected_{symlinks,hardlinks,fifos,regular}, sudo-rs withwheelNeedsPassword, minimal polkit, no world-writable paths.
5. Anti-forensics (impermanence)
Section titled “5. Anti-forensics (impermanence)”The root subvolume @ is reset to a blank snapshot on every boot. Only /home, /nix, and an explicit
/persist allowlist survive. This wipes logs, shell history, caches, thumbnails, and non-persistent malware
each reboot. Honest limits:
/nix/storestill records every package you’ve installed (a usage history) until GC.- Whatever you choose to persist still accumulates.
- CoW “deleted” data isn’t securely erased; the real wipe is crypto-erase (destroy the LUKS key /
cryptsetup luksErase), not file shredding — especially on SSD/NVMe (wear-leveling). - RAM remnants persist while powered on/suspended → full shutdown when crossing a hostile boundary.
- No hibernation (a hibernation image would write keys + RAM to disk).
6. Network & firewall
Section titled “6. Network & firewall”- Firewall: nftables backend; default-deny inbound (NixOS default); nothing opened unless needed; refused connections logged.
- Egress: on a single-user laptop, per-port egress rules add little (malware runs as you). The real
egress control is the proxy: with the opt-in kill-switch (
my.features.killSwitch) an nftables output chain drops everything that is not loopback, the TUN, established replies, DHCP/NDP, the local network, or the proxy service itself — so nothing leaves the physical interface while mihomo is down. - Wi-Fi: NetworkManager + iwd backend; MAC randomization configured in iwd (NM’s own randomization is silently disabled under the iwd backend — a documented gotcha). Hostname not leaked to DHCP.
- IPv6: temporary (privacy) addresses preferred.
- DNS:
systemd-resolvedwith DNS-over-TLS + DNSSEC (Quad9/Mullvad); upgrade path to dnscrypt-proxy2 with anonymized relays. - Anonymity: app-level Tor + Tor Browser — never system-wide transparent torification (false confidence + leak risk).
- VPN: first-class ([region] censorship context). WireGuard preferred; kill-switch via firewall so traffic can’t leak if the tunnel drops. Provider/exit config pending.
7. Deniability — honest assessment
Section titled “7. Deniability — honest assessment”Linux deniability is weak and detectable; this section is a tradeoff map, not a promise.
- Detached header, no partition table (chosen): the internal disk carries no header, no table and no label → defeats identification and unlock, not suspicion (a whole-disk random blob still implies encryption, and with discards enabled the TRIMmed free space reads as zeros, so the used extent is visible).
- Duress / destructive-wipe password (OFF by default): wiping the LUKS header is not upstream, legally fraught (destroying data under compulsion can be obstruction and can escalate danger), reversible if a header backup exists, and detectable. Opt-in only.
- The reliable practice: don’t carry the sensitive data across a hostile boundary. Encrypt → upload to remote storage → wipe the device → re-download later. A genuinely empty machine beats a clever hidden one.
8. Supply-chain integrity
Section titled “8. Supply-chain integrity”- Inputs pinned in
flake.lock; updates are deliberate (nix flake update+ diff review). require-sigs = true;trusted-users = [ "root" ](anything more is root-equivalent); no stray substituters; each extra cache needs its own vetted key. China mirrors deliberately dropped.- Prefer nixpkgs (reviewed/CI’d) over random community flakes; every extra flake input/cache is added trust.
- Periodic
nix store verify; reproducible-build spot checks (--rebuild+ diffoscope). - Offline install uses a signed
file://cache on the USB (unsigned caches are silently skipped).
9. Sandboxing (the Apple-App-Sandbox/TCC analogue, #20)
Section titled “9. Sandboxing (the Apple-App-Sandbox/TCC analogue, #20)”No single Linux feature equals macOS’s App Sandbox + TCC, but the stack approximates it:
- Flatpak + xdg-desktop-portal (bubblewrap underneath) — per-app confinement + user consent prompts for camera/mic/screen/files. Closest to TCC. Used for the browser, Signal/Element, PDF/office viewers.
- AppArmor — system-wide mandatory access control (the global-policy layer).
- Wayland — apps can’t keylog/screen-scrape each other (baseline X11 lacks).
- systemd unit sandboxing —
ProtectSystem/SystemCallFilter/etc. for daemons. - bubblewrap is preferred over setuid-root firejail.
10. Repository layout
Section titled “10. Repository layout”nix-config/├─ flake.nix inputs, the host factory, the installer image outputs├─ my/ placeholders for identity and disk ids; real values in the git-ignored my/private.nix├─ hosts/cobalt/ the Acer A315-24P host: default.nix · hardware.nix · disko.nix├─ modules/nixos/ the hardened module set: nix-settings, boot-crypto, impermanence, secrets, users,│ desktop-kde, locale-fonts, base-packages, yubikey, home, messaging, tor,│ hardening/{kernel,network,permissions,peripherals,sandbox,killswitch}├─ home/ operator.nix (Home-Manager entry) + home/modules/* + home/dotfiles/├─ pkgs/ custom packages├─ installer/ the offline installer: ISO with the whole system embedded, plus its UKI├─ scripts/ flash-usb.sh (macOS/Linux) · install.sh · vm-rehearsal.sh├─ secrets/ sops-encrypted secrets: users.yaml, mihomo.yaml├─ example.CM.min.yaml skeleton of the mihomo profile that secrets/mihomo.yaml encrypts└─ .github/donate/ donation QR codes and addressesThe module headers cite their sources inline.