commit 7f4371bbd34381c9353b92739e917b297084ffe0
parent d6d42a863f25d11b6f10aa624b4a053beec9389f
Author: DAEMON <zer0sec.xp@icloud.com>
Date: Sat, 10 Oct 2026 11:50:44 +0100
feat(ssd): add secure-vault LUKS container on the SSD
- Add a `secure-vault` script that creates, opens, locks, and reports on a 100 GB LUKS2 container stored as a file on /mnt/ssd, with a `busy` subcommand to list processes holding the vault mount.
- Serialize vault operations with a lock, and refuse to act when the SSD mount source, the container path, or the mapper's backing file is not the expected one.
- Install cryptsetup, btrfs-progs, e2fsprogs, keepassxc, age, restic, and the vault script system-wide.
- Declare /mnt/vault as a noauto, nofail ext4 mount with nosuid, nodev, and noexec, and create its root-owned tmpfiles directory.
Commit-Date: 2026-10-10T11:50:52+01:00
Commit-Host: daemonsec@nixos
Diffstat:
1 file changed, 128 insertions(+), 2 deletions(-)
diff --git a/modules/hosts/laptop/ssd.nix b/modules/hosts/laptop/ssd.nix
@@ -14,12 +14,138 @@
{ ... }:
{
flake.nixosModules.laptop-ssd =
- { ... }:
- {
+ { pkgs, user, ... }:
+ let
+ vault = pkgs.writeShellApplication {
+ name = "secure-vault";
+ runtimeInputs = with pkgs; [ cryptsetup util-linux coreutils e2fsprogs psmisc gawk ];
+ text = ''
+ export LC_ALL=C
+ case "''${1:-help}" in
+ init|open|lock|status|busy) ;;
+ *) echo 'Usage: secure-vault {init|open|lock|status|busy}'; exit 0 ;;
+ esac
+ if (( EUID != 0 )); then
+ exec /run/wrappers/bin/sudo "$0" "$@"
+ fi
+ image=/mnt/ssd/.secure-vault/vault.luks
+ pending=/mnt/ssd/.secure-vault/vault.luks.building
+ mapper=netrunner-private
+ target=/mnt/vault
+ owner=${user}
+ fail() { echo "$*" >&2; exit 1; }
+ # Serialize lifecycle operations. Nothing here manages the outer ssd mapper.
+ exec 9>/run/lock/secure-vault.lock
+ flock -n 9 || fail 'Another vault operation is running.'
+ require_ssd() {
+ mountpoint -q /mnt/ssd || fail 'The existing SSD must be mounted at /mnt/ssd first.'
+ local source
+ source=$(findmnt -nro SOURCE --mountpoint /mnt/ssd)
+ [[ "$source" == /dev/mapper/ssd || "$source" == /dev/mapper/ssd\[* ]] ||
+ fail 'Unexpected SSD mount source; refusing to create/open a container.'
+ }
+ verify_mapper() {
+ local loop backing
+ loop=$(cryptsetup status "$mapper" | awk '$1 == "device:" {print $2}')
+ [[ "$loop" == /dev/loop* ]] || fail 'Vault mapper is not backed by a loop device.'
+ backing=$(losetup --noheadings --raw --output BACK-FILE "$loop")
+ [[ "$backing" == "$image" || ( "$backing" == "$pending" && ! -e "$image" ) ]] || fail 'Vault mapper belongs to a different container; refusing.'
+ }
+ verify_mount() {
+ local source
+ source=$(findmnt -nro SOURCE --mountpoint "$target")
+ [[ "$(readlink -f "$source")" == "$(readlink -f "/dev/mapper/$mapper")" ]] ||
+ fail 'Unexpected filesystem at /mnt/vault; refusing to unmount it.'
+ }
+ case "$1" in
+ init)
+ require_ssd
+ [[ ! -e "$image" && ! -L "$image" && ! -e "$pending" && ! -L "$pending" ]] ||
+ fail 'Container or unfinished creation already exists; refusing to overwrite it.'
+ [[ ! -e "/dev/mapper/$mapper" ]] || fail 'Vault mapper already exists.'
+ ! mountpoint -q "$target" || fail 'Vault mountpoint is already occupied.'
+ # 100 GB decimal, plus 5 GB spare space for the outer filesystem.
+ available=$(df --output=avail -B1 /mnt/ssd | tail -n 1)
+ (( available >= 105000000000 )) || fail 'Need at least 105 GB free on the SSD.'
+ [[ ! -L /mnt/ssd/.secure-vault ]] || fail 'Container directory must not be a symlink.'
+ install -d -m 0700 -o root -g root /mnt/ssd/.secure-vault
+ umask 077
+ ( set -o noclobber; : > "$pending" )
+ fallocate -l 100000000000 "$pending"
+ echo 'Creating a NEW 100 GB container. Choose a long, unique vault passphrase.'
+ echo 'This does not format or close the existing SSD.'
+ cryptsetup luksFormat --type luks2 --pbkdf argon2id --iter-time 3000 "$pending"
+ # Opening a regular file uses a cryptsetup-managed loop device.
+ cryptsetup open --type luks "$pending" "$mapper"
+ cleanup_init() {
+ if mountpoint -q "$target"; then
+ echo 'Initialization stopped with vault mounted; close apps and run secure-vault lock.' >&2
+ else
+ cryptsetup close "$mapper" || true
+ fi
+ }
+ trap cleanup_init EXIT
+ mkfs.ext4 -m 0 -L PrivateVault "/dev/mapper/$mapper"
+ install -d -m 0700 -o root -g root "$target"
+ mount -o nosuid,nodev,noexec "/dev/mapper/$mapper" "$target"
+ install -d -m 0700 -o "$owner" -g "$(id -gn "$owner")" "$target/Private"
+ umount "$target"
+ cryptsetup close "$mapper"
+ trap - EXIT
+ mv -T "$pending" "$image"
+ echo '100 GB vault created and locked. Use secure-vault open.'
+ ;;
+ open)
+ require_ssd
+ [[ -f "$image" && ! -L "$image" ]] || fail 'No vault container. Run secure-vault init once.'
+ [[ ! -e "/dev/mapper/$mapper" ]] || fail 'Vault mapper is already open; check status.'
+ ! mountpoint -q "$target" || fail 'Vault mountpoint is already occupied.'
+ cryptsetup open --type luks "$image" "$mapper"
+ if ! mount "$target"; then
+ cryptsetup close "$mapper"
+ fail 'Mount failed; vault mapping closed.'
+ fi
+ echo 'Vault open: /mnt/vault/Private'
+ ;;
+ lock)
+ if cryptsetup status "$mapper" >/dev/null 2>&1; then
+ verify_mapper
+ if mountpoint -q "$target"; then
+ verify_mount
+ umount "$target"
+ fi
+ cryptsetup close "$mapper"
+ else
+ ! mountpoint -q "$target" || fail 'Vault mountpoint occupied but mapper absent; inspect manually.'
+ fi
+ echo 'Private vault locked. Existing SSD remains available.'
+ ;;
+ status)
+ cryptsetup status "$mapper" || true
+ findmnt --mountpoint "$target" || true
+ ;;
+ busy)
+ mountpoint -q "$target" || fail 'Vault is not mounted.'
+ fuser -vm "$target"
+ ;;
+ esac
+ '';
+ };
+ in {
+ environment.systemPackages = with pkgs; [ cryptsetup btrfs-progs e2fsprogs keepassxc age restic vault ];
+
environment.etc.crypttab.text = ''
ssd UUID=8cd17d0f-adf3-430f-9d95-d17ef42df9b0 /etc/secrets/ssd.key luks,nofail
'';
+ # The new inner container is independent of the existing SSD unlock.
+ fileSystems."/mnt/vault" = {
+ device = "/dev/mapper/netrunner-private";
+ fsType = "ext4";
+ options = [ "noauto" "nofail" "nosuid" "nodev" "noexec" ];
+ };
+ systemd.tmpfiles.rules = [ "d /mnt/vault 0700 root root -" ];
+
fileSystems."/mnt/ssd" = {
device = "/dev/mapper/ssd";
fsType = "btrfs";