Seagate Primary SSD
Overview
Primary external SSD for daily encrypted backups. Uses LUKS2 encryption with btrfs subvolumes for organized, compressed storage.
| Property | Value |
|---|---|
Capacity |
2TB USB-C |
Encryption |
LUKS2 (aes-xts-plain64, 256-bit) |
Filesystem |
btrfs with zstd:3 compression |
Mount Point |
|
Frequency |
Daily |
Passphrase |
|
Why This Architecture
LUKS2 Encryption
LUKS2 provides full-disk encryption at the block layer:
-
AES-XTS-Plain64 - Industry standard for disk encryption (256-bit)
-
Argon2id KDF - Memory-hard key derivation (resists GPU attacks)
-
Header contains - Salt, cipher params, encrypted master key
-
Passphrase → KDF → Master Key → Data Encryption
Without the passphrase, the entire drive is cryptographic noise.
btrfs Subvolumes
Subvolumes provide logical separation without partitioning:
| Benefit | Why It Matters |
|---|---|
Independent snapshots |
Snapshot secrets without snapshotting 20GB of atelier |
Compression per-subvolume |
zstd:3 gives ~30% space savings on text/configs |
Mount flexibility |
Mount only what you need for recovery |
No resize operations |
Subvolumes share the pool - no partition resizing ever |
Subvolume Structure
/dev/mapper/seagate-crypt (LUKS2 container)
└── btrfs filesystem
├── @secrets → /mnt/seagate/secrets/ (critical)
├── @configs → /mnt/seagate/configs/ (important)
├── @backups → /mnt/seagate/backups/ (convenience)
├── @storage → /mnt/seagate/storage/ (bulk)
└── @snapshots → (internal, read-only snapshots)
Contents Breakdown
| Subvolume | Source Directories | Size | Recovery Priority |
|---|---|---|---|
@secrets |
|
~235MB |
P0 - Critical |
@configs |
|
~7GB |
P1 - Important |
@backups |
|
~700MB |
P2 - Convenience |
@storage |
|
~20GB |
P3 - Bulk |
Script Internals
seagate-primary-mount
What the script does step-by-step:
#!/bin/bash
set -euo pipefail # Exit on error, undefined var, pipe fail
DEVICE="${1:-/dev/sdb1}" # Default device, overridable
MAPPER_NAME="seagate-crypt" # dm-crypt mapper name
MOUNT_BASE="/mnt/seagate" # Mount point base
# Check if already mounted (idempotent)
if mountpoint -q "$MOUNT_BASE/secrets" 2>/dev/null; then
echo "Already mounted at $MOUNT_BASE/"
exit 0
fi
# Verify device exists before attempting LUKS open
if [[ ! -b "$DEVICE" ]]; then
echo "Device $DEVICE not found. Check lsblk for correct device."
exit 1
fi
# Open LUKS container - prompts for passphrase
# Creates /dev/mapper/seagate-crypt
sudo cryptsetup open "$DEVICE" "$MAPPER_NAME"
# Create mount points
sudo mkdir -p "$MOUNT_BASE"/{secrets,configs,backups,storage}
# Mount each subvolume with compression
# -o compress=zstd:3 - level 3 zstd (good balance)
# -o subvol=@name - mount specific subvolume
sudo mount -o compress=zstd:3,subvol=@secrets /dev/mapper/$MAPPER_NAME $MOUNT_BASE/secrets
sudo mount -o compress=zstd:3,subvol=@configs /dev/mapper/$MAPPER_NAME $MOUNT_BASE/configs
sudo mount -o compress=zstd:3,subvol=@backups /dev/mapper/$MAPPER_NAME $MOUNT_BASE/backups
sudo mount -o compress=zstd:3,subvol=@storage /dev/mapper/$MAPPER_NAME $MOUNT_BASE/storage
# Set ownership so rsync works without sudo
sudo chown -R $USER:$USER $MOUNT_BASE/*
Key concepts:
-
cryptsetup open- Decrypts LUKS header, creates mapper device -
subvol=@name- btrfs mounts specific subvolume, not root -
compress=zstd:3- Transparent compression on write -
Same mapper device mounted 4 times with different subvols
seagate-primary-backup
What the backup script does:
#!/bin/bash
set -euo pipefail
MOUNT_BASE="/mnt/seagate"
# Verify mounted before attempting backup
if ! mountpoint -q "$MOUNT_BASE/secrets" 2>/dev/null; then
echo "Seagate not mounted. Run seagate-primary-mount first."
exit 1
fi
# === SECRETS (@secrets subvolume) ===
# These are the critical credentials - age keys, GPG, SSH, gopass
rsync -av --delete ~/.secrets/ $MOUNT_BASE/secrets/
rsync -av --delete ~/.gnupg/ $MOUNT_BASE/secrets/gnupg/
rsync -av --delete ~/.password-store/ $MOUNT_BASE/secrets/password-store/
rsync -av --delete ~/.ssh/ $MOUNT_BASE/secrets/ssh/
rsync -av --delete ~/.pki/ $MOUNT_BASE/secrets/pki/
# === CONFIGS (@configs subvolume) ===
# Application configs, local data, shell history
rsync -av --delete --exclude='borg' ~/.config/ $MOUNT_BASE/configs/dotconfig/
rsync -av --delete ~/.local/ $MOUNT_BASE/configs/dotlocal/
rsync -av --delete ~/.ansible/ $MOUNT_BASE/configs/ansible/
rsync -av --delete ~/.claude/ $MOUNT_BASE/configs/claude/
rsync -av --delete ~/.terraform.d/ $MOUNT_BASE/configs/terraform.d/ 2>/dev/null || true
rsync -av --delete ~/bin/ $MOUNT_BASE/configs/bin/
rsync -av ~/.zsh_history ~/.bash_history $MOUNT_BASE/configs/
# === BACKUPS (@backups subvolume) ===
rsync -av --delete ~/.mozilla/ $MOUNT_BASE/backups/mozilla/
# === STORAGE (@storage subvolume) ===
# Excludes: oil.nvim temp dirs, FUSE trash
rsync -av --delete --exclude='oil' --exclude='.Trash-*' ~/atelier/ $MOUNT_BASE/storage/atelier/
rsync -av --delete ~/Documents/ $MOUNT_BASE/storage/Documents/
rsync -av --delete ~/Pictures/ $MOUNT_BASE/storage/Pictures/ 2>/dev/null || true
rsync flags explained:
| Flag | Purpose |
|---|---|
|
Archive mode: preserves permissions, timestamps, symlinks, owner/group |
|
Verbose: show files being transferred |
|
Delete files in dest that don’t exist in source (true mirror) |
|
Skip matching files/dirs (oil.nvim, trash, borg cache) |
|
Suppress errors for optional dirs that may not exist |
Why --delete matters: Without it, deleted files on source remain on backup. With it, backup is exact mirror.
seagate-primary-snapshot
Creates read-only btrfs snapshots for point-in-time recovery:
#!/bin/bash
set -euo pipefail
MOUNT_BASE="/mnt/seagate"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
MAPPER_NAME="seagate-crypt"
SNAPSHOT_MNT="/tmp/seagate-snapshot-$$"
# Must temporarily mount btrfs root to access all subvolumes
sudo mkdir -p "$SNAPSHOT_MNT"
sudo mount -o compress=zstd:3 /dev/mapper/$MAPPER_NAME "$SNAPSHOT_MNT"
# Ensure snapshot directory exists
sudo mkdir -p "$SNAPSHOT_MNT/@snapshots"
# Create read-only snapshot of each subvolume
# -r = read-only (immutable)
for subvol in secrets configs backups storage; do
sudo btrfs subvolume snapshot -r \
"$SNAPSHOT_MNT/@$subvol" \
"$SNAPSHOT_MNT/@snapshots/${subvol}_$TIMESTAMP"
done
# Cleanup temp mount
sudo umount "$SNAPSHOT_MNT"
rmdir "$SNAPSHOT_MNT"
Snapshot concepts:
-
Snapshots are instant (copy-on-write, no data copied)
-
Read-only (
-r) prevents accidental modification -
Each subvolume snapshots independently
-
Stored in
@snapshots/subvolume
seagate-primary-umount
Safe unmount sequence:
#!/bin/bash
set -euo pipefail
MOUNT_BASE="/mnt/seagate"
MAPPER_NAME="seagate-crypt"
echo "Syncing filesystems..."
sync # Flush all pending writes to disk
echo "Unmounting subvolumes..."
sudo umount $MOUNT_BASE/secrets
sudo umount $MOUNT_BASE/configs
sudo umount $MOUNT_BASE/backups
sudo umount $MOUNT_BASE/storage
echo "Closing LUKS volume..."
sudo cryptsetup close $MAPPER_NAME
echo "Done. Safe to remove drive."
Why order matters:
-
sync- Ensures all writes complete before unmount -
umount- Release all mounts before closing LUKS -
cryptsetup close- Flushes dm-crypt buffers, removes mapper
Daily Workflow
# 1. Mount (enter passphrase)
seagate-primary-mount
# 2. Run backup
seagate-primary-backup
# 3. Optional: create snapshot before risky changes
seagate-primary-snapshot
# 4. Verify
du -sh /mnt/seagate/{secrets,configs,backups,storage}
# 5. Unmount
seagate-primary-umount
Initial Setup
One-time setup for a new drive:
# 1. Create LUKS2 container with strong encryption
# --type luks2: Modern format with Argon2id KDF
# --cipher aes-xts-plain64: Standard disk encryption
# --key-size 512: 256-bit AES (XTS uses 2 keys)
sudo cryptsetup luksFormat --type luks2 \
--cipher aes-xts-plain64 \
--key-size 512 \
--hash sha256 \
--iter-time 5000 \
/dev/sdX1
# 2. Open the container
sudo cryptsetup open /dev/sdX1 seagate-crypt
# 3. Create btrfs filesystem
sudo mkfs.btrfs -L seagate-primary /dev/mapper/seagate-crypt
# 4. Mount and create subvolumes
sudo mount /dev/mapper/seagate-crypt /mnt
sudo btrfs subvolume create /mnt/@secrets
sudo btrfs subvolume create /mnt/@configs
sudo btrfs subvolume create /mnt/@backups
sudo btrfs subvolume create /mnt/@storage
sudo btrfs subvolume create /mnt/@snapshots
sudo umount /mnt
# 5. Close
sudo cryptsetup close seagate-crypt
LUKS Header Backup
|
The LUKS header contains the encrypted master key. If corrupted, ALL data is lost - even with correct passphrase. Always backup headers before any disk operation. |
# Backup header (16MB for LUKS2)
sudo cryptsetup luksHeaderBackup /dev/sdX1 \
--header-backup-file seagate-ssd1-$(date +%Y%m%d).img
# Encrypt with age before storing
age -e -R ~/.config/age/recipients.txt \
-o ~/.secrets/luks-headers/seagate-ssd1-$(date +%Y%m%d).img.age \
seagate-ssd1-$(date +%Y%m%d).img
# Securely delete plaintext header
shred -vzn 3 seagate-ssd1-$(date +%Y%m%d).img
Recovery Scenarios
Restore Single File
seagate-primary-mount
cp /mnt/seagate/secrets/ssh/id_ed25519 ~/.ssh/
seagate-primary-umount
Restore from Snapshot
# Mount btrfs root
sudo mount /dev/mapper/seagate-crypt /mnt
# List snapshots
ls /mnt/@snapshots/
# Copy file from snapshot
cp /mnt/@snapshots/configs_20260217_201500/dotconfig/nvim/init.lua ~/.config/nvim/
sudo umount /mnt
Full Secrets Recovery
seagate-primary-mount
rsync -av /mnt/seagate/secrets/ ~/.secrets/
rsync -av /mnt/seagate/secrets/gnupg/ ~/.gnupg/
rsync -av /mnt/seagate/secrets/password-store/ ~/.password-store/
rsync -av /mnt/seagate/secrets/ssh/ ~/.ssh/
chmod 700 ~/.ssh && chmod 600 ~/.ssh/id_*
seagate-primary-umount
Verification
After backup, verify sizes match expectations:
du -sh /mnt/seagate/secrets /mnt/seagate/configs /mnt/seagate/backups /mnt/seagate/storage
du -sh /mnt/seagate/
Expected output (~28GB total):
235M /mnt/seagate/secrets
6.9G /mnt/seagate/configs
692M /mnt/seagate/backups
20G /mnt/seagate/storage
28G /mnt/seagate/