commit de158dfad01b94e31ed96cdab01748dda83c5fa2
parent 7f4371bbd34381c9353b92739e917b297084ffe0
Author: DAEMON <zer0sec.xp@icloud.com>
Date: Sat, 10 Oct 2026 12:06:44 +0100
feat(ssd): add SOPS-encrypted LUKS header backup to secure-vault
- Add a `backup-header` subcommand that exports the LUKS header, encrypts it with SOPS, and publishes it to a backup path without overwriting anything
- Remove the unused container file if `luksFormat` fails before a LUKS header is written; keep it if a header exists
- Prompt the user to type uppercase YES at the cryptsetup confirmation step
- Ignore vault images, headers, and key material in `.gitignore` so they stay out of the public flake
Commit-Date: 2026-10-10T12:06:51+01:00
Commit-Host: daemonsec@nixos
Diffstat:
2 files changed, 56 insertions(+), 4 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -8,3 +8,18 @@ keys.txt
# scratch of the plan executor and a stray empty clone, never flake source
.superpowers/
/NixDaemon/
+
+# Vault data and plaintext recovery material belong outside this public flake.
+*.luks
+*.luks.building
+*.luksHeader
+*.header.bin
+ssd.key
+vault.key
+vault.passphrase
+secure-vault-header.*
+/secrets/*.key
+/secrets/*.pem
+/secrets/*.backup
+/secrets/*.bak
+/secrets/*.tmp
diff --git a/modules/hosts/laptop/ssd.nix b/modules/hosts/laptop/ssd.nix
@@ -18,12 +18,12 @@
let
vault = pkgs.writeShellApplication {
name = "secure-vault";
- runtimeInputs = with pkgs; [ cryptsetup util-linux coreutils e2fsprogs psmisc gawk ];
+ runtimeInputs = with pkgs; [ cryptsetup util-linux coreutils e2fsprogs psmisc gawk sops ];
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 ;;
+ init|open|lock|status|busy|backup-header) ;;
+ *) echo 'Usage: secure-vault {init|open|lock|status|busy|backup-header DEST.sops.json}'; exit 0 ;;
esac
if (( EUID != 0 )); then
exec /run/wrappers/bin/sudo "$0" "$@"
@@ -74,7 +74,15 @@
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"
+ echo 'At the cryptsetup confirmation prompt, type uppercase YES.'
+ if ! cryptsetup luksFormat --type luks2 --pbkdf argon2id --iter-time 3000 "$pending"; then
+ # Only this newly created file may be removed, and only without a LUKS header.
+ if cryptsetup isLuks "$pending"; then
+ fail 'Formatting stopped but a LUKS header exists; unfinished container preserved.'
+ fi
+ rm -- "$pending"
+ fail 'Creation aborted; unused container file removed. You can retry init.'
+ fi
# Opening a regular file uses a cryptsetup-managed loop device.
cryptsetup open --type luks "$pending" "$mapper"
cleanup_init() {
@@ -120,6 +128,35 @@
fi
echo 'Private vault locked. Existing SSD remains available.'
;;
+ backup-header)
+ require_ssd
+ [[ -f "$image" && ! -L "$image" ]] || fail 'Vault container does not exist.'
+ destination="''${2:-}"
+ [[ "$destination" == /*.sops.json ]] || fail 'Supply an absolute destination ending in .sops.json on your backup drive.'
+ [[ ! -e "$destination" && ! -L "$destination" ]] || fail 'Backup destination already exists; refusing to overwrite it.'
+ # Only public SOPS recipients/rules enter the Nix store.
+ # Header plaintext stays in root-only /run and is removed on exit.
+ umask 077
+ header=""
+ encrypted=""
+ cleanup_header() {
+ [[ -z "$header" ]] || rm -f "$header"
+ [[ -z "$encrypted" ]] || rm -f "$encrypted"
+ return 0
+ }
+ trap cleanup_header EXIT
+ header=$(mktemp /run/secure-vault-header.XXXXXX)
+ encrypted=$(mktemp "$(dirname "$destination")/.vault-header-encrypted.XXXXXX")
+ rm "$header"
+ cryptsetup luksHeaderBackup "$image" --header-backup-file "$header"
+ sops encrypt --config ${../../../.sops.yaml} \
+ --filename-override secrets/vault-header.sops.json \
+ --input-type binary --output-type json "$header" > "$encrypted"
+ # Atomic publication refuses overwrite, including symlinks.
+ ln "$encrypted" "$destination"
+ chown "$owner:$(id -gn "$owner")" "$destination"
+ echo "SOPS-encrypted header backup: $destination"
+ ;;
status)
cryptsetup status "$mapper" || true
findmnt --mountpoint "$target" || true