Backup and restore
LOSPOR Hospital 1.2 creates an authenticated PostgreSQL recovery object every
four hours. A completed object is a private directory named
backups/lospor-YYYYMMDDTHHMMSSZ-RANDOM.backup containing the custom-format
database dump and a manifest published last. The manifest binds the dump hash
to the site, appliance, Hospital release, exchange/data-dictionary versions,
PostgreSQL major, migration and schema fingerprints, and fingerprints of the
clinical keys, the exact OMOP_PSEUDONYM_SALT, and the external-AI seal key.
It contains no raw secret or patient data and is authenticated with a separate
manifest HMAC key.
The scheduler retains every verified object for 48 hours and then one verified
object per UTC day for 14 days. Pre-update, pre-restore, and explicitly
immutable recovery points are protected from ordinary pruning. Cleanup runs
only after a newly verified or safely reused object; a failed dump never
deletes an older recovery point. Capacity is checked before pg_dump and the
manifest is published only after the dump catalog, checksum, schema, and
migration evidence pass.
This gives a four-hour logical-backup RPO target. PostgreSQL WAL/PITR is outside the appliance, so recovery to an arbitrary transaction is not promised.
Operator language
backup-now.sh, ensure-backup-configuration.sh, and restore-backup.sh
render operator instructions and failures in the appliance language selected by
LOSPOR_DEFAULT_LOCALE (Bulgarian by default). A visiting technician can select
English for one command without changing the appliance setting, for example:
sudo env LOSPOR_DEFAULT_LOCALE=en sh /opt/lospor-hospital/current/scripts/backup-now.sh
Only explanatory prose is translated. Stable process and recovery tokens such
as BACKUP_BUSY, BACKUP_MAINTENANCE_BUSY, RESTORE_PREFLIGHT_OK,
NEEDS_OPERATOR, the TEMPORARY RESTORE / EMERGENCY RESTORE typed values,
environment names, journal phases, paths, and commands remain exact in both
languages.
Off-host copy and secret escrow
A local copy is not disaster recovery. secrets/backup/offhost-copy is the
fixed, vendor-neutral hook invoked with the path of each verified object. The
generated hook exits 75, meaning “deferred”; it never falsely acknowledges an
external copy. Hospital IT may replace that regular executable with its own
encrypted off-host transfer. Exit 0 means that exact object is durably
acknowledged, exit 75 defers it, and every other exit is a copy failure. LOSPOR
records only the privacy-safe object name, acknowledgement time, and manifest
hash in .last-offhost-verified.v1.
Encrypted off-host copies
The appliance can make those copies itself, to a network share mounted on the
server (SMB or NFS) or to an SFTP server. A timer checks every 15 minutes. Each
new verified backup is encrypted on the server (AES-256, with an HMAC-SHA256
over the object name, manifest digest and ciphertext) and copied to the
destination. It is then read back and compared, and only then written to
.last-offhost-verified.v1. Set it up from Maintenance in Status, or at the
console.
For a share, Hospital IT mounts it first, under /mnt, /media or /srv.
The adapter refuses a path that is only an ordinary local directory. An SMB
example for /etc/fstab, with the password in a root-only credentials file
(username=, password=, domain= lines, mode 0600, and the cifs-utils
package installed):
//fileserver.hospital.local/lospor-backups /mnt/lospor-backups cifs credentials=/etc/lospor-backups.cred,uid=0,gid=0,file_mode=0600,dir_mode=0700,nofail,_netdev 0 0
For SFTP, configuration generates an SSH key for the appliance and pins the server's host keys. Install the printed public key for the SFTP user, and check the printed host-key fingerprints with the server's administrator. There is no password option.
sudo losporctl backup offhost configure mount /mnt/lospor-backups
sudo losporctl backup offhost configure sftp backup.hospital.local 22 lospor lospor-backups
sudo losporctl backup offhost test
sudo losporctl backup offhost run
sudo losporctl backup offhost drill
sudo losporctl backup offhost state
test stores a probe file, reads it back and deletes it. drill fetches the
newest acknowledged copy, checks its authentication, decrypts it and restores it
into a temporary database, then removes it. That is the quarterly restore from
the real off-host medium described below. The encryption key is
secrets/backup/offhost-encryption.key. It is generated once and never
replaced, because every copy needs it. Escrow secrets/ again after the first
setup: until you do, Status reports the escrow acknowledgement as out of date.
The destination's own retention is Hospital IT's to set.
The hospital must escrow site.env, .env (including the installation's exact
OMOP_PSEUDONYM_SALT), the complete secrets/ directory (including
secrets/backup/manifest-hmac-key and secrets/api/external-ai-seal-key), and
the verified recovery objects in a
separate encrypted, access-controlled system outside the appliance VM and
storage. Monitor local and acknowledged off-host ages independently and carry
out a recorded restore from the real off-host medium at least quarterly.
Escrow the secrets from Status
Maintenance → Secrets escrow → Create the escrow copy. Status asks for the administrator password and a code from the authenticator app, then shows a passphrase once: write it down before leaving that page. The host writes the same encrypted file the console command below writes, decrypts it and requires it to match the files in use, and offers it for 30 minutes to the administrator who asked. Download it to a USB stick on your own computer. The download is reported to the host, which records the acknowledgement Go-live checks and removes the copy from the server.
Because the file and its passphrase together open every secret of the installation, Status offers this only while its own network list is limited to the IT management networks, allows at most three copies in 24 hours (counted by the host), logs every request and download, and notes a download on the overview for a week. A console-recovery session cannot use it.
Escrow the secrets at the console
Plug in a USB stick or mount a share from outside this server, then:
sudo losporctl secrets escrow /media/usb
It writes site.env, .env, advanced.env (when present) and all of
secrets/ as one encrypted file (lospor-hospital-secrets-<time>.tar.gz.enc,
AES-256 with a PBKDF2-derived key) with a .sha256 beside it, decrypts the copy
and requires it to match the files in use, and only then records the
acknowledgement Go-live checks. It refuses a directory on the server's own
disk. The passphrase is generated and shown once: keep it in the IT password
vault, apart from the USB stick. A hospital that manages its own passphrase can
give it with --passphrase-file FILE instead. Run it again after any change to
the secrets: a credential rotation, or the off-host encryption key created by
the first off-host setup.
To restore on a replacement server, as root in /opt/lospor-hospital:
openssl enc -d -aes-256-cbc -pbkdf2 -iter 600000 -md sha256 -in lospor-hospital-secrets-<time>.tar.gz.enc | tar -xz
Secrets escrowed another way are recorded with
sudo sh /opt/lospor-hospital/current/scripts/acknowledge-secrets-escrow.sh.
Neither the acknowledgement nor Status ever holds a key: only one-way
fingerprints, so a later change of keys shows the escrow as out of date.
Create and inspect recovery points
Create an ordinary manual point:
sudo sh /opt/lospor-hospital/current/scripts/backup-now.sh
Protected kinds are reserved for the matching workflow:
sudo sh /opt/lospor-hospital/current/scripts/backup-now.sh --kind pre-update
sudo sh /opt/lospor-hospital/current/scripts/backup-now.sh --kind pre-restore
sudo sh /opt/lospor-hospital/current/scripts/backup-now.sh --kind immutable
Concurrent scheduled, manual, and pre-update backup requests share the backup
mutex. A caller may reuse the same newly verified object; contention returns
the stable BACKUP_BUSY process result without overwriting the active backup’s
Status signal. Backup also takes the same persistent OS lock as release
migrations and service replacement. If maintenance owns it, the backup defers
before reading PostgreSQL with exit 75 and BACKUP_MAINTENANCE_BUSY; an update
never overlaps a dump.
Safe restore
The default restores into an isolated temporary database while clinical services and the live database remain unchanged:
sudo sh /opt/lospor-hospital/current/scripts/restore-backup.sh --temporary \
backups/lospor-YYYYMMDDTHHMMSSZ-RANDOM.backup
A restore drill proves a backup restores without keeping the copy. It runs the
same checks, restore, migrations and validation as a temporary restore, then
removes the temporary database. Nobody types a confirmation, because nothing a
clinician uses changes. losporctl backup drill drills the newest backup, and
Maintenance in Status runs the same drill and keeps its results:
sudo sh /opt/lospor-hospital/current/scripts/restore-backup.sh --drill \
backups/lospor-YYYYMMDDTHHMMSSZ-RANDOM.backup
sudo losporctl backup drill
Before creating anything, restore authenticates the closed manifest schema,
parses the dump catalog, verifies capacity, and compares site, appliance,
release, exchange/data-dictionary, PostgreSQL, migrations, schema, and all key
fingerprints. The external-AI fingerprint is SHA-256 over the decoded 32-byte
seal key, not over its base64 text. A missing or different seal key returns
RESTORE_EXTERNAL_AI_SEAL_KEY_MISMATCH before any database mutation, because
restored provider credentials would otherwise be unreadable. The OMOP
pseudonym-salt fingerprint is SHA-256 over the exact canonical 64-character
lowercase hexadecimal value in .env, with no newline. A different escrowed
salt returns RESTORE_OMOP_PSEUDONYM_SALT_MISMATCH before any database
mutation, because the restored OMOP projection would otherwise derive different
numeric source identifiers. Wrong-site, wrong-key, corrupt, unsupported, and
too-new objects fail before outage. The temporary database is migrated and
validated; a failed attempt removes only that isolated database.
An in-place switch is an emergency operation:
sudo sh /opt/lospor-hospital/current/scripts/restore-backup.sh --in-place \
backups/lospor-YYYYMMDDTHHMMSSZ-RANDOM.backup
It additionally requires the exact displayed site/timestamp confirmation, a
new authenticated pre-restore safety snapshot, and a second destructive-
boundary acknowledgement. The live database is retained under a safe previous
name until the switch is proven. A fail-closed journal records each boundary.
Caddy may reopen only after doctor.sh --restore-preopen proves PostgreSQL,
migrations/schema, API, Web, PWA, Browser, Status, operator consistency, and
terminology readiness and emits RESTORE_PREOPEN_OK.
Status SQLite is operational state, not part of the clinical PostgreSQL
recovery object. Preserve its volume on the same appliance. If that volume is
lost but the restored clinical database still has an appliance operator, use
the supported ./scripts/appliance-operator.sh repair-status flow; lost Status
history cannot be reconstructed by copying an unrelated SQLite database.
The release quality gates exercise corrupt, wrong-site, wrong-key, incompatible, temporary, and emergency-boundary cases. That automated proof does not replace the hospital’s quarterly off-host restore drill.