Skip to content

Backup and restore

The database and the key, together

Edit this page
On this page

Two things must survive together, and the second is the one people lose.

The database, which holds every host, service, finding and session.

LOOMSCOPE_KMS_KEY, which decrypts the credentials table. A database restored under a different key comes back looking completely healthy — hosts, users, networks, findings all present — and every stored SNMP community, SSH key and cloud secret silently fails to decrypt. You find out days later, when scans stop authenticating.

Taking a backup

bash
./scripts/backup.sh /var/backups/loomscope

It reads DATABASE_URL and LOOMSCOPE_KMS_KEY from the environment or from .env, and writes one timestamped .tar.gz containing:

FileWhat it is
database.sqlA plain-SQL pg_dump, restorable with psql alone
database.sql.sha256Checksum, verified before any restore
manifest.txtSchema level, row counts, and a fingerprint of the encryption key

The key itself is never in the archive. If it were, the archive alone would be enough to read every credential in the estate — and backups travel to places databases do not. Store the key in a secret manager, separately.

The fingerprint is a one-way digest. It cannot reproduce the key; it only lets a restore tell whether the key you hold is the key the credentials were encrypted with.

Restoring

bash
./scripts/restore.sh /var/backups/loomscope/loomscope-20260815T031500Z.tar.gz

It refuses, rather than proceeding, when:

  • the dump's checksum does not match the one recorded at backup time;
  • LOOMSCOPE_KMS_KEY is not the key the backup was taken with;
  • the target database already has tables, unless you pass --force.

After restoring it re-counts the rows and compares them to the manifest, so a partial restore fails loudly instead of looking finished.

If the credentials are genuinely expendable and you intend to re-enter every one by hand, --ignore-kms-mismatch proceeds anyway. There is no other way past that check, by design.

Verifying a backup without restoring it

bash
./scripts/restore.sh loomscope-20260815T031500Z.tar.gz --verify-only

Checks the archive and exits, writing nothing. It needs neither a database nor a Postgres client, so it runs where the backups are stored — including from cron, so a corrupt archive is noticed on the day it is written rather than on the day you need it.

Test the full restore too

An untested backup is a hypothesis.

bash
# Restore into a scratch database
DATABASE_URL=postgres://…/loomscope_restore_test ./scripts/restore.sh backup.tar.gz

Then open a stored credential in the UI. If it decrypts, the pair — dump and key — is good. Nothing else proves that; the row counts matching only proves the dump arrived.

A backup schedule that works

cron
# Nightly backup
15 3 * * *  /opt/loomscope/scripts/backup.sh /var/backups/loomscope

# Verify every archive the morning after it is written
30 6 * * *  /opt/loomscope/scripts/restore.sh --verify-only $(ls -t /var/backups/loomscope/*.tar.gz | head -1)

# Prune beyond 30 days
0  4 * * *  find /var/backups/loomscope -name '*.tar.gz' -mtime +30 -delete

And, separately from all of it: a copy of LOOMSCOPE_KMS_KEY in a secret manager, with at least one person other than you able to retrieve it.

What is not backed up

Not in the archiveWhyWhat to do
LOOMSCOPE_KMS_KEYIt would defeat encrypting the credentialsSecret manager, separately
BETTER_AUTH_SECRETSame reasoningSecret manager. Losing it invalidates sessions, nothing worse
Daemon API keysOnly hashes exist anywhereRe-enrol daemons after a restore, or keep the keys with your configuration
Container imagesThey are in a registryFor air-gap, save them alongside — see Air-gapped
The CVE mirrorRebuildableRe-copy it; nothing is lost

Disaster recovery, end to end

  1. Provision a host with Docker and restore your .env — including LOOMSCOPE_KMS_KEY from the secret manager.
  2. Start PostgreSQL only.
  3. ./scripts/restore.sh <archive>.
  4. Start the control plane, run migrations if the archive predates the image version.
  5. Start the worker.
  6. Re-enrol daemons if you did not keep their keys.
  7. Open a stored credential to confirm the key matched.
  8. Check Settings → Jobs for a recent run of everything.

Steps 7 and 8 are the ones people skip, and they are the two that tell you whether the restore actually worked.

Proving it, rather than assuming it

A backup nobody has restored is a hypothesis. The archive can be well-formed, its checksum can match, restore.sh can exit 0, and the result can still be missing a table nobody thought to dump.

bash
make backup-roundtrip

It backs up, verifies the archive without writing, restores into a scratch database, compares nine tables row for row, and checks that every stored credential still decrypts under the current key — the failure that otherwise surfaces days later as scans that no longer authenticate. The source database is never written to, and the script refuses outright if the scratch name resolves to it.

It runs on every change in CI. It needs psql, pg_dump and tar; where those are missing it says so rather than reporting an empty database.