#!/usr/bin/env bash # Backs up the ASP.NET Data Protection keyring. Without this keyring, PHI # encrypted by Phase 32 cannot be decrypted — a database backup alone is not # sufficient to restore the system. See docs/ops/phi-encryption-runbook.md. # # Volume name matches docker-compose.prod.yml `name: vigilcare` → vigilcare_dp_keys. # Do not derive the name from the host directory; /opt/vigilcare would otherwise # produce a different volume than a checkout named VigilCareClinical. # # Usage (on the production host): # ./scripts/backup-dp-keys.sh # BACKUP_DIR=/mnt/offsite/vigilcare/dp-keys ./scripts/backup-dp-keys.sh # # Cron example (daily 02:15 UTC): # 15 2 * * * /opt/vigilcare/scripts/backup-dp-keys.sh >> /var/log/vigilcare-dp-backup.log 2>&1 set -euo pipefail VOLUME_NAME="${VOLUME_NAME:-vigilcare_dp_keys}" BACKUP_DIR="${BACKUP_DIR:-/var/backups/vigilcare/dp-keys}" STAMP="$(date -u +%Y%m%dT%H%M%SZ)" ARCHIVE="${BACKUP_DIR}/dp-keys-${STAMP}.tar.gz" if ! docker volume inspect "${VOLUME_NAME}" >/dev/null 2>&1; then echo "ERROR: Docker volume '${VOLUME_NAME}' not found." >&2 echo "Confirm docker-compose.prod.yml uses 'name: vigilcare' and the API has started once." >&2 exit 1 fi mkdir -p "${BACKUP_DIR}" chmod 700 "${BACKUP_DIR}" echo "Backing up volume ${VOLUME_NAME} → ${ARCHIVE}" docker run --rm \ -v "${VOLUME_NAME}:/keys:ro" \ -v "${BACKUP_DIR}:/backup" \ alpine tar czf "/backup/dp-keys-${STAMP}.tar.gz" -C /keys . chmod 600 "${ARCHIVE}" # Refuse empty archives (volume had no key XML yet). if ! tar tzf "${ARCHIVE}" | grep -q '\.xml$'; then echo "ERROR: archive contains no *.xml key files — refusing to keep empty backup." >&2 rm -f "${ARCHIVE}" exit 1 fi echo "Keyring backed up to ${ARCHIVE}" # Retain 30 days. find "${BACKUP_DIR}" -name 'dp-keys-*.tar.gz' -mtime +30 -delete echo "Retention: removed archives older than 30 days under ${BACKUP_DIR}"