Database audit fix

How to Configure Database Transaction Log Placement

A transaction log that shares a partition with the data files collapses two failures into one: when the disk fills, writes and point-in-time recovery stop at the same moment. Put pg_wal and the MySQL binary log on their own device.

Hiren KalariyaLast reviewed: Sep 9, 2026Check transaction-log-placement
Low
severity
Yes
needs root or sudo
3
results it can return
4 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.

Low severityNeeds sudo

What this check reads

For MySQL the check first reads SELECT @@log_bin. If binary logging is on it also reads @@datadir and @@log_bin_basename, takes the dirname of the latter, resolves both paths to a mount point with df -P (field 6 of the second line), and records mysql:binlog-same-disk when the two mount points are identical or mysql:binlog-separate when they differ; with binary logging off it records mysql:binlog-off and moves on. For PostgreSQL it reads SHOW data_directory and applies a different test entirely, test -L "$PGDATA/pg_wal": a symlink counts as postgres:wal-separate and anything else, including a real mount point, as postgres:wal-same-disk. One same-disk verdict from either engine is the threshold that turns the whole check into a WARN.

When it applies

Runs only for engines it can query: the MySQL branch needs MYOK=1, the PostgreSQL branch needs PGOK=1; with neither, SKIP. Only MySQL/MariaDB and PostgreSQL have branches - MongoDB and Redis are not considered. The MySQL branch only evaluates placement when binary logging is on; with log_bin off it records binlog-off and contributes to a PASS. The two branches use different tests: MySQL compares df -P mount points (correct), PostgreSQL only asks whether $PGDATA/pg_wal is a symlink - so a pg_wal that is its own mount point is wrongly reported as sharing the disk. This is a known script issue, listed in the README.

What each result means

Result thresholds this check applies
ResultWhenWhat it means
SKIPNeither engine accessibleNothing was verified: neither the MySQL nor the PostgreSQL probe could reach a server, so no log placement was examined.
WARNAny engine's log shares the data diskAt least one engine writes its log to the same mount point as its data files. On PostgreSQL confirm with df first, because the check only tests for a symlink.
PASSLogs separate, or binlog off (noted for review)No engine reported a shared disk. A mysql:binlog-off entry inside this pass still deserves attention: it means MySQL has no point-in-time recovery.

Why it matters

OWASP Database Security Cheat Sheet lists "transaction logs on a separate disk" under hardening. Availability and recovery: if one partition fills, both writes and point-in-time recovery stop together. Separate devices also isolate I/O and let you keep WAL/binlogs on a device with different retention.

Why it fails, and when it is wrong

  • PostgreSQL pg_wal that is a mount point rather than a symlink is reported as same-disk: false WARN. The check should compare df like the MySQL branch; this is a known script issue.
  • Single-disk VPS plans make this unavoidable; treat as an accepted risk and rely on disk-space monitoring and offsite backups.
  • binlog-off is reported inside a PASS but deserves attention: without binary logs you cannot do point-in-time recovery for MySQL.
  • LVM/btrfs subvolumes on the same physical device count as separate mount points; the check measures partitions, not devices.

How to fix it

  • PostgreSQL: stop the server, move pg_wal to the new volume, ln -s /mnt/wal/pg_wal $PGDATA/pg_wal, start. (initdb --waldir for new clusters.)
  • MySQL: set log_bin=/mnt/binlog/mysql-bin and log_bin_index, move existing files, restart.
  • If only one disk exists, set binlog_expire_logs_seconds / wal_keep_size and alert on free space.

Verify the fix

# PostgreSQL - compare real mount points, which is what actually matters:
PGDATA=$(sudo -u postgres psql -X -tAc 'SHOW data_directory')
df -P "$PGDATA" "$PGDATA/pg_wal" | awk 'NR>1{print $6, $1}'
# expected: two different mount points

# MySQL - the same comparison the check makes:
sudo mysql -N -e 'SELECT @@log_bin, @@datadir, @@log_bin_basename'
df -P "$(sudo mysql -N -e 'SELECT @@datadir')" \
      "$(dirname "$(sudo mysql -N -e 'SELECT @@log_bin_basename')")" | awk 'NR>1{print $6}'
# expected: two different mount points

Debugging

MYOK and PGOK are both 0. Test with sudo mysql -N -e 'SELECT 1' and sudo -u postgres psql -X -tAc 'SELECT 1'.

The known false positive: the check tests test -L "$PGDATA/pg_wal" and a mount point is not a symlink. Confirm with the df -P command above; if the mount points differ, record it as a false positive. There is no way to make the check pass short of using a symlink instead of a mount.

Worth a second look. Binary logging being off is not a placement problem, but it means no point-in-time recovery for MySQL. Turn it on if your recovery objective needs it.

Accurate and usually unavoidable. Compensate with free-space alerting and offsite backups, and record it as accepted.

The check measures mount points, not physical devices, so two subvolumes on one disk look separate. Verify the underlying device with lsblk/findmnt if isolation is the goal.

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