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 | When | What it means |
|---|---|---|
| PASS | A 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. |
| WARN | Nothing found | No 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 -deleteBetter: 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_testDebugging
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
- PostgreSQL: Backup and Restore
- PostgreSQL: pg_dump
- PostgreSQL: pg_dumpall
- PostgreSQL: pg_basebackup
- MySQL 8.4: Backup and Recovery
- MariaDB KB: Backup and Restore Overview
- MongoDB: Back Up and Restore with MongoDB Tools
- Redis: Persistence (RDB/AOF)
- pgBackRest - https://pgbackrest.org/ ; Percona XtraBackup
- restic - https://restic.readthedocs.io/ ; BorgBackup
- crontab(5)
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