What this check reads
The check runs one find across three fixed web roots and counts what comes back:
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' \) | head -20Nothing is read out of the files themselves: carrying one of those five names, inside one of those three trees, at a depth of 4 or less, is the entire test. A count of zero is the only PASS, and one match or more is a WARN, in which case the message quotes the first two paths (head -2) along with the total.
When it applies
Always runs - it is a filesystem scan, not gated on any database or web server being detected, and needs no root. Scope is fixed and narrow: exactly three roots (/var/www, /srv/www, /usr/share/nginx/html), a maximum depth of 4, five filename patterns, and only the first 20 matches (head -20). Sites served from /home/*/public_html, /opt or any other root are never scanned, so PASS means "nothing found in those three trees", not "no exposed config anywhere". Files the audit user cannot traverse to are silently skipped.
What each result means
| Result | When | What it means |
|---|---|---|
| PASS | No matching files | Nothing matched in the three scanned roots, so a site served from a path outside them was never looked at. |
| WARN | One or more found | At least one credential-bearing config file sits inside a served tree, and the message names the first two plus the total count. |
Why it matters
Credential-bearing files inside a document root are one misconfiguration away from being served as plain text: a PHP handler outage serves wp-config.php raw; a missing dotfile deny rule serves .env. OWASP Top 10 A05:2021 Security Misconfiguration; OWASP Secrets Management Cheat Sheet; WSTG-CONF-04 "Review Old Backup and Unreferenced Files". Best practice is to keep config outside the web root and load it by path, or use environment variables injected by the service manager.
Why it fails, and when it is wrong
- WordPress requires
wp-config.phpnext to or one level abovewp-content. Moving it one directory above the document root is supported by WordPress and resolves the finding. - Laravel, Symfony and Node projects often have
.envat the project root while the real document root ispublic/. The file is then not served, but it is still within/var/www/<app>and will be reported. This is a WARN, not FAIL, for that reason. Verify withcurl -I https://host/.env(expect 403/404) and the Hidden File Exposure web check. - Only three common roots are searched, to a depth of 4. Sites under
/home/*/public_htmlor/optare not scanned. - Files are only readable by the audit user if permissions allow; unreadable files are silently skipped (
2>/dev/null).
How to fix it
Find the files
Start from the full list rather than the two paths (head -2) the finding quotes, by re-running the scan the check performs:
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' \) 2>/dev/nullMove what can move
Move the file above the document root and point the app at it. WordPress looks for wp-config.php in the directory above the web root with no configuration change at all, so placing it in the parent directory resolves the finding on its own. For Laravel, Symfony and Node projects the web root should be public/, which leaves the project-root .env outside the served tree.
Block what cannot move
For anything that has to stay where it is, add an explicit deny so that even a misconfigured handler cannot serve it:
location ~ /\.(?!well-known) { deny all; }
location ~* (wp-config\.php|config\.php|database\.yml)$ { deny all; }<FilesMatch "^(\.env|wp-config\.php|database\.yml)$">
Require all denied
</FilesMatch>A deny rule depends on the config staying correct through every future change, so treat it as the second layer. Better still, prefer environment variables or a secrets manager for database credentials, which leaves no file on disk to serve.
Verify from outside
Request the file over HTTP from another machine and confirm the server hands back a refusal rather than the contents:
curl -sS -o /dev/null -w '%{http_code}\n' https://your-host/.env
# expected: 403 or 404Verify the fix
# Reproduce the exact scan the check performs:
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' \) 2>/dev/null
# expected after the fix: no output
# And confirm the file is not actually served (the finding that really matters):
curl -sS -o /dev/null -w '%{http_code}\n' https://your-host/.env
# expected: 403 or 404Debugging
Most often a framework .env at the project root while the document root is public/. The file is inside /var/www/<app> so it is reported, even though it is not served. Confirm with the curl above; if it returns 403/404 this is a defence-in-depth WARN, not an exposure. That is exactly why the check is WARN and not FAIL.
Your web root is outside the three scanned paths, deeper than 4 levels, or you have more than 20 matches and the extras were truncated by head -20. Re-run the find above against your real root: find /path/to/root -maxdepth 6 -type f -name '.env' -o -name 'wp-config.php'.
The message lists only the first two (head -2) plus a total count. Run the find command to see the full list.
The scan runs as the audit user with 2>/dev/null, so permission-denied directories vanish silently. Re-run the find under sudo to compare counts.
Sources
- OWASP Secrets Management Cheat Sheet
- OWASP Top 10 A05:2021 Security Misconfiguration
- OWASP WSTG-CONF-04 Review Old Backup and Unreferenced Files
- WordPress: Moving wp-config.php (Hardening WordPress)
- Laravel: Deployment, web server configuration (public/ as root)
- nginx: location directive
- Apache: FilesMatch
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