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

/mnt/seagate/

Frequency

Daily

Passphrase

gopass show v3/domains/d000/storage/seagate/primary

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

~/.secrets/ (dsec vaults)
~/.gnupg/ (GPG keyring)
~/.password-store/ (gopass)
~/.ssh/ (SSH keys)
~/.pki/ (certificates)

~235MB

P0 - Critical

@configs

~/.config/ (app configs)
~/.local/ (local data)
~/.ansible/ (playbooks)
~/.claude/ (Claude settings)
~/.terraform.d/ (TF plugins)
~/bin/ (personal scripts)
Shell history files

~7GB

P1 - Important

@backups

~/.mozilla/ (Firefox profiles, bookmarks, sessions)

~700MB

P2 - Convenience

@storage

~/atelier/ (all repos, docs, projects)
~/Documents/
~/Pictures/

~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

-a

Archive mode: preserves permissions, timestamps, symlinks, owner/group

-v

Verbose: show files being transferred

--delete

Delete files in dest that don’t exist in source (true mirror)

--exclude='pattern'

Skip matching files/dirs (oil.nvim, trash, borg cache)

2>/dev/null || true

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:

  1. sync - Ensures all writes complete before unmount

  2. umount - Release all mounts before closing LUKS

  3. 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/