Database audit fix

How to Secure Web Configuration File Permissions

Web config files such as .env and wp-config.php are created 644 by default, which lets every local account on the server read the database password inside them. Set them to 640, owned by the web server group.

Hiren KalariyaLast reviewed: Aug 30, 2026Check config-file-permissions
High
severity
No
needs root or sudo
3
results it can return
2 of 4
checks in this audit

Every threshold on this page is transcribed from the database-security-credential-storage audit script that CtrlOps runs, and a build check fails if the two ever disagree. See all 4 Credential Storage checks, or how the audit runs.

High severityNo sudo needed

What this check reads

The check re-runs the same scan as Config Files In Web Root: find /var/www /srv/www /usr/share/nginx/html -maxdepth 4 -type f matching .env, wp-config.php, config.php, settings.php, database.yml, capped at the first 20 hits. If that scan returns nothing there is no file to inspect and the check reports SKIP. Otherwise it takes each file's octal mode from stat -c %a and examines the last digit only, the "other" bits: a value of 4, 5, 6 or 7 means the file is world-readable and the check FAILs, while anything else passes, which is why group-readable 640 is accepted.

When it applies

Runs on every host (no engine or web server needed, no root required), but only when the previous check's find actually matched something - NC=0 means SKIP, because there is nothing to stat. It therefore inherits that scan's limits exactly: three fixed roots, depth 4, five filename patterns, first 20 matches only. Only the "other" digit of the mode is examined, so group-readable 640 passes by design. A file the audit user cannot stat yields an empty mode and is treated as safe, so run the audit with sudo for a complete result.

What each result means

Result thresholds this check applies
ResultWhenWhat it means
SKIPNo config files found by the previous searchNothing was checked, because the web-root scan returned no file to stat. This is an absence of input, not a clean result.
FAILAny file is world-readableAt least one file holding database credentials can be read by every local account on the host, and the message lists each offending path.
PASSNone areEvery config file the scan found has its world-read bit clear, although a group-readable 640 counts as a pass here.

Why it matters

A database password in a 644 file is readable by every local account, including other tenants on a shared host and any process that escapes a different service. Principle of least privilege on the filesystem; CIS Linux benchmarks "Ensure permissions on sensitive files are configured". The file must be readable by the web server worker (usually via group), not by the world.

Why it fails, and when it is wrong

  • Default umask 022 creates new files 644. Composer, wp-cli, Git checkouts and SFTP uploads all produce world-readable files.
  • Group-readable (640) is accepted. The script only looks at the "other" digit.
  • If the audit user cannot stat the file (no traverse permission on a parent directory), M is empty and the file is treated as safe. Run with sudo for full coverage.
  • Some panels (cPanel with suPHP/CageFS) isolate users so 644 is less dangerous, but the check cannot know that.

How to fix it

Read the current modes

Read the real modes rather than working from the finding's list. The last digit is the "other" bit, which is the only one this check tests:

find /var/www /srv/www /usr/share/nginx/html -maxdepth 4 -type f \
  \( -name '.env' -o -name 'wp-config.php' -o -name 'config.php' -o -name 'settings.php' -o -name 'database.yml' \) \
  -exec stat -c '%a %U:%G %n' {} + 2>/dev/null

Set an application config to 640

Grant access by group, not by world. The web server worker has to read the file, so 600 would break the application while 640 with the web group gives it access and denies everyone else:

chown root:www-data /var/www/app/.env          # or the app's deploy user : web group
chmod 640 /var/www/app/.env
# WordPress
chmod 640 /var/www/html/wp-config.php

Set client credential files to 600

An application config needs one group reader. A personal database client file has exactly one legitimate reader, so lock those to their owner, on every account that has them rather than just your own:

chmod 600 ~/.my.cnf ~/.pgpass ~/.mongoshrc.js ~/.dbshell ~/.rediscli_auth 2>/dev/null
sudo chmod 600 /root/.my.cnf /root/.pgpass 2>/dev/null
sudo chmod 600 /home/*/.my.cnf /home/*/.pgpass 2>/dev/null

Stop it recurring

Set umask 027 in deploy scripts so new files are not world-readable, or add an explicit chmod step to the deployment. The default umask 022 creates files at 644, and that applies to Composer, wp-cli, Git checkouts and SFTP uploads alike, so fixing the mode by hand only fixes today.

Verify the fix

# Mode of every config file the check would look at:
find /var/www /srv/www /usr/share/nginx/html -maxdepth 4 -type f \
  \( -name '.env' -o -name 'wp-config.php' -o -name 'config.php' -o -name 'settings.php' -o -name 'database.yml' \) \
  -exec stat -c '%a %U:%G %n' {} + 2>/dev/null
# expected: every mode ends in 0 (e.g. 640, 600) - the last digit is what the check tests

# Confirm an unprivileged account really cannot read it:
sudo -u nobody cat /var/www/app/.env
# expected: Permission denied

Debugging

Not a pass. The Config Files In Web Root scan found nothing, so this check had no input. See that check's debugging notes; if your web root is not one of the three scanned paths, neither check covers you.

You fixed a different copy, or a deploy re-created it. Deploys under umask 022 recreate files as 644; set umask 027 in the deploy script or add an explicit chmod to it, otherwise the finding returns after the next release.

The audit user could not stat it (no traverse permission on a parent directory), so the mode came back empty and the file was skipped. Re-run the audit as root or with sudo, and compare the find … -exec stat output above run as your user versus under sudo.

Grant access by group, not by world: chown root:www-data file && chmod 640 file. The check only tests the "other" digit, and more importantly 640 is the correct answer regardless of the check.

Sources

How the script reads this

Next

Re-run the Credential Storage audit after applying the fix and confirm this check moves to PASS. CtrlOps runs all 4 checks over your existing SSH connection and scores the result, so the change is visible without reading another config file.

All 4 Credential Storage 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