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 | When | What it means |
|---|---|---|
| SKIP | No config files found by the previous search | Nothing was checked, because the web-root scan returned no file to stat. This is an absence of input, not a clean result. |
| FAIL | Any file is world-readable | At least one file holding database credentials can be read by every local account on the host, and the message lists each offending path. |
| PASS | None are | Every 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 022creates new files644. 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
statthe file (no traverse permission on a parent directory),Mis empty and the file is treated as safe. Run with sudo for full coverage. - Some panels (cPanel with suPHP/CageFS) isolate users so
644is 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/nullSet 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.phpSet 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/nullStop 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 deniedDebugging
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
- chmod(1)
- umask(2) and umask(1p)
- WordPress: Changing File Permissions
- OWASP WSTG-CONF-09 Test File Permission
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