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 | When | What it means |
|---|---|---|
| SKIP | No root, or no dumps and no encrypted archives | Nothing was verified: the check either lacked the root it needs to read the backup paths, or found neither dumps nor encrypted archives to judge. |
| FAIL | Plaintext dumps, no encrypted archives, no encrypting tool | Dumps 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. |
| WARN | Plaintext 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. |
| PASS | Encrypted archives present | At 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 encwith a.sql.gz.encname is detected; one uploaded straight to an encrypted bucket (SSE-KMS) is not, giving a false FAIL/WARN. restic/borgalways encrypt client-side; the WARN branch assumes plaintext dumps are staging files for them.ageandgpgare 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 managerTest 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> snapshotsDebugging
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
- OWASP Cryptographic Storage Cheat Sheet
- NIST SP 800-111 Guide to Storage Encryption Technologies for End User Devices
- GnuPG manual
- age encryption tool
- restic: Encryption design
- BorgBackup: Security and encryption
- MySQL Enterprise Backup encryption (commercial)
- pgBackRest: Encryption
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