2026年11月12日

2026年11月12日

Block Access to wp-config.php: Apache 2.4, Nginx and Log Monitoring

This article is a localized edition of the Japanese original. Read the Japanese original

wp-config.php holds your database credentials and authentication keys. Because it is a PHP file, opening it in a browser normally runs it and returns an empty page instead of showing its contents. You still want to deny access to it, to keep the contents from leaking in cases like these:

  • A server configuration change or failure stops PHP from running, and .php files are served as plain text
  • Copies created while editing, such as wp-config.php.bak, wp-config.php~ or .wp-config.php.swp, are left behind and served as-is because they are not run as PHP

This article walks through checking the current state, denying access in Apache 2.4 and Nginx, moving the file out of the document root, checking access logs and blocking probes with fail2ban. The examples were tested with Apache 2.4.67, Nginx (stable) and fail2ban 1.1.0 (as of October 2026).

Check the current state

First, check that no copies of wp-config are left on the server. Run this in the WordPress directory (for example /var/www/html).

# Find files starting with wp-config and editor temp files starting with a dot
ls -la /var/www/html | grep -i 'wp-config'

If you find files other than wp-config.php and wp-config-sample.php (ending in .bak, .old or ~, or .swp files), check their contents, then move them out of the document root or delete them.

Next, check how the files look from outside. Replace example.com with your own domain.

for f in wp-config.php wp-config.php.bak wp-config.bak wp-config-sample.php readme.html license.txt .git/config .env; do
  printf '%-24s %s\n' "$f" "$(curl -s -o /dev/null -w '%{http_code}' "https://example.com/$f")"
done

Before you add the rules, wp-config.php usually returns 200 (with an empty body) because PHP runs it. After you add them, it returns 403. Files that do not exist return the WordPress 404 page, so 404 or 403 is fine. Check the contents of any file that returns 200.

Deny access in Apache 2.4 (.htaccess)

Add the rules to the .htaccess file in the WordPress root, before the # BEGIN WordPress line. Anything inside the WordPress block is rewritten when, for example, you save the permalink settings.

# Deny wp-config.php and its backups/copies (wp-config.php.bak and so on)
<FilesMatch "^(wp-config(-sample)?\.php.*|wp-config\.(bak|old|orig|save|txt))$">
    Require all denied
</FilesMatch>

# Deny bundled files that reveal the version (optional)
<FilesMatch "^(readme\.html|license\.txt)$">
    Require all denied
</FilesMatch>

# Deny dot files such as .git and .env (except .well-known, used for certificate issuance)
<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteRule "(^|/)\.(?!well-known/)" - [F]
</IfModule>

With these rules, wp-config.php, wp-config.php.bak, wp-config.php~, wp-config.bak, wp-config-sample.php, .wp-config.php.swp, .git/config and .env return 403, while everything under .well-known/acme-challenge/ (used by Let's Encrypt HTTP validation) and post permalinks that start with wp-config, such as wp-config-guide, still load normally. We confirmed this behavior in testing.

Points to watch:

  • Order Deny,Allow and Deny from all are Apache 2.2 syntax. In Apache 2.4 they work only when the compatibility module (mod_access_compat) is loaded, and mixing them with the newer Require can give unexpected results. In 2.4, use Require all denied.
  • To use Require in .htaccess, AllowOverride must allow AuthConfig (or All) for that directory, and RewriteRule needs FileInfo (or All). With AllowOverride None, .htaccess is not read at all, so put the rules in the virtual host configuration as shown below.
  • .htaccess and .htpasswd are already denied by the default configuration of Apache on Debian and Ubuntu (<FilesMatch "^\.ht">). The dot-file rule in the example above denies them too.

Putting the rules in the virtual host configuration

If you cannot or prefer not to use .htaccess, add the rules to the site's configuration file (under /etc/apache2/sites-available/ on Debian and Ubuntu).

<Directory /var/www/html>
    <FilesMatch "^(wp-config(-sample)?\.php.*|wp-config\.(bak|old|orig|save|txt))$">
        Require all denied
    </FilesMatch>
</Directory>

After changing the configuration, check the syntax and then reload.

sudo apache2ctl configtest && sudo systemctl reload apache2

Deny access in Nginx

Nginx does not read .htaccess, so add the rules to the server block. Regular-expression location blocks are evaluated in the order they are written, and the first match wins. Put them before the location ~ \.php$ block that handles PHP.

server {
    # ... existing settings ...

    # Deny wp-config.php and its backups/copies
    location ~* ^/(wp-config(-sample)?\.php.*|wp-config\.(bak|old|orig|save|txt))$ {
        deny all;
    }

    # Deny bundled files that reveal the version (optional)
    location ~* ^/(readme\.html|license\.txt)$ {
        deny all;
    }

    # Deny dot files (except .well-known)
    location ~ /\.(?!well-known/) {
        deny all;
    }

    # Put location / and location ~ \.php$ after this
}

If you write the dot-file rule as location ~ /\., .well-known is also denied, and issuing or renewing certificates with Let's Encrypt HTTP validation (HTTP-01) fails. (?!well-known/) excludes it.

If WordPress lives in a subdirectory (for example /blog/), change the first and second ^/ to / so they match at any level.

sudo nginx -t && sudo systemctl reload nginx

Move wp-config.php out of the document root

If wp-config.php is not in the WordPress directory, WordPress looks for it one directory up (but it does not use it if that directory also contains wp-settings.php, meaning another WordPress install lives there). If your document root is /var/www/html and WordPress is installed in it, do the following.

# Move it (do not leave copies or backups inside the document root)
sudo mv /var/www/html/wp-config.php /var/www/wp-config.php

# Check that the site still loads
curl -s -o /dev/null -w '%{http_code}\n' https://example.com/

Points to watch:

  • When you make a backup, do not create it inside the document root, such as /var/www/html/wp-config.php.bak. It is not run as PHP, so its contents can be read directly. Save it outside the document root or on your local PC.
  • On some shared hosting, the directory one level up is the document root of another site. In that case moving the file still leaves it in a publicly readable place. Make sure the directory above is not served to the web before you move it.
  • If the site stops loading after the move, put the file back where it was.

Permissions

wp-config.php only needs to be readable by the user that runs PHP. The official WordPress documentation (Hardening WordPress) gives 440 or 400 as examples so other users cannot read it. If the PHP user (for example www-data) differs from the file owner, set the group to the PHP user's group and use 440 or 640.

ls -l /var/www/wp-config.php
sudo chown deploy:www-data /var/www/wp-config.php   # replace deploy with the owner in your environment
sudo chmod 440 /var/www/wp-config.php

After changing permissions, check that the site and the admin still load. If PHP cannot read the file, you get a database connection error.

Check the access logs

Watch in real time

sudo tail -f /var/log/apache2/access.log | grep --line-buffered 'wp-config'

For Nginx, use /var/log/nginx/access.log instead (the path varies by distribution and configuration).

Count probes in the last 24 hours by source IP

Counting with just grep and awk '{print $1}' counts every period still kept in the file. To limit it to the last 24 hours, convert the timestamp of each line (in the form [30/Oct/2026:10:15:00 +0900]) to UNIX time and compare. The following example uses mktime() from GNU awk (gawk). If gawk is missing, install it with sudo apt install gawk.

sudo gawk -v since="$(date -d '24 hours ago' +%s)" '
BEGIN { split("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec", m, " "); for (i = 1; i <= 12; i++) mon[m[i]] = i }
$7 ~ /^\/wp-config/ {
  split(substr($4, 2), t, /[\/:]/)
  if (mktime(t[3] " " mon[t[2]] " " t[1] " " t[4] " " t[5] " " t[6]) >= since) print $1
}' /var/log/apache2/access.log | sort | uniq -c | sort -rn | head -20

Assuming the standard Apache and Nginx log format (combined), it counts only lines whose seventh field (the request path) starts with /wp-config. Timestamps are interpreted in the server's time zone, so the log must be written in the same time zone as the server. If logs rotate daily, include the previous day's file (access.log.1) when the 24 hours span midnight.

Ban probing IPs with fail2ban

You can use fail2ban to block IP addresses that probe for wp-config for a while. See the related article for installing fail2ban and the basics.

Create the filter in /etc/fail2ban/filter.d/wp-config-probe.conf.

[Definition]
failregex = ^<HOST> [^"]*"(?:GET|POST|HEAD) /wp-config[^ "]* HTTP/
ignoreregex =

fail2ban reads the timestamp part of the log line itself before applying the regular expression, so the pattern deliberately leaves the timestamp out. You can check whether it matches your real log with fail2ban-regex.

sudo fail2ban-regex /var/log/apache2/access.log /etc/fail2ban/filter.d/wp-config-probe.conf

Next, create /etc/fail2ban/jail.d/wp-config-probe.local.

[wp-config-probe]
enabled  = true
port     = http,https
filter   = wp-config-probe
logpath  = /var/log/apache2/access.log
maxretry = 1
findtime = 1h
bantime  = 1d
ignoreip = 127.0.0.1/8 ::1

Because of maxretry = 1, any IP that requests wp-config even once is banned for a day. If you test with curl from your own IP, you will ban yourself, so add the IP you test from to ignoreip. Behind a proxy such as Cloudflare, the source address in the log is the proxy's IP, so this configuration cannot be used as-is.

sudo fail2ban-client -t && sudo systemctl reload fail2ban
sudo fail2ban-client status wp-config-probe

Summary

  • wp-config.php is normally not displayed, but it can leak when PHP stops or when copies such as .bak are left behind. Start by checking for leftover copies
  • In Apache 2.4, write Require all denied in .htaccess (before # BEGIN WordPress) or in the virtual host configuration. Do not use the 2.2 Order and Deny directives
  • In Nginx, put the deny all location blocks before the PHP location, and exclude .well-known from the dot-file rule
  • wp-config.php can be moved one directory up. Do not create backups inside the document root
  • To limit logs to the last 24 hours, compare timestamps. fail2ban can block IPs that probe for the file

Related articles

Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)

お気軽にご相談ください

お見積りへ お問い合わせへ
▲