Database audit fix

How to Configure & Schedule Database Backups on Linux

A scheduled dump is the only control that survives ransomware, a destructive migration or a dropped table. Put mysqldump, pg_dump or mongodump in cron or a systemd timer, add retention, and restore-test the output on a scratch instance.

Hiren KalariyaLast reviewed: Sep 9, 2026Check database-backups
Medium
severity
Yes
needs root or sudo
2
results it can return
5 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.

Medium severityNeeds sudo

What this check reads

The check greps the world-readable system cron locations - /etc/crontab, /etc/cron.d and /etc/cron.{hourly,daily,weekly,monthly} - case-insensitively for mysqldump|mariadb-dump|pg_dump|pg_basebackup|mongodump|xtrabackup|mariabackup|borg|restic, discarding commented lines. If that returns nothing and the audit has root, it repeats the same pattern recursively over /var/spool/cron to cover user crontabs. If both come back empty it falls back to systemctl list-timers --all, filtered for backup|dump in the unit name. The first match found by any of the three wins and is printed truncated to 80 characters: one match anywhere is the PASS, and no match at all is the WARN.

When it applies

Runs on any host. The system cron locations are world-readable so most of the check works unprivileged; only /var/spool/cron (user crontabs) needs root, and it is consulted only if nothing was found in /etc/cron* first. The systemd-timer fallback runs last and only if both greps came up empty. Detection is by keyword match on the cron line or timer name, so it proves a job is scheduled - never that it runs, succeeds, or produces a restorable backup. A wrapper script such as /usr/local/bin/backup.sh is matched only if its name contains one of the keywords; the script body is never read. Backups driven by an external agent (a hypervisor snapshot, a managed service, a pull-based tool on another host) are invisible.

What each result means

Result thresholds this check applies
ResultWhenWhat it means
PASSA matching cron line or timer exists (first match shown)Something matching a backup command is scheduled on this host. It proves a job exists, not that it runs, succeeds, or produces a restorable dump.
WARNNothing foundNo cron line or timer on this host names a backup command. Backups driven from outside the host are invisible here, so confirm before treating it as a real gap.

Why it matters

Backups are the control against ransomware, destructive migrations and operator error. OWASP: configure regular backups and test restores. NIST SP 800-34 contingency planning; CIS Controls v8 Control 11 (Data Recovery). The check only proves that something is scheduled; it cannot prove success or restorability.

Why it fails, and when it is wrong

  • Backups done by an agent or external scheduler (cloud snapshots, managed DB services, Velero, Kubernetes CronJobs, Jenkins, a backup server that pulls over SSH, WAL archiving via archive_command, pgBackRest, Barman, Percona operator) are not in cron on this host: false WARN.
  • A cron line that calls a wrapper script (/usr/local/bin/backup.sh) is matched only if the script name contains one of the keywords; the script body is not read.
  • /var/spool/cron/crontabs (Debian) is covered by the recursive grep of /var/spool/cron.
  • The timer fallback matches the unit name only.

How to fix it

Example daily dump with retention and a restore test reminder:

# /etc/cron.d/db-backup
15 2 * * * root /usr/bin/pg_dumpall -U postgres | gzip > /var/backups/pg/$(date +\%F).sql.gz && find /var/backups/pg -mtime +14 -delete

Better: pgBackRest/Barman (PostgreSQL), xtrabackup/mariabackup (MySQL), mongodump with --oplog, plus restic/borg to offsite storage. Schedule a periodic restore to a scratch instance.

Verify the fix

# Reproduce the search:
grep -rhiE 'mysqldump|mariadb-dump|pg_dump|pg_basebackup|mongodump|xtrabackup|mariabackup|borg|restic' \
  /etc/crontab /etc/cron.d /etc/cron.{hourly,daily,weekly,monthly} 2>/dev/null | grep -v '^[[:space:]]*#'
sudo grep -rhiE 'mysqldump|pg_dump|mongodump|borg|restic' /var/spool/cron 2>/dev/null
systemctl list-timers --all | grep -iE 'backup|dump'

# What actually matters - a recent, non-empty artefact and a real restore:
ls -lh /var/backups/pg/ | tail -5
gunzip -t /var/backups/pg/$(date +%F).sql.gz && echo 'archive intact'
# then restore into a scratch database and count rows:
createdb restore_test && gunzip -c /var/backups/pg/$(date +%F).sql.gz | psql -d restore_test

Debugging

The most common causes, in order: the job calls a wrapper script whose name lacks a keyword (rename it to pg_dump-nightly.sh, or invoke the dump binary directly in the cron line); the job lives in a user crontab and the audit had no root (re-run with sudo); the job is a systemd timer whose unit name contains neither backup nor dump; or backups are taken from outside the host entirely. Any of these is a false WARN - verify with the commands above and record it.

This is the check's central limitation: it only proves something is scheduled. Confirm the artefacts are recent, non-empty and restorable using the second block above. An untested backup is not a backup.

The message prints one line, truncated to 80 characters. Use the grep -r commands to see every scheduled job.

Cron mails output to the crontab owner, which nobody reads on most servers. Check journalctl -u cron -u crond --since '-7 days', and make the job write a status file or ping a monitoring endpoint on success.

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