Hardening wp
Security case study: Hardening WordPress wp-config.php.
What You Will Learn
- Why
wp-config.phpis the most sensitive WordPress file. - 12 Linux and server-level hardening methods, each with command, expected output, and purpose.
- How to combine **permissions, ownership, ACLs, symlinks, and encryption for layered security.
- How to block HTTP access, prevent tampering, and monitor suspicious activity.
- Best practices for **secure deployment, backups, and monitoring in WordPress VPS hosting.
Prerequisites
- **Access Level:
sudoor root. - **Software: Apache/Nginx/OLS,
acl,auditd(optional). - **Knowledge: File permissions, ownership, WordPress structure.
5W + 1H Framework
| Question | Answer |
|---|---|
| What | 12 practical methods to harden wp-config.php. |
| Why | Protects database credentials, salts, and security definitions from theft or modification. |
| When | Immediately after setup, before production, and after migrations. |
| Where | Default: /var/www/html/wp-config.php (or relocated). |
| Who | Sysadmins, DevOps, WordPress VPS owners. |
| How | By combining Linux file controls, web server rules, ACLs, immutability, and monitoring. |
Hardening Methods (with Expected Output & Purpose)
Restrict File Permissions
chmod 600 /var/www/html/wp-config.php
ls -l /var/www/html/wp-config.php
Expected Output:
-rw------- 1 www-data www-data 4201 Sep 24 wp-config.php
**Purpose: Only the owner can read/write. Others have no access. Prevents other system users from reading DB credentials.
Use Root Ownership with Group Read
chown root:www-data /var/www/html/wp-config.php
chmod 640 /var/www/html/wp-config.php
ls -l /var/www/html/wp-config.php
Expected Output:
-rw-r----- 1 root www-data 4201 Sep 24 wp-config.php
**Purpose: Root owns the file, so PHP/webserver (www-data) can read but cannot modify. Adds protection against web-based exploits.
Move Outside Web Root
mv /var/www/html/wp-config.php /var/www/wp-config.php
ls -l /var/www/wp-config.php
Expected Output:
-rw------- 1 root www-data 4201 Sep 24 wp-config.php
**Purpose: File sits outside public directory, preventing direct HTTP access. WordPress auto-detects it above root.
Deny via Web Server Config
-
Apache:
<files wp-config.php>deny from all</files> -
Nginx:
location ~* wp-config.php { deny all; -
OLS:
<location wp-config.php> deny all </location>
Verification:
curl -I http://localhost/wp-config.php
Expected Output:
HTTP/1.1 403 Forbidden
**Purpose: Prevents file from being served if misconfigured or still under web root.
Apply ACLs
setfacl -m u:www-data:r /var/www/html/wp-config.php
getfacl /var/www/html/wp-config.php
Expected Output:
user::rw-
user:www-data:r--
group::---
mask::r--
other::---
**Purpose: Grants only the www-data user read access, even if ownership is root. Granular access control.
Make File Immutable
sudo chattr +i /var/www/html/wp-config.php
lsattr /var/www/html/wp-config.php
Expected Output:
----i--------e----- wp-config.php
**Purpose: Prevents modification/deletion of file, even by root, until immutable flag is removed (chattr -i). Stops malware from overwriting the config.
Use Symlink to Secure Location
mv /var/www/html/wp-config.php /etc/wp-config-secure.php
ln -s /etc/wp-config-secure.php /var/www/html/wp-config.php
ls -l /var/www/html/wp-config.php
Expected Output:
lrwxrwxrwx 1 root root 28 Sep 24 wp-config.php -> /etc/wp-config-secure.php
**Purpose: Keeps real config in a secure system directory, only exposing a symlink in the web root.
Apply SELinux/AppArmor Context
chcon -t httpd_sys_content_t /var/www/html/wp-config.php
ls -Z /var/www/html/wp-config.php
Expected Output (SELinux enabled):
-rw-------. root www-data system_u:object_r:httpd_sys_content_t:s0 wp-config.php
**Purpose: Uses SELinux/AppArmor to strictly control which processes can access the file.
Encrypt with GPG
gpg -c /var/www/html/wp-config.php
ls -l /var/www/html/wp-config.php.gpg
Expected Output:
-rw-r--r-- 1 root root 2000 Sep 24 wp-config.php.gpg
**Purpose: Protects config in backups and repos. Only decrypted on deploy. Ensures secrets arent stored in plaintext.
Disable Directory Listings
-
Apache:
Options -Indexes -
Nginx:
autoindex off;
Verification:
curl -I http://localhost/wp-content/
Expected Output:
HTTP/1.1 403 Forbidden
**Purpose: Prevents attackers from browsing directories in case of misconfigurations.
Use Environment Variables
Edit wp-config.php:
define('DB_USER', getenv('WP_DB_USER'));
define('DB_PASSWORD', getenv('WP_DB_PASS'));
Set variables in system:
export WP_DB_USER=wpuser
export WP_DB_PASS=securepass
printenv | grep WP_DB
Expected Output:
WP_DB_USER=wpuser
WP_DB_PASS=securepass
**Purpose: Credentials stored outside code. Safer for CI/CD and avoids committing secrets.
Monitor Access Attempts
auditctl -w /var/www/html/wp-config.php -p rwxa -k wpconfig_audit
ausearch -k wpconfig_audit
Expected Output:
time->Tue Sep 24 10:45:12 2025
type=PATH msg=audit(...): item=0 name="/var/www/html/wp-config.php"
**Purpose: Detects every read/write/execute attempt on the file. Helps identify intrusion or suspicious behavior.
Practical Scenarios
| Scenario | Method Applied | Outcome |
|---|---|---|
| Fresh install | chmod 600 | DB creds protected. |
| Shared VPS | Root ownership + 640 | PHP can read, root controls writes. |
| CI/CD pipeline | Env vars + symlinks | Secrets secured in deployment process. |
| Host misconfig | Web server deny rules | HTTP requests blocked. |
| Malware injection | chattr +i | Config can’t be overwritten. |
| Forensics | Audit logging | Suspicious file access detected. |
Best Practices
- Use least privilege: start with
600or640. - Combine multiple layers: permissions + ownership + server rules + monitoring.
- Move
wp-config.php**outside web root if possible. - **Never use
777for any fix. Correct ownership instead. - Use **root ownership with
640in production. - Enable **immutable flag (
chattr +i) after config is finalized. - Add **deny rules in Apache/Nginx/OLS as a fail-safe.
- Prefer **environment variables for DB creds in CI/CD pipelines.
- Encrypt backups containing
wp-config.php. - Use **audit logging to monitor all access attempts.
- Recheck perms after migration or restore.
- Regularly test with
curlto ensure file isnt accessible.
Quick Lab
cd /var/www/html
# 1. Lock down permissions
chmod 600 wp-config.php
ls -l wp-config.php
# 2. Root owns with group-read
chown root:www-data wp-config.php
chmod 640 wp-config.php
ls -l wp-config.php
# 3. Make immutable
sudo chattr +i wp-config.php
lsattr wp-config.php
# 4. Test HTTP access
curl -I http://localhost/wp-config.php
Expected Results:
- File shows
rw-r----- root www-data. lsattrshowsiflag.curlreturns403 Forbidden.
Cheat Sheet
| Method | Command | Expected Output | Purpose |
|---|---|---|---|
| Restrict perms | chmod 600 wp-config.php | -rw------- | Owner-only access |
| Root ownership | chown root:www-data wp-config.php && chmod 640 | -rw-r----- root www-data | Root controls, PHP can read |
| Move above root | mv wp-config.php /var/www/ | File in /var/www/ | Hidden from web |
| Block access | curl -I http://localhost/wp-config.php | 403 Forbidden | Verify deny rules |
| Immutable flag | chattr +i wp-config.php | ----i-------- in lsattr | Stops modifications |
| ACLs | setfacl -m u:www-data:r wp-config.php | getfacl shows ACL | Granular control |
| Env vars | `printenv | grep WP_DB` | Shows DB creds |
| Audit monitor | ausearch -k wpconfig_audit | Logs read/write events | Detects intrusion |
Mini Quiz
- What does
chmod 600achieve forwp-config.php? - Why is
chown root:www-datawith640safer thanwww-data:www-datawith644? - What is the effect of
chattr +ion a file? - How does moving
wp-config.phpoutside web root improve security? - Which command shows who last accessed
wp-config.php?