schemes / LUKS Dropbox: Key + Vault

LUKS Dropbox: Key + Vault

2025-04-23 updated 2026-04-23 15:11 luks raspberry-pi tailscale

Two-device encrypted storage scheme. A Raspberry Pi "vault" hosts a LUKS-encrypted partition that only unlocks when a second "key" device is physically present on the local network. Remote access via Tailscale, locked down with UFW + Fail2Ban.

Customize for your environment

Fill in your values below — the code blocks update live as you type. All fields are required.

.local

This scheme uses two devices on a local network:

  • key — a small always-on device that holds half the unlock credential and broadcasts a private Wi-Fi hotspot
  • vault — the main storage device with a LUKS-encrypted partition that auto-unlocks at boot by reaching the key over the hotspot, then switches to your home Wi-Fi

The full unlock credential is derived by combining the vault’s CPU serial with the key’s CPU serial + stored passphrase. Neither device alone holds the complete key.

Hardware

I devised this scheme using the following hardware. Your mileage may vary with others.

Component Role Notes
Raspberry Pi 4B Vault Main storage device
Raspberry Pi Zero 2 Key Always-on unlock device
128 GB micro-SD card. Vault storage Raspberry Pi OS Lite Trixie
16 GB micro-SD card Key storage Raspberry Pi OS Lite Trixie
USB-A stick Vault boot Ubuntu flashed via Raspberry Pi Imager

Phase 1 — Key device setup

Prepare the key device: harden it, create the service account the vault will SSH into, and enable temporary password auth so the vault can push its public key.

sudo apt update
sudo apt full-upgrade -y
sudo apt install ufw fail2ban screen -y

sudo adduser --disabled-password --gecos "" unlocker && \
echo "unlocker:password" | sudo chpasswd

sudo ufw default allow outgoing
sudo ufw default deny incoming
sudo ufw allow ssh
sudo ufw limit ssh/tcp
sudo ufw --force enable

sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local

sudo sed 's/PasswordAuthentication no/PasswordAuthentication yes/g' \
  /etc/ssh/sshd_config.d/50-cloud-init.conf \
  | sudo tee /etc/ssh/sshd_config.d/50-cloud-init.conf && \
sudo systemctl restart ssh
# we'll be using screen in the next section related to the key
screen -q

Phase 2 — Vault: initial boot

On first boot, open crontab once to initialize cron infrastructure, then shut down cleanly.

crontab -e   # open once to initialize — do NOT save a job
sudo shutdown -h now

Phase 3 — Vault: partition resize and LUKS setup

This script shrinks the root filesystem to 8 GB, creates a new ~60 GB LUKS partition, formats it, then reboots. Run from recovery or initramfs — the root filesystem must not be mounted.

cat << 'EOF' > resize_and_luks.sh
#!/bin/bash
set -Eeuo pipefail

trap 'echo "Failed at line $LINENO"; exit 1' ERR

sudo e2fsck -fp /dev/mmcblk0p2
sudo resize2fs /dev/mmcblk0p2 8G

sudo parted /dev/mmcblk0 resizepart 2 60GB

sudo resize2fs /dev/mmcblk0p2
sudo e2fsck -f /dev/mmcblk0p2

sudo parted /dev/mmcblk0 mkpart primary ext4 64GB 124GB

echo "Starting LUKS encryption. Please follow the prompts below:"
sudo cryptsetup luksFormat /dev/mmcblk0p3 && \
sudo cryptsetup open /dev/mmcblk0p3 encrypted_data && \
sudo mkfs.ext4 /dev/mapper/encrypted_data && \
sudo cryptsetup close encrypted_data
EOF

chmod +x resize_and_luks.sh
./resize_and_luks.sh && \
rm ./resize_and_luks.sh && \
echo "Process complete. Rebooting..." && \
sleep 3 && \
sudo reboot

Phase 4 — Vault: full provisioning

Back on the vault. This script installs Tailscale, configures the firewall, generates the service SSH keypair, pushes the public key to the key device, derives the two-part unlock credential, adds a second LUKS key slot, writes the unlock-and-start.sh and lock-and-stop.sh helpers, installs systemd services for shutdown-time locking, and configures cron to auto-unlock on boot.

cat >./setup.sh <<'SCRIPT_EOF'
#!/usr/bin/env bash
set -Eeuo pipefail

on_err() {
  local exit_code=$?
  echo
  echo "FAILED"
  echo "Line: $1"
  echo "Command: $2"
  echo "Exit code: $exit_code"
  echo "Script kept at: $0"
  exit "$exit_code"
}
trap 'on_err $LINENO "$BASH_COMMAND"' ERR

read -r -s -p "LUKS Service passphrase: " PW
echo

sudo apt update
sudo apt full-upgrade -y
sudo apt autoremove --purge -y
sudo apt autoclean -y
sudo apt install cryptsetup ufw fail2ban screen sshpass -y

curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale set --operator="$USER"

sudo ufw default allow outgoing
sudo ufw default deny incoming
sudo ufw allow ssh
sudo ufw limit ssh/tcp
sudo ufw allow in on tailscale0
sudo ufw --force enable

sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local

cat <<'EOF' | sudo tee -a /etc/fail2ban/jail.local >/dev/null
[sshd]
enabled  = true
port     = ssh
filter   = sshd
backend  = systemd
maxretry = 6
EOF

sudo mkdir -p /mnt/secure
sudo chown -R "$USER:$USER" /mnt/secure

set +o history

ssh-keygen -t ed25519 -C "service-account-unlocker" -f ~/.ssh/id_ed25519 -N ""
sshpass -p 'password' ssh-copy-id -o StrictHostKeyChecking=accept-new unlocker@keyhostname.local

REMOTE_KEY="$(
  ssh -q -o ConnectTimeout=20 unlocker@keyhostname.local \
    "printf '%s' '$PW' > .key && tr -d '\n' < .key && grep Serial /proc/cpuinfo | cut -d ' ' -f 2"
)"

CPU="$(grep Serial /proc/cpuinfo | cut -d ' ' -f 2)"
FULL_KEY="${CPU}${REMOTE_KEY}"
echo "passcode: $FULL_KEY"

echo "Adding service key (second LUKS key slot)..."
sudo cryptsetup luksAddKey /dev/mmcblk0p3

sudo tee /usr/local/bin/unlock-and-start.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
set -Eeuo pipefail

REMOTE_KEY="$(
  ssh -q -o ConnectTimeout=20 unlocker@keyhostname.local \
    "tr -d '\n' < .key && grep Serial /proc/cpuinfo | cut -d ' ' -f 2"
)"

if [ -z "$REMOTE_KEY" ]; then
    echo "Connection failed or Serial empty. Aborting."
    exit 1
fi

CPU="$(grep Serial /proc/cpuinfo | cut -d ' ' -f 2)"
FULL_KEY="${CPU}${REMOTE_KEY}"

echo "Decrypting LUKS partition..."
printf '%s' "$FULL_KEY" | sudo cryptsetup open /dev/mmcblk0p3 secure_drive --key-file -

if [ -d "/mnt/secure" ]; then
    sudo mount /dev/mapper/secure_drive /mnt/secure
    echo "Partition mounted at /mnt/secure"
else
    echo "Error: /mnt/secure directory does not exist. Aborting."
    exit 1
fi

nmcli connection down "ciphernet"
nmcli connection modify "netplan-wlan0-homewifi" connection.metered yes connection.autoconnect yes
nmcli connection up "netplan-wlan0-homewifi"

tailscale up

echo "vault unlocked and online."
EOF

sudo chmod +x /usr/local/bin/unlock-and-start.sh

cat <<'EOF' | sudo tee /etc/sudoers.d/username-unlock >/dev/null
username ALL=(ALL) NOPASSWD: /usr/sbin/cryptsetup open /dev/mmcblk0p3 *, /usr/bin/mount /dev/mapper/* /mnt/secure, /usr/bin/tailscale up *, /usr/bin/nmcli *
EOF

sudo chmod 440 /etc/sudoers.d/username-unlock
sudo visudo -cf /etc/sudoers.d/username-unlock

sudo cryptsetup open /dev/mmcblk0p3 secure_drive
sudo mount /dev/mapper/secure_drive /mnt/secure
sudo chown -R "$USER:$USER" /mnt/secure
sudo umount /mnt/secure
sudo cryptsetup close secure_drive

sudo tee /usr/local/bin/lock-and-stop.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
set -Eeuo pipefail

echo "Unmounting /mnt/secure..."
sudo umount /mnt/secure

echo "Closing LUKS partition..."
sudo cryptsetup close secure_drive

echo "System locked successfully."
EOF

sudo chmod +x /usr/local/bin/lock-and-stop.sh

sudo tee /etc/systemd/system/luks-halt.service >/dev/null <<'EOF'
[Unit]
Description=Cleanly stop apps, unmount, and close LUKS before system halt
DefaultDependencies=no
Before=shutdown.target reboot.target halt.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/lock-and-stop.sh
RemainAfterExit=yes

[Install]
WantedBy=halt.target reboot.target shutdown.target
EOF

sudo systemctl enable luks-halt.service

sudo tee /usr/local/bin/boot.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
set -Eeuo pipefail
sudo nmcli connection down "netplan-wlan0-homewifi"
sudo nmcli connection modify "netplan-wlan0-homewifi" \
  connection.metered no \
  connection.autoconnect no
sudo nmcli connection modify "ciphernet" \
  connection.autoconnect no
sudo nmcli connection up "ciphernet"
EOF

sudo chmod +x /usr/local/bin/boot.sh

sudo tee /etc/systemd/system/boot-connection.service >/dev/null <<'EOF'
[Unit]
Description=Set NetworkManager connection properties
After=network.target NetworkManager.service
Wants=NetworkManager.service

[Service]
Type=oneshot
ExecStart=/usr/local/bin/boot.sh
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl enable boot-connection.service

echo '*/2 * * * * /usr/local/bin/unlock-and-start.sh >> /tmp/unlock.log 2>&1' | crontab -

sudo tailscale up
SCRIPT_EOF

chmod +x ./setup.sh
echo "Running ./setup.sh"
sudo bash ./setup.sh && \
rm ./setup.sh && \
echo "Success. Temp script removed." && \
# we'll use screen for the next vault step
screen -q

Phase 5 — Key: lock down SSH and create hotspot

Disables password SSH, restricts the unlocker account to key-only auth, and creates the ciphernet Wi-Fi hotspot.

sudo sed 's/PasswordAuthentication yes/PasswordAuthentication no/g' \
  /etc/ssh/sshd_config.d/50-cloud-init.conf \
  | sudo tee /etc/ssh/sshd_config.d/50-cloud-init.conf && \

sudo tee /etc/ssh/sshd_config > /dev/null << 'EOF'
Match User unlocker
    PasswordAuthentication no
    AuthenticationMethods publickey
    AllowTcpForwarding no
    X11Forwarding no
EOF
sudo systemctl restart ssh

sudo nmcli connection add con-name ciphernet \
  type wifi \
  ifname wlan0 \
  ssid ciphernet \
  802-11-wireless.mode ap \
  802-11-wireless.band bg \
  wifi-sec.pairwise ccmp \
  wifi-sec.proto rsn \
  wifi-sec.key-mgmt wpa-psk \
  wifi-sec.psk secure-wifi-pw \
  ipv4.method shared \
  ipv4.address 192.168.7.1/24 \
  autoconnect yes

sudo nmcli connection delete "netplan-wlan0-homewifi"

sudo nmcli connection up ciphernet

Phase 6 — Vault: connect to ciphernet hotspot

On the vault, wait for the key’s hotspot to appear (re-run the rescan line every minute or so), then lock in the static IP and reboot.

sudo nmcli dev wifi rescan
sudo nmcli connection modify "netplan-wlan0-homewifi" \
  connection.metered no \
  connection.autoconnect no

sudo nmcli connection add type wifi \
  ifname wlan0 \
  con-name ciphernet \
  ssid ciphernet 

sudo nmcli connection modify "ciphernet" \
  wifi-sec.key-mgmt wpa-psk \
  wifi-sec.psk sisima18 \
  ipv4.method manual \
  ipv4.address 192.168.7.3/24 \
  connection.autoconnect yes

sudo reboot