Skip to content

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.


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.

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 — a dd clone 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=zstd everywhere; nodev,nosuid,noexec on /boot, /var/log, /.snapshots, /persist where safe (ported from the Arch scripts, cited in-module).

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 /boot on 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.
  • 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, 0700 homes, fs.protected_{symlinks,hardlinks,fifos,regular}, sudo-rs with wheelNeedsPassword, minimal polkit, no world-writable paths.

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/store still 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).
  • 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-resolved with 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.

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.
  • 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.

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 addresses

The module headers cite their sources inline.