#!/usr/bin/env bash # One-time setup: put Petal's data directory on an encrypted volume. # # sudo ./setup-encrypted-data.sh # create + migrate + persist # sudo ./setup-encrypted-data.sh --status # report, change nothing # # WHAT THIS DOES AND DOES NOT PROTECT # ----------------------------------- # The volume auto-unlocks from a keyfile stored on the same host. That is a # deliberate choice (availability over paranoia), and it means: # # protects against : a decommissioned or resold disk, someone reading the # raw block device, casual browsing of a filesystem-level # snapshot that does not include /etc # does NOT protect : anyone who takes the whole VM image -- they get # against /etc/petal/dataset.key along with the ciphertext; and # anything at all once the host is running and mounted # # For real protection against a provider-side snapshot the key has to live off # the box (fetched over the VPN at boot). That was considered and not chosen. # # WHY LUKS-ON-A-FILE RATHER THAN gocryptfs # ---------------------------------------- # Petal is SQLite in WAL mode. WAL needs a shared-memory index (-shm) mapped # consistently across processes, and FUSE filesystems have a long history of # subtle mmap/locking differences. A LUKS block device with ext4 on top behaves # exactly like a normal disk to SQLite, which is the only guarantee worth having # under a database. set -euo pipefail IMG="${IMG:-/var/lib/petal-crypt.img}" SIZE="${SIZE:-8G}" MAPPER_NAME="${MAPPER_NAME:-petal-data}" KEYFILE="${KEYFILE:-/etc/petal/dataset.key}" MOUNTPOINT="${MOUNTPOINT:-/home/reala/petal/data}" STACK_DIR="${STACK_DIR:-/home/reala/petal}" OWNER_UID="${OWNER_UID:-1001}" OWNER_GID="${OWNER_GID:-1001}" [ "$(id -u)" -eq 0 ] || { echo "must run as root" >&2; exit 1; } # systemd-cryptsetup ships the generator that turns /etc/crypttab into units. # On a minimal Debian it is NOT installed, and without it crypttab is silently # ignored -- the volume simply never unlocks at boot. Found the hard way. if [ ! -x /usr/lib/systemd/system-generators/systemd-cryptsetup-generator ]; then echo "!! systemd-cryptsetup-generator is missing: /etc/crypttab would be ignored at boot." echo " install it first: apt-get install systemd-cryptsetup" exit 1 fi status() { echo "image : $IMG $( [ -f "$IMG" ] && echo "($(du -h --apparent-size "$IMG" | cut -f1) apparent, $(du -h "$IMG" | cut -f1) on disk)" || echo "(absent)")" echo "mapper : /dev/mapper/$MAPPER_NAME $( [ -e "/dev/mapper/$MAPPER_NAME" ] && echo "(open)" || echo "(closed)")" echo "keyfile : $KEYFILE $( [ -f "$KEYFILE" ] && echo "(present, mode $(stat -c%a "$KEYFILE"))" || echo "(absent)")" echo "mountpoint : $MOUNTPOINT $(mountpoint -q "$MOUNTPOINT" && echo "(mounted)" || echo "(NOT mounted)")" grep -q "^$MAPPER_NAME " /etc/crypttab 2>/dev/null && echo "crypttab : present" || echo "crypttab : MISSING" grep -q " $MOUNTPOINT " /etc/fstab 2>/dev/null && echo "fstab : present" || echo "fstab : MISSING" } if [ "${1:-}" = "--status" ]; then status; exit 0; fi if [ -f "$IMG" ]; then echo "$IMG already exists — refusing to re-create. Use --status." >&2 exit 1 fi echo ">> stopping the stack so nothing is writing to $MOUNTPOINT" if [ -f "$STACK_DIR/docker-compose.yml" ]; then ( cd "$STACK_DIR" && docker compose down ) fi echo ">> generating keyfile $KEYFILE (root-only)" install -d -m 0700 "$(dirname "$KEYFILE")" if [ ! -f "$KEYFILE" ]; then dd if=/dev/urandom of="$KEYFILE" bs=512 count=1 status=none chmod 0400 "$KEYFILE" fi echo ">> creating $SIZE sparse image at $IMG" truncate -s "$SIZE" "$IMG" chmod 0600 "$IMG" echo ">> LUKS format + open" cryptsetup luksFormat --type luks2 --batch-mode --key-file "$KEYFILE" "$IMG" cryptsetup luksOpen --key-file "$KEYFILE" "$IMG" "$MAPPER_NAME" echo ">> mkfs + mount" mkfs.ext4 -q -L petal-data "/dev/mapper/$MAPPER_NAME" # Preserve whatever is already in the plaintext directory, then swap it in. STAGING="" if [ -d "$MOUNTPOINT" ] && [ -n "$(ls -A "$MOUNTPOINT" 2>/dev/null)" ]; then STAGING="$(mktemp -d)" echo ">> preserving existing plaintext data -> $STAGING" cp -a "$MOUNTPOINT/." "$STAGING/" # Critical, and easy to miss: mounting over a directory HIDES its contents, # it does not remove them. Skip this and the original plaintext petal.db sits # on the unencrypted root filesystem forever, invisible under the mount, # defeating the entire exercise. Clear the mountpoint before mounting. echo ">> shredding the plaintext originals under the mountpoint" find "$MOUNTPOINT" -mindepth 1 -type f -exec shred -uz {} + 2>/dev/null || true find "$MOUNTPOINT" -mindepth 1 -depth -type d -exec rmdir {} + 2>/dev/null || true [ -z "$(ls -A "$MOUNTPOINT" 2>/dev/null)" ] || { echo "!! $MOUNTPOINT is not empty after cleanup; refusing to mount over live data" >&2 echo " (data is preserved at $STAGING)" >&2 exit 1 } fi mkdir -p "$MOUNTPOINT" mount "/dev/mapper/$MAPPER_NAME" "$MOUNTPOINT" if [ -n "$STAGING" ]; then echo ">> restoring data onto the encrypted volume" cp -a "$STAGING/." "$MOUNTPOINT/" find "$STAGING" -type f -exec shred -uz {} + 2>/dev/null || true rm -rf "$STAGING" fi # Mount-liveness sentinel: docker-compose bind-mounts this file with # create_host_path:false, so an unmounted volume becomes a loud container start # failure rather than Petal quietly serving an empty database. touch "$MOUNTPOINT/.volume-ok" chown -R "$OWNER_UID:$OWNER_GID" "$MOUNTPOINT" echo ">> persisting across reboots" # systemd-cryptsetup loop-mounts a regular file source on its own. if ! grep -q "^$MAPPER_NAME " /etc/crypttab 2>/dev/null; then echo "$MAPPER_NAME $IMG $KEYFILE luks,nofail" >> /etc/crypttab fi # nofail: a problem here must never wedge the boot of a host running half a # dozen other services. # x-systemd.before=docker.service is the important one: without it Docker can # start first, find $MOUNTPOINT empty, and bring Petal up against a blank # unencrypted directory that the real volume then hides. if ! grep -q " $MOUNTPOINT " /etc/fstab 2>/dev/null; then echo "/dev/mapper/$MAPPER_NAME $MOUNTPOINT ext4 defaults,nofail,x-systemd.requires=/dev/mapper/$MAPPER_NAME,x-systemd.before=docker.service 0 2" >> /etc/fstab fi systemctl daemon-reload echo ">> restarting the stack" if [ -f "$STACK_DIR/docker-compose.yml" ]; then ( cd "$STACK_DIR" && docker compose up -d ) fi echo status