#!/bin/bash set -euo pipefail umask 077 stage="$(cd "$(dirname "$0")" && pwd)" mountpoint -q /mnt/data || { echo 'External disk is not mounted'; exit 1; } findmnt -n -o OPTIONS /mnt/data | tr ',' '\n' | grep -qx rw test -f "$stage/token.sha256" if ! id qsafe >/dev/null 2>&1; then useradd --system --home-dir /nonexistent --shell /usr/sbin/nologin qsafe; fi install -d -o root -g root -m 755 /opt/qsafe install -d -o qsafe -g qsafe -m 700 /mnt/data/qsafe/data install -d -o root -g qsafe -m 750 /etc/qsafe install -d -o root -g root -m 700 /var/backups/qsafe /var/backups/qsafe-install install -m 644 "$stage/server.py" "$stage/backup.py" /opt/qsafe/ if test -f /etc/qsafe/token.sha256; then cmp -s "$stage/token.sha256" /etc/qsafe/token.sha256 || { echo 'Existing connection key differs; refusing to rotate it'; exit 1; } else install -o root -g qsafe -m 640 "$stage/token.sha256" /etc/qsafe/token.sha256 fi cat > /etc/systemd/system/qsafe.service <<'EOF' [Unit] Description=QSafe encrypted vault synchronization After=network.target RequiresMountsFor=/mnt/data/qsafe/data [Service] Type=simple User=qsafe Group=qsafe UMask=0077 ExecStart=/usr/bin/python3 /opt/qsafe/server.py --data /mnt/data/qsafe/data --token-hash-file /etc/qsafe/token.sha256 Restart=on-failure RestartSec=3 NoNewPrivileges=true PrivateTmp=true ProtectSystem=strict ProtectHome=true ReadWritePaths=/mnt/data/qsafe/data RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX MemoryMax=256M TasksMax=48 [Install] WantedBy=multi-user.target EOF cat > /etc/systemd/system/qsafe-backup.service <<'EOF' [Unit] Description=QSafe verified encrypted backup to SD card RequiresMountsFor=/mnt/data/qsafe/data [Service] Type=oneshot UMask=0077 ExecStart=/usr/bin/python3 /opt/qsafe/backup.py /mnt/data/qsafe/data/vault.sqlite3 /var/backups/qsafe NoNewPrivileges=true ProtectSystem=strict ProtectHome=true ReadWritePaths=/var/backups/qsafe /mnt/data/qsafe/data EOF cat > /etc/systemd/system/qsafe-backup.timer <<'EOF' [Unit] Description=Daily QSafe encrypted backup [Timer] OnCalendar=*-*-* 04:25:00 Persistent=true RandomizedDelaySec=300 [Install] WantedBy=timers.target EOF : "${QSAFE_LAN_IP:?Set the Pi private IPv4; public vault endpoints are forbidden}" python3 "$stage/lan-only.py" --ip "$QSAFE_LAN_IP" systemctl daemon-reload systemctl enable --now qsafe.service qsafe-backup.timer systemctl restart qsafe.service for i in $(seq 1 20); do if curl -fsS http://127.0.0.1:18792/health; then break; fi sleep 1 done systemctl start qsafe-backup.service systemctl is-active qsafe.service caddy.service qsafe-backup.timer