Backup and restore
The database and the key, together
Edit this pageOn 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
./scripts/backup.sh /var/backups/loomscopeIt reads DATABASE_URL and LOOMSCOPE_KMS_KEY from the environment or from
.env, and writes one timestamped .tar.gz containing:
| File | What it is |
|---|---|
database.sql | A plain-SQL pg_dump, restorable with psql alone |
database.sql.sha256 | Checksum, verified before any restore |
manifest.txt | Schema 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
./scripts/restore.sh /var/backups/loomscope/loomscope-20260815T031500Z.tar.gzIt refuses, rather than proceeding, when:
- the dump's checksum does not match the one recorded at backup time;
LOOMSCOPE_KMS_KEYis 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
./scripts/restore.sh loomscope-20260815T031500Z.tar.gz --verify-onlyChecks 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.
# Restore into a scratch database
DATABASE_URL=postgres://…/loomscope_restore_test ./scripts/restore.sh backup.tar.gzThen 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
# 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 -deleteAnd, 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 archive | Why | What to do |
|---|---|---|
LOOMSCOPE_KMS_KEY | It would defeat encrypting the credentials | Secret manager, separately |
BETTER_AUTH_SECRET | Same reasoning | Secret manager. Losing it invalidates sessions, nothing worse |
| Daemon API keys | Only hashes exist anywhere | Re-enrol daemons after a restore, or keep the keys with your configuration |
| Container images | They are in a registry | For air-gap, save them alongside — see Air-gapped |
| The CVE mirror | Rebuildable | Re-copy it; nothing is lost |
Disaster recovery, end to end
- Provision a host with Docker and restore your
.env— includingLOOMSCOPE_KMS_KEYfrom the secret manager. - Start PostgreSQL only.
./scripts/restore.sh <archive>.- Start the control plane, run migrations if the archive predates the image version.
- Start the worker.
- Re-enrol daemons if you did not keep their keys.
- Open a stored credential to confirm the key matched.
- 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.
make backup-roundtripIt 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.