Database audit fix

How to Encrypt Database Backup Files Before Uploading Them

Backups leave the host, so their real protection is the encryption they carry with them, not the file modes they had on disk. Wrap dumps with gpg or age, or use a client-side-encrypting tool such as borg or restic, and keep the key elsewhere.

Hiren KalariyaLast reviewed: Sep 9, 2026Check backup-encryption
High
severity
Yes
needs root or sudo
4
results it can return
7 of 7
checks in this audit

Every threshold on this page is transcribed from the database-security-hardening audit script that CtrlOps runs, and a build check fails if the two ever disagree. See all 7 Configuration & Hardening checks, or how the audit runs.

High severityNeeds sudo

What this check reads

The check counts two things across the same five paths (find /var/backups /backup /opt/backup /srv/backup /root/backups -maxdepth 3 -type f): plaintext dumps matching *.sql, *.sql.gz, *.dump or *.bak, capped at 20 and held in ND, and encrypted archives matching *.gpg, *.enc, *.age or *.aes, capped at 5 and held in ENC. It then sets TOOL if any of borg, restic or duplicity is on PATH. Detection is by filename extension alone, which makes the threshold blunt: any ENC above zero is the PASS, ND above zero with ENC at zero is a FAIL without TOOL and a WARN with it, and both counts at zero is the SKIP.

When it applies

Needs root or sudo - without it the check SKIPs immediately, before looking at anything. It scans the same five backup paths at depth 3 (/var/backups, /backup, /opt/backup, /srv/backup, /root/backups), counting up to 20 plaintext dumps and up to 5 encrypted archives, and separately checks whether borg, restic or duplicity is installed. Detection is purely by file extension (*.gpg, *.enc, *.age, *.aes): a backup encrypted at the destination (an SSE-KMS bucket, an encrypted filesystem, a repository written by a tool that keeps its own format) counts as plaintext here. With no dumps and no encrypted archives it SKIPs, which is not a pass.

What each result means

Result thresholds this check applies
ResultWhenWhat it means
SKIPNo root, or no dumps and no encrypted archivesNothing was verified: the check either lacked the root it needs to read the backup paths, or found neither dumps nor encrypted archives to judge.
FAILPlaintext dumps, no encrypted archives, no encrypting toolDumps are sitting on disk in the clear and nothing on this host is set up to encrypt them, so any copy that leaves the server leaves readable.
WARNPlaintext dumps, no encrypted archives, but an encrypting tool exists (staging files?)Plaintext dumps are present, but borg, restic or duplicity is installed, so the check assumes they are staged for a tool that encrypts them. Confirm that assumption or treat this as a FAIL.
PASSEncrypted archives presentAt least one file with an encrypted extension was found in those paths. The check never opened it and never looked at where the key is kept.

Why it matters

Backups leave the host (offsite, object storage, laptops). Encryption at rest protects them wherever they end up. OWASP Cryptographic Storage Cheat Sheet; NIST SP 800-111 (storage encryption); CIS Controls v8 11.3 "Protect recovery data". Key handling matters: a key stored next to the backup is decoration.

Why it fails, and when it is wrong

  • Heuristic by file extension. A dump piped into openssl enc with a .sql.gz.enc name is detected; one uploaded straight to an encrypted bucket (SSE-KMS) is not, giving a false FAIL/WARN.
  • restic/borg always encrypt client-side; the WARN branch assumes plaintext dumps are staging files for them.
  • age and gpg are not checked as tools, only as output extensions.

How to fix it

# gpg (public key, private key kept off-host)
pg_dumpall | gzip | gpg --encrypt --recipient backup@example.com > /var/backups/pg/$(date +%F).sql.gz.gpg
# age
mysqldump --all-databases | age -r age1... > /var/backups/all.sql.age
# or use restic/borg with a repository key stored in a password manager

Test decryption and restore on a different machine.

Verify the fix

# What the check counts:
sudo find /var/backups /backup /opt/backup /srv/backup /root/backups -maxdepth 3 -type f \
  \( -name '*.sql' -o -name '*.sql.gz' -o -name '*.dump' -o -name '*.bak' \) 2>/dev/null | wc -l
sudo find /var/backups /backup /opt/backup /srv/backup /root/backups -maxdepth 3 -type f \
  \( -name '*.gpg' -o -name '*.enc' -o -name '*.age' -o -name '*.aes' \) 2>/dev/null | wc -l

# Prove an archive is really encrypted, not just named that way:
file /var/backups/pg/latest.sql.gz.gpg      # expected: GPG/PGP encrypted data
head -c 16 /var/backups/pg/latest.sql.gz.gpg | xxd   # expected: not readable SQL

# Prove you can decrypt AND restore, on a different machine:
gpg --decrypt latest.sql.gz.gpg | gunzip | head -20
restic -r <repo> check && restic -r <repo> snapshots

Debugging

The check does nothing at all without privilege. Re-run as root or with sudo.

Nothing matched in the five scanned paths. Confirm where your backups actually live before treating this as clean; see the Backup File Permissions debugging notes.

The likely false positive. The check reads extensions only, so encryption performed by the destination (SSE-KMS, an encrypted volume) or by a tool that writes its own repository format is not visible. Verify with the file command above and record it as accepted. If you want the check to pass, name the artefacts with one of the four extensions.

This fires when plaintext dumps exist and borg/restic/duplicity is installed; the check is assuming those dumps are staging files that get encrypted before leaving the host. Confirm that is true - check the backup script and restic snapshots - otherwise treat it as a FAIL.

The check cannot see key handling at all. A passphrase in the backup script, or a private key in /root/.gnupg on the machine being backed up, means an attacker with root gets both halves. Keep the private key or repository password somewhere the host cannot reach, and test decryption elsewhere.

Sources

How the script reads this

Next

Re-run the Configuration & Hardening audit after applying the fix and confirm this check moves to PASS. CtrlOps runs all 7 checks over your existing SSH connection and scores the result, so the change is visible without reading another config file.

All 7 Configuration & Hardening fixes
Audit your fleet

Find every one of these on every server, in one click

CtrlOps runs this audit over your existing SSH connection - no agents, no scripts to manage. $7/user/month after a 1 month free trial - no credit card required.

✓ Start instantly·✓ No credit card·✓ No sneaky autorenewals