home / security stack

Personal Security Stack

A detailed breakdown of how my own machines, browsers, mail and research environments are set up, with configs. Secrets are redacted. It describes my setup; it is not a recommendation for yours.

Threat model

who I defend against
in scope
Opportunistic malware and phishing, credential theft, theft or loss of the laptop, network observers on untrusted Wi-Fi, and mistakes that link my research identity to my daily one.
out of scope
A well-resourced targeted adversary and covert physical access. I reduce exposure to these but do not claim to defend against them.
known gaps
No measured or verified boot of the initramfs. Browsers and the VPN app are third-party binaries I do not build myself. Local detection is logs I read myself, not a staffed monitoring service.

Overview

trust zones
networkMullvad VPNWireGuard, defaultProtonVPNWireGuard, second providerhostVoid Linux (musl), minimaldwm + st, LUKS2, nftables default-denyaccountsuser: dailyBrave (hardened), mailuser: researchMullvad Browser, toolinguser: admindoas only, no browsingisolationResearch VMs (KVM/QEMU)disposable overlay on br-labbr-lab forwards to VPN onlynever to the LAN or host
Trust zones, from network edge to research VMs

Operating system

Void Linux musl, minimal

Daily driver is a minimal Void Linux (musl) install: runit for services, xbps for packages, dwm as the window manager and st as the terminal. No display manager and no desktop environment, so far fewer services and listening sockets than a general-purpose desktop. musl is a smaller codebase than glibc, but that is not a security mitigation in itself and I do not treat it as one.

Brave, Mullvad Browser and the Mullvad app ship as glibc binaries, so they run from a separate glibc Void chroot rather than on the musl host.

Hardening

  • Install only what I use; xbps-query -m reviewed monthly and unused packages removed.
  • Updates weekly with xbps-install -Su, then reboot if the kernel changed.
  • doas instead of sudo, with permit persist for the admin group only; no root login.
  • Default-deny inbound firewall with nftables (below).
  • Kernel and network sysctls tightened (below).
  • Nothing listens beyond loopback; checked with ss -tulpn after changes.
# /etc/sysctl.d/99-hardening.conf
kernel.kptr_restrict = 2
kernel.dmesg_restrict = 1
kernel.yama.ptrace_scope = 2
kernel.unprivileged_bpf_disabled = 1
net.core.bpf_jit_harden = 2
fs.protected_symlinks = 1
fs.protected_hardlinks = 1
fs.protected_fifos = 2
fs.protected_regular = 2
net.ipv4.tcp_syncookies = 1
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
# /etc/nftables.conf (loaded from /etc/rc.local)
table inet filter {
  chain input {
    type filter hook input priority 0; policy drop;
    ct state established,related accept
    iif lo accept
    ip protocol icmp icmp type { echo-request } limit rate 5/second accept
    ip6 nexthdr icmpv6 accept
  }
  chain forward {                       # research VMs: out via the VPN only, never to the LAN
    type filter hook forward priority 0; policy drop;
    ct state established,related accept
    iifname "br-lab" oifname "wg-lab" accept
  }
  chain output { type filter hook output priority 0; policy accept; }
}

Disk encryption and backups

LUKS2 and restic

Root is a LUKS2 volume with a long passphrase; only the EFI partition is unencrypted. I shut down rather than suspend when the laptop leaves my sight. Backups follow 3-2-1: the machine, an encrypted external drive, and one offline copy, with restic encrypting every snapshot on the client. I test a restore every quarter, because an untested backup is a hope, not a backup.

# restic to an encrypted external drive; second copy kept offline
restic -r /mnt/backup/restic backup ~ \
  --exclude ~/.cache --exclude ~/vm --exclude-caches
restic -r /mnt/backup/restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --prune
restic -r /mnt/backup/restic check --read-data-subset=10%

Browsers, passwords and 2FA

separate roles, separate profiles

Brave (hardened) is for logged-in daily use. Managed policy disables Rewards, Wallet, VPN, AI chat, sync and the built-in password manager, and blocks every extension, so the attack surface is Brave itself plus its Shields.

Mullvad Browser is for research browsing. It standardises its fingerprint so I look like every other Mullvad Browser user, and ships with uBlock Origin. It never logs into anything tied to me.

Neither browser has Firefox-style containers, so isolation comes from a different browser, profile and OS user per role, not from tabs in one profile.

Passwords and 2FA

  • Passwords live in KeePassXC: a local database with a long passphrase, no browser sync, backed up with everything else.
  • Two FIDO2 hardware keys, one in daily use and one stored offline, registered everywhere that supports them.
  • TOTP only where hardware keys are unsupported, with codes kept apart from the password for that account.
  • Recovery codes printed and stored offline.
// /etc/brave/policies/managed/hardening.json
{
  "BraveRewardsDisabled": true,
  "BraveWalletDisabled": true,
  "BraveVPNDisabled": true,
  "BraveAIChatEnabled": false,
  "SyncDisabled": true,
  "PasswordManagerEnabled": false,
  "MetricsReportingEnabled": false,
  "SearchSuggestEnabled": false,
  "BlockThirdPartyCookies": true,
  "WebRtcIPHandling": "disable_non_proxied_udp",
  "DnsOverHttpsMode": "off",
  "ExtensionInstallBlocklist": ["*"]
}

VPNs and network

Mullvad and ProtonVPN

I use Mullvad VPN as the default and ProtonVPN as a second provider, so research traffic and daily traffic need not share an exit or an account. Both run as plain WireGuard interfaces (wg-quick) rather than GUI apps, which keeps the client small and easy to audit. A VPN moves trust from my ISP to the provider; it does not make me anonymous.

# /etc/wireguard/wg-lab.conf  (Mullvad WireGuard, no GUI app needed)
[Interface]
PrivateKey = <redacted>
Address    = 10.x.x.x/32
DNS        = 10.64.0.1
[Peer]
PublicKey  = <redacted>
Endpoint   = <redacted>:51820
AllowedIPs = 0.0.0.0/0, ::0/0

Email

Proton Mail and PGP

Mail is Proton Mail, with login protected by a hardware key. People can encrypt to my published PGP key (see the secure contact ritual) and filters sort disclosure mail by subject prefix. The body is protected; subject lines and metadata are not, which is why I ask for generic subjects.

senderReporterverifies my key, encrypts and signstransportProton Mailbody encrypted; subject and metadata visiblefiltersSieve filter[REPORT] goes to the Reports folderhandlingDecrypt and readhardware key for loginReproduce in a VMresearch user, throwaway overlay, no real data
Mail flow for an encrypted report
# Proton Mail filter (Sieve): route disclosure mail to its own folder
require ["fileinto"];
if header :contains "subject" ["[REPORT]", "[SETUP-TEST]"] { fileinto "Reports"; }
elsif header :contains "subject" "[COLLAB]" { fileinto "Collab"; }

Isolating research

VMs, accounts and segmentation

Research never happens in my daily session. Anything I run or poke at goes in a disposable KVM/QEMU guest on its own bridge (br-lab). The firewall forwards that bridge to the VPN interface only, so a compromised guest cannot reach my LAN or the host's services.

  • Separate OS users for daily, research and admin; no shared home directories.
  • Each guest is a throwaway overlay on a read-only base image, deleted when done.
  • No shared folders, no clipboard sharing, no USB passthrough by default.
  • No real credentials or client data inside any guest. Write-ups follow the sanitization policy.
# throwaway research VM: disposable overlay on a read-only base image
qemu-img create -f qcow2 -b base-void.qcow2 -F qcow2 run-$(date +%F).qcow2
qemu-system-x86_64 -enable-kvm -m 4G -smp 2 \
  -drive file=run-$(date +%F).qcow2,if=virtio \
  -nic bridge,br=br-lab,model=virtio-net-pci \
  -display gtk,gl=off          # no shared folders, no clipboard sharing
# done? delete the overlay; the base image is never written to
rm run-*.qcow2

Firmware and physical access

the layer below the OS

Encryption protects a lost laptop only if the boot path cannot be tampered with, so the firmware settings matter as much as the disk passphrase. I set an administrator password in UEFI, disable booting from removable media, and keep boot order fixed. Fast boot and wake-from-USB are off, so I see the boot media change before the OS starts.

Removable media is treated as untrusted and off by default. I do not allow autorun anywhere, and a drive I did not buy myself is only ever read in the research VMs. The webcam has a physical cover. Anything that needs stronger separation than an encrypted volume and a hardened boot path does not live on this machine.

  • UEFI admin password set, external boot disabled, boot order pinned.
  • BIOS/firmware updates applied from the vendor after checking the release notes.
  • Removable media off by default, no autorun; unknown drives only in research VMs.
  • Theft and loss are handled by LUKS2 plus shutdown-when-unattended, not by relying on suspend.

Detection on my own machines

notice when the boundary is touched

Hardening is half of it; I also want to know when something crosses a line. The host keeps a small, focused set of logs that survive a reboot, and auditd watches the paths that matter: privilege and access-control files, SSH keys, cron and the bootloader. Authentication failures are rate-limited, and integrity is checked against a baseline kept off the machine.

# /etc/audit/rules.d/host.rules (excerpt)
-w /etc/doas.conf -p wa -k priv_esc
-w /root/.ssh/authorized_keys -p wa -k ssh_keys
-w /etc/cron.d -p wa -k cron
-w /boot -p wa -k boot
-a always,exit -F arch=b64 -S execve -k exec
# the same watches /detections/ documents for the lab apply here
  • auditd watches privilege, key, cron and boot paths; alerts land in my mailbox.
  • Auth failures are rate-limited and logged, not silently dropped.
  • A file-integrity baseline is stored off the host, so a change can be spotted after the fact.
  • The rules are the same shape as the ones in the detection library, tested in the lab first.

Verification and maintenance

what I check, and how often
CadenceCheck
WeeklyApply updates (xbps-install -Su) and reboot if the kernel changed; confirm nothing new is listening with ss -tulpn.
MonthlyReview installed packages against what I actually use (xbps-query -m); re-read the nftables rules and the browser managed policy.
QuarterlyTest a full restore from a restic snapshot; verify the offline FIDO2 key still works; rotate long-lived secrets and the WireGuard keys.
After any changeRe-run the listener and firewall checks; re-test the VPN kill-switch; confirm auditd rules still load.

What is redacted

Keys, endpoints, addresses and usernames are replaced with placeholders. Snippets are excerpts, not complete files, and will change as my setup does.