Database audit fix

How to Secure Database Backup File Permissions

A database dump is the whole database in one file, so a world-readable one hands every row to any local account and bypasses every grant you set. Keep dumps at 600, their directory at 700, and set umask 077 in the backup script.

Hiren KalariyaLast reviewed: Sep 9, 2026Check backup-file-permissions
High
severity
Yes
needs root or sudo
3
results it can return
6 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 first inventories dump files with 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' \), keeping the first 50 matches as the count NB. It then re-runs the identical search with -perm -004 added, which isolates the files carrying the world-read bit. An empty inventory is the SKIP; a non-empty inventory with no -perm -004 match is the PASS; and a single match is enough for the FAIL, with the first three paths printed in the message. Unprivileged runs drop /root/backups from both searches and say so in the message.

When it applies

Runs on any host, but its scope depends on privilege: with root it scans /var/backups, /backup, /opt/backup, /srv/backup and /root/backups; without root it drops /root/backups and says so in the message. Depth is capped at 3 and the inventory at the first 50 files; only four extensions count (*.sql, *.sql.gz, *.dump, *.bak). Backups anywhere else - /home/*/backups, /mnt/backup, an object store - are never seen, so SKIP means "nothing found in those five paths", not "no exposed dumps". The test is -perm -004, i.e. the world-read bit on the file; a restrictive parent directory mitigates in practice but does not change the result.

What each result means

Result thresholds this check applies
ResultWhenWhat it means
SKIPNo dump files in those pathsNothing was verified: no dump matched in the scanned paths, which is not the same as having no exposed dumps elsewhere on the host.
FAILAny dump is world-readableAt least one dump can be read by every local account on the server. The message lists the first three matches only; there may be more.
PASSDumps exist, none world-readableEvery dump found in those paths has the world-read bit clear. Nothing was checked about the directory above them, the file owner, or any offsite copy.

Why it matters

A dump is the entire database in one file. World-readable dumps bypass every database-level control. OWASP: protect backups, ideally encrypted. PCI DSS 3.x requires protecting stored cardholder data wherever it sits, including backups.

Why it fails, and when it is wrong

  • mysqldump > file inherits umask 022 and creates 644 files. pg_dump -Fc the same.
  • Dumps stored elsewhere (/home/user/backups, /mnt/backup, S3) are not scanned.
  • .bak also matches unrelated files (editor backups, config backups). Those are still worth protecting.
  • Directory permissions (700 on the parent) mitigate but the check looks at file modes only.

How to fix it

Find the dumps and their modes

Inventory what is actually on disk before you change anything, so you know how many files the fix has to cover:

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' \) \
  -exec stat -c '%a %U:%G %n' {} + 2>/dev/null

Remove world read

A dump is the entire database in a single file, so the owner is the only account that should be able to read it. Take the world-read bit off every file the inventory found, and close the directory above them as well:

sudo chmod 600 /var/backups/*.sql.gz
sudo chmod 700 /var/backups/db

Set umask in the backup script

A one-off chmod only fixes the files that already exist. Add umask 077 at the top of the script that creates the dumps, so future files are created at 600 without a separate chmod step:

# in the backup script:
umask 077

Encrypt during creation, not after

Permissions only stop other accounts on this host. Pipe the dump straight through gpg, age or openssl enc so a plaintext copy never touches disk, and store the key somewhere the server itself cannot read. See the Backup Encryption check for the encryption side of this.

Verify the fix

# Reproduce the scan, showing modes:
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' \) \
  -exec stat -c '%a %U:%G %n' {} + 2>/dev/null
# expected: every mode is 600 (or 640 with a trusted group) - none ending in 4,5,6,7

# The check's exact world-readable test must return nothing:
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' \) -perm -004 2>/dev/null

# Confirm from an unprivileged account:
sudo -u nobody cat /var/backups/db/latest.sql.gz    # expected: Permission denied

Debugging

Not a pass. Either there are no backups on this host, or they live outside the five scanned paths or deeper than three levels. Find them with sudo find / -xdev -name '*.sql.gz' -o -name '*.dump' 2>/dev/null | head.

The audit ran unprivileged. Re-run as root or with sudo before treating the result as clean.

The backup job recreates files under umask 022. Add umask 077 at the top of the backup script (or UMask=0077 in the systemd unit); a one-off chmod only fixes the files that already exist.

The FAIL message shows head -3. Use the -perm -004 command above for the complete list.

Editor and config backups match the pattern too. They are still worth protecting, but if the finding is noise, confirm what was matched with the first command.

Only from local accounts. Anyone who reaches the storage location or an offsite copy still gets the whole database; see the Backup Encryption check.

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