WordPress wp-config.php contains database credentials, authentication keys and salts, and table prefix — exposure of this file via a misconfigured server, a path traversal vulnerability, or a backup plugin that stores files in the webroot can result in full database compromise. Moving wp-config.php one directory above the WordPress root is supported natively: WordPress checks the parent directory automatically if wp-config.php is not found in the webroot, and the parent directory is typically outside the web server document root on standard hosting configurations. For servers where moving the file is not possible, an Apache .htaccess rule that returns 403 for any direct HTTP request to wp-config.php blocks remote access: <Files wp-config.php> Require all denied </Files>. File permissions for a correctly configured WordPress installation follow a strict hierarchy: directories at 755, PHP files at 644, and wp-config.php at 640 or 600 so the web server user can read it but world-read is disabled. The wp-content/uploads/ directory requires 755 for the web server to write uploaded files, but PHP execution inside it should be blocked via an .htaccess rule (php_flag engine off for Apache or a Nginx location block that returns 403 for \.php$) to prevent webshell execution via malicious file uploads. The WordPress authentication keys and salts in wp-config.php should be rotated after any suspected compromise or plugin removal — rotating them immediately invalidates all existing browser sessions and forces re-login for all users. Defining DISALLOW_FILE_EDIT and DISALLOW_FILE_MODS as true in wp-config.php disables the theme and plugin editors in the admin panel and blocks automatic plugin and theme updates, reducing the attack surface if an admin account is compromised. Setting WP_DEBUG to false and WP_DEBUG_LOG to a path outside the webroot on production prevents error messages from leaking database structure, file paths, or credentials in HTTP responses. The rate limiting post and this hardening guide form complementary layers: rate limiting addresses the application layer while file-permission hardening addresses the server layer.
Problem: A default WordPress installation stores wp-config.php in the webroot with world-readable permissions, allows PHP execution in uploads/, and leaves debug output enabled — each of which individually exposes credentials or enables webshell execution.
Solution: Move wp-config.php above the webroot, set its permissions to 640, block PHP execution in uploads/ via server config, deny direct HTTP access to sensitive files with .htaccess rules, and define DISALLOW_FILE_EDIT and WP_DEBUG false in production.
# Set correct ownership and permissions for a WordPress installation
# Replace www-data with your web server user (nginx, apache, etc.)
WP_ROOT="/var/www/html/wordpress"
# Directories: 755 (owner rwx, group rx, world rx)
find "$WP_ROOT" -type d -exec chmod 755 {} \;
# PHP/other files: 644
find "$WP_ROOT" -type f -exec chmod 644 {} \;
# wp-config.php: 640 (owner rw, group r, world none)
chmod 640 "$WP_ROOT/wp-config.php"
chown root:www-data "$WP_ROOT/wp-config.php" # root owns, web server group reads
# uploads/ directory: web server must write, but PHP must not execute
chmod 755 "$WP_ROOT/wp-content/uploads"
find "$WP_ROOT/wp-content/uploads" -type f -exec chmod 644 {} \;
# .htaccess — block direct access to wp-config.php and sensitive files
<Files wp-config.php>
Require all denied
</Files>
<Files .htaccess>
Require all denied
</Files>
<Files xmlrpc.php>
Require all denied
</Files>
# Block PHP execution in uploads/ — place this in wp-content/uploads/.htaccess
<Files *.php>
Require all denied
</Files>
// wp-config.php — production hardening constants
// Disable theme and plugin file editors in wp-admin
define('DISALLOW_FILE_EDIT', true);
// Disable plugin/theme installation and updates from wp-admin
define('DISALLOW_FILE_MODS', true);
// Disable debug output on production
define('WP_DEBUG', false);
define('WP_DEBUG_DISPLAY', false);
// Log errors to a file outside the webroot instead of displaying them
define('WP_DEBUG_LOG', '/var/log/wordpress/debug.log');
ini_set('log_errors', 1);
ini_set('error_log', '/var/log/wordpress/php-errors.log');
// Force HTTPS for admin and login
define('FORCE_SSL_ADMIN', true);
// Use a non-default table prefix (set at install time)
// \$table_prefix = 'wp_'; // change this to something unique like 'xK9p_'
// Limit post revisions to reduce wp_posts bloat
define('WP_POST_REVISIONS', 5);
// Move wp-config.php one directory above the webroot (no code change needed)
// WordPress automatically checks the parent directory:
// /var/www/wp-config.php <-- place here
// /var/www/html/ <-- webroot (WordPress files)
NOTE: After rotating authentication keys and salts in wp-config.php, all logged-in users including admins will be immediately logged out and will need to re-authenticate. Do this during a low-traffic window and notify active admins in advance. Fresh keys can be generated at https://api.wordpress.org/secret-key/1.1/salt/.