WordPress – configuration and troubleshooting

This article will contain a number of tips and tricks when working with WordPress.

Working with permaLinks

When changing permalinks around in wp-admin, WordPress will warn you if it is unable to make the changes directly to your .htaccess file. This happens when:

- The main .htaccess file for the site is not writable by the web server user
- The Apache vhost setting, AllowOverride, is not set to 'All'
- Apache mod_rewrite may not be enabled

If wp-admin is unable to write the changes into the .htaccess, you can do this manually by:

[root@web01 ~]# vim /var/www/vhosts/example.com/.htaccess
...
# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPress
...

Password protecting wp-admin

As people can oftentimes use weak passwords for their WordPress users, it is recommended to password protect the entire wp-admin login portal with a strong password as shown below:

First create the htaccess username and password:

[root@web01 ~]# htpasswd -c /etc/httpd/conf.d/example.com-wp-admin-htpasswd your_username

Then update the .htaccess file within the wp-admin directory by:

[root@web01 ~]# vim /var/www/vhosts/example.com/wp-admin/.htaccess
...
<Files admin-ajax.php>
    Order allow,deny
    Allow from all
    Satisfy any
</Files>
AuthType Basic
AuthName " Restricted"
AuthUserFile /etc/httpd/conf.d/example.com-wp-admin-htpasswd
Require valid-user
...

Disable PHP execution in uploads directory

When a site becomes compromised, malware is often uploaded that can be executed easily. Below is a common example for disabling PHP execution within the wp-content/uploads directory to help minimize the impact of a compromise:

[root@web01 ~]# vim /var/www/vhosts/example.com/wp-content/uploads/.htaccess
...
# Prevent PHP execution
<Files *.php>
deny from all
</Files>
...

Blocking xmlrpc.php attacks

XML-RPC is often subjected to brute force attacks within WordPress. These attempts can create severe resource contention issues, causing performance issues for the site.

Before blocking this blindly, there are modules such as JetPack, WordPress Desktop and Mobile apps that need XML-RPC enabled. So use caution! JetPack can mitigate these brute force attacks if the option is enabled within the plugin.

First determine if xmlrpc.php is being brute forced by checking your site’s access log as shown below. Generally hundreds or thousands of these entries would be found within a short period of time.

[root@web01 ~]# tail /var/log/httpd/example.com-access.log
....
xxx.xxx.xxx.xxx - - [19/May/2016:15:45:02 +0000] "POST /xmlrpc.php HTTP/1.1" 200 247 "-" "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1"
xxx.xxx.xxx.xxx - - [19/May/2016:15:45:02 +0000] "POST /xmlrpc.php HTTP/1.1" 200 247 "-" "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1"
xxx.xxx.xxx.xxx - - [19/May/2016:15:45:03 +0000] "POST /xmlrpc.php HTTP/1.1" 200 247 "-" "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1"
...

The brute force attacks against xmlrpc.php can be blocked by adding the following in the site’s .htaccess file:

[root@web01 ~]# vim /var/www/vhosts/example.com/.htaccess
...
# Block WordPress xmlrpc.php requests
<Files xmlrpc.php>
order allow,deny
deny from all
</Files>
...

Force SSL on wp-admin

To force all logins for wp-admin to go over SSL, update the site’s wp-config.php with the options below. Just be sure to put this before the line “/* That’s all, stop editing! Happy blogging. */”:

[root@web01 ~]# vim /var/www/vhosts/example.com/wp-config.php
...
define('FORCE_SSL_LOGIN', true);
define('FORCE_SSL_ADMIN', true);
/* That's all, stop editing! Happy blogging. */
...

Make WordPress aware of SSL termination on the load balancer

When using SSL termination on the load balancer or perhaps through something like CloudFlare, you can sometimes create a redirect loop on the site. WordPress needs to believe that everything is really going over SSL since the load balancer is already handling that, and not the server. This can be corrected by adding the following near the top of the site’s .htaccess file:

[root@web01 ~]# vim /var/www/vhosts/example.com/.htaccess
...
SetEnvIf X-Forwarded-Proto https HTTPS=on
...

Search for outdated versions of WordPress

A primary reason why WordPress sites get compromised is due to outdated versions of the software. If a server has dozens of WordPress sites, it can be time consuming to determine what sites are running what versions. Shown below is a quick method of obtaining the versions of WordPress on the server:

[root@web01 ~]# yum install mlocate
[root@web01 ~]# updatedb
[root@web01 ~]# locate wp-includes/version.php | while read x; do echo -n "$x : WordPress Version " && egrep '^\s*\$wp_version\s*=' "$x" | cut -d\' -f2; done | column -t -s :
/var/www/vhosts/example.com/wp-includes/version.php         WordPress Version 4.3.1
/var/www/vhosts/example2.com/wp-includes/version.php        WordPress Version 4.3.3
/var/www/vhosts/example3.com/wp-includes/version.php        WordPress Version 3.9.4
/var/www/vhosts/example4.com/wp-includes/version.php        WordPress Version 3.7.1

Compare the versions returned against the following site to see how old the version is:
https://codex.wordpress.org/WordPress_Versions

Error establishing a database connection

This error usually means one of three things:

- The database credentials within wp-config.php may be wrong
- The database server is busy and cannot accept additional connections
- The database itself may be corrupted

To ensure the database credentials are correct, test them by doing the following:

[root@web01 ~]# cat /var/www/vhosts/example.com/wp-config.php | grep -iE 'DB_USER|DB_PASSWORD|DB_HOST|DB_NAME'
define('DB_NAME', 'example');
define('DB_USER', 'wordpress');
define('DB_PASSWORD', 'mysecurepassword');
define('DB_HOST', 'localhost');
[root@web01 ~]# mysql -h localhost -uwordpress -pmysecurepassword
mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| example            |
+--------------------+
2 rows in set (0.00 sec)

Confirm that Apache MaxClients does not exceed the max-connections variable within MySQL. While this example is specific for CentOS 6, it can be easily adapted for any distro. To check these variables, run the following:

[root@web01 ~]# cat /etc/httpd/conf/httpd.conf |grep MaxClients |grep -v \# | head -1
    MaxClients            63
[root@web01 ~]# mysql -e 'show variables where Variable_name like "max_connections";'
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| max_connections | 65    |
+-----------------+-------+

Check for database corruption by adding the following before the line ‘/* That’s all, stop editing! Happy blogging. */’ in the wp-config.php:

[root@web01 ~]# vim /var/www/vhosts/example.com/wp-config.php
...
define( 'WP_ALLOW_REPAIR', true );
...

From there, do the following to repair the corruption:

- Point your browser to the following URL replacing placing the domain according:  http://www.example.com/wp-admin/maint/repair.php
- Select 'Repair database'
- Once done, remove the WP_ALLOW_REPAIR from the wp-config.php

Reset the WordPress Admin password

If you find yourself locked out of wp-admin, you can restore access to the portal by updating the active themes function.php file right after the opening comments as shown below. Just be sure to remove this code immediately after the password is updated:

[root@web01 ~]# vim /var/www/vhosts/example.com/wp-content/themes/twentyfifteen/functions.php
<?php
wp_set_password( 'your_secure_password_here', 1 );
...

Another way of resetting the admin password is to update MySQL directly by:

[root@web01 ~]# mysql
mysql> use your_wordpress_db_name;
mysql> UPDATE wp_users SET user_pass=MD5('your_new_password_here') WHERE user_login='admin';

Find number of SQL queries executed on each page load

To quickly determine how many queries a page is making to the database, add the following to the active theme’s footer.php near the top:

[root@web01 ~]# vim /var/www/vhosts/example.com/wp-content/themes/twentyfifteen/footer.php
...
<?php if ( current_user_can( 'manage_options' ) ) {
echo $wpdb->num_queries . " SQL queries performed.";
} else {
  // Uncomment the below line to show SQL queries to everybody
  echo $wpdb->num_queries . " SQL queries performed.";
}?>

This will display the query count at the bottom of every page. The public will be able to see this, so do not leave this in your footer.php longer than needed. In the example above on the second to last line, you can comment that out so only someone logged into WordPress will be able to see the results.

Enable the WordPress debug log

WordPress has the ability to log all errors, notices and warnings to a file called debug.log. This file is placed by default in wp-content/debug.log. This will hide the errors from showing up on the production site, and simply allow the developers to review them at their leisure.

To enable this, first create the log file and allow it to be writable by the web server user, then insert the following before the line ‘/* That’s all, stop editing! Happy blogging. */’ in the wp-config.php file as shown below:

[root@web01 ~]# touch /var/www/vhosts/example.com/wp-content/debug.log
[root@web01 ~]# chown apache:apache /var/www/vhosts/example.com/wp-content/debug.log
[root@web01 ~]# vim /var/www/vhosts/example.com/wp-config.php
// Enable WP_DEBUG mode
define( 'WP_DEBUG', true );

// Enable Debug logging to the /wp-content/debug.log file
define( 'WP_DEBUG_LOG', true );

// Disable display of errors and warnings 
define( 'WP_DEBUG_DISPLAY', false );
@ini_set( 'display_errors', 0 );
...
/* That's all, stop editing! Happy blogging. */

Deactivating WordPress plugins

This is useful when trying to determine which plugins are causing memory leaks or overall performance issues. This should only be done after creating a backup of the database and also manually backing up the wp-content/plugins directory so a rollback option exists just in case.

Keep in mind this will break the site since you may be disabling plugins that the site requires to work.

If you prefer to disable to disable the modules one by one until the problem module is identified:

[root@web01 ~]# cd /var/www/vhosts/example.com/wp-content/plugins
[root@web01 ~]# mv akismet akismet.disabled

To disable all the modules at once, then enable them one by one after testing the site each time to see if the issue manifests, do the following:

[root@web01 ~]# mkdir /var/www/vhosts/example.com/wp-content/plugins.disabled
[root@web01 ~]# mv /var/www/vhosts/example.com/wp-content/plugins/* /var/www/vhosts/example.com/wp-content/plugins.disabled
[root@web01 ~]# cd /var/www/vhosts/example.com/wp-content/plugins
[root@web01 ~]# mv ../plugins.disabled/akismet .
[root@web01 ~]# mv ../plugins.disabled/buddypress .
etc

WordPress setup on CentOS 6

Setting up WordPress is a pretty common task. However all too often I see people installing WordPress, and setting the ownership to ‘apache:apache’ recursively. While this makes life easier for the administrator, it opens up a host of security issues.

Taken directly from WordPress’s best practice guide on permissions:

Typically, all files should be owned by your user (ftp) account on your web server, and should be writable by that account. On shared hosts, files should never be owned by the web server process itself (sometimes this is www, or apache, or nobody user).

Most people know that using FTP is bad. However if you plan on using the wp-admin portal for media uploads, plugin updates, and core updates, you MUST have an FTP server installed and running. Using the Pecl SSH2 library looks like it would work in theory, but in reality, it doesn’t. Or at least, I haven’t found a way to make it work for the wp-admin portal without giving permission errors for this, that and everything in between since it needs weaker permissions. So while your users can use SSH/SCP to upload content via the command line, if they choose to do most of the WordPress tasks through wp-admin like most people would, use the FTP option from within /wp-admin.

This guide is going to show how you can setup WordPress properly accordingly to the note above from WordPress’s best practices guide on permissions. This guide will assume that you already have a working LAMP stack installed.

FTP Server Setup

First, install an FTP server called vsftpd:

[root@web01 ~]# yum install vsftpd
[root@web01 ~]# chkconfig vsftpd on

Now disable anonymous logins since vsftpd enables this by default for some reason:

[root@web01 ~]# vim /etc/vsftpd/vsftpd.conf
...
anonymous_enable=NO
...
[root@web01 ~]# service vsftpd restart

Then confirm you have a firewall in place that has a default to deny policy. So in the example below, I am only allowing in ports 80 and 443 from the world. Then I have SSH restricted to my IP address. Everything else is blocked, including that FTP server.

[root@web01 ~]# vim /etc/sysconfig/iptables
# Generated by iptables-save v1.4.7 on Fri Nov 13 19:24:15 2015
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [2:328]
-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 
-A INPUT -p tcp -m tcp --dport 443 -j ACCEPT 
-A INPUT -p tcp -m tcp --dport 80 -j ACCEPT 
-A INPUT -i eth0 -s xx.xx.xx.xx/32 -p tcp -m tcp --dport 22 -m comment --comment "Allow inbound SSH from remote ip" -j ACCEPT
-A INPUT -p icmp -j ACCEPT 
-A INPUT -i lo -j ACCEPT 
-A INPUT -j REJECT --reject-with icmp-host-prohibited 
-A FORWARD -j REJECT --reject-with icmp-host-prohibited 
COMMIT
# Completed on Fri Nov 13 19:24:15 2015

Database Setup

Create a database for your new WordPress site by:

[root@web01 ~]# mysql
mysql> create database your_database;

Now grant access for that database to a user:

[root@web01 ~]# mysql
mysql> grant all on your_database.* to 'your_db_user'@'localhost' identified by 'your_secure_db_password';
mysql> flush privileges;
mysql> quit

Apache Setup

First, create a FTP/SCP user:

[root@web01 ~]# mkdir -p /var/www/vhosts/example.com
[root@web01 ~]# chmod 755 /var/www/vhosts/example.com
[root@web01 ~]# useradd -d /var/www/vhosts/example.com example_site_user
[root@web01 ~]# passwd example_site_user

Now setup the Apache vhost:

[root@web01 ~]# vim /etc/httpd/vhost.d/example.com.conf
<VirtualHost *:80>
        ServerName example.com
        ServerAlias www.example.com
        #### This is where you put your files for that domain
        DocumentRoot /var/www/vhosts/example.com

        ### Enable this if you are using a SSL terminated Load Balancer
        SetEnvIf X-Forwarded-Proto https HTTPS=on

	#RewriteEngine On
	#RewriteCond %{HTTP_HOST} ^example.com
	#RewriteRule ^(.*)$ http://www.example.com [R=301,L]

        <Directory /var/www/vhosts/example.com>
                Options -Indexes +FollowSymLinks -MultiViews
                AllowOverride All
		Order deny,allow
		Allow from all
        </Directory>
        CustomLog /var/log/httpd/example.com-access.log combined
        ErrorLog /var/log/httpd/example.com-error.log
        # New Relic PHP override
        <IfModule php5_module>
               php_value newrelic.appname example.com
        </IfModule>
        # Possible values include: debug, info, notice, warn, error, crit,
        # alert, emerg.
        LogLevel warn
</VirtualHost>


##
# To install the SSL certificate, please place the certificates in the following files:
# >> SSLCertificateFile    /etc/pki/tls/certs/example.com.crt
# >> SSLCertificateKeyFile    /etc/pki/tls/private/example.com.key
# >> SSLCACertificateFile    /etc/pki/tls/certs/example.com.ca.crt
#
# After these files have been created, and ONLY AFTER, then run this and restart Apache:
#
# To remove these comments and use the virtual host, use the following:
# VI   -  :39,$ s/^#//g
# RedHat Bash -  sed -i '39,$ s/^#//g' /etc/httpd/vhost.d/example.com.conf && service httpd reload
# Debian Bash -  sed -i '39,$ s/^#//g' /etc/apache2/sites-available/example.com && service apache2 reload
##

# <VirtualHost _default_:443>
#        ServerName example.com
#        ServerAlias www.example.com
#        DocumentRoot /var/www/vhosts/example.com
#        <Directory /var/www/vhosts/example.com>
#                Options -Indexes +FollowSymLinks -MultiViews
#                AllowOverride All
#        </Directory>
#
#        CustomLog /var/log/httpd/example.com-ssl-access.log combined
#        ErrorLog /var/log/httpd/example.com-ssl-error.log
#
#        # Possible values include: debug, info, notice, warn, error, crit,
#        # alert, emerg.
#        LogLevel warn
#
#        SSLEngine on
#        SSLCertificateFile    /etc/pki/tls/certs/2016-example.com.crt
#        SSLCertificateKeyFile /etc/pki/tls/private/2016-example.com.key
#        SSLCACertificateFile /etc/pki/tls/certs/2016-example.com.ca.crt
#
#        <IfModule php5_module>
#                php_value newrelic.appname example.com
#        </IfModule>
#        <FilesMatch \"\.(cgi|shtml|phtml|php)$\">
#                SSLOptions +StdEnvVars
#        </FilesMatch>
#
#        BrowserMatch \"MSIE [2-6]\" \
#                nokeepalive ssl-unclean-shutdown \
#                downgrade-1.0 force-response-1.0
#        BrowserMatch \"MSIE [17-9]\" ssl-unclean-shutdown
#</VirtualHost>

Then restart Apache to apply the changes:

[root@web01 ~]# service httpd restart

WordPress Setup

Download a copy of WordPress, uncompress, and move the files into place by:

[root@web01 ~]# cd /var/www/vhosts/example.com
[root@web01 ~]# wget http://wordpress.org/latest.tar.gz && tar -xzf latest.tar.gz
[root@web01 ~]# mv wordpress/* ./ && rmdir ./wordpress && rm -f latest.tar.gz

Update the files and directories ownership to lock it down accordingly:

[root@web01 ~]# chown -R example_site_user:example_site_user /var/www/vhosts/example.com

Then open up a few files so wp-admin can manage the .htaccess, and so it can install plugins, upload media, and use the cache if you choose to configure it:

[root@web01 ~]# mkdir /var/www/vhosts/example.com/wp-content/uploads
[root@web01 ~]# mkdir /var/www/vhosts/example.com/wp-content/cache
[root@web01 ~]# touch /var/www/vhosts/example.com/.htaccess
[root@web01 ~]# chown apache:apache /var/www/vhosts/example.com/wp-content/uploads
[root@web01 ~]# chown apache:apache /var/www/vhosts/example.com/wp-content/cache
[root@web01 ~]# chown apache:apache /var/www/vhosts/example.com/.htaccess

And thats it! Once you have the domain setup in DNS, you should be able to navigate to the domain, and follow the WordPress installation wizard to complete the setup. Afterwards, log into wp-admin, and try to update a plugin, or install a new one. When it prompts you for the FTP information, be sure to use:

Hostname:  localhost
FTP Username:  example_site_user
FTP Username:  example_site_user_pw
Connection Type:  FTP

Apache Proxypass

Many solutions today are built using highly available configurations that can easily scale. Setting up a solution to scale is easy, but getting your web application to work correctly with a multi-server configuration can be difficult as not everyone has access to a quality shared storage solution that is fast and reliable.

In many web applications such as WordPress, you typically want all your wp-admin traffic to go to the master server. There are probably a dozen ways to go about this, many of which get very over complicated with wacky Varnish configurations handling the redirection, or even with Nginx.

These is where ProxyPass can offer a cleaner alternative. ProxyPass allows you to take a request for a specific URL, and forward it to another server, which would be known as your backend server, or your master web server.

This guide will assume that you are performing this on all web servers in the solution, unless otherwise specified. The specific examples are for a WordPress based solution, but it can be easily adapted for other CMS’s.

To get started, first ensure that mod_proxy is installed:

# CentOS 6
[root@web01 ~]# yum install mod_proxy_html
[root@web01 ~]# service httpd restart
[root@web01 ~]# httpd -M |grep proxy
 proxy_module (shared)
 proxy_balancer_module (shared)
 proxy_ftp_module (shared)
 proxy_http_module (shared)
 proxy_connect_module (shared)
 proxy_ajp_module (shared)

# Ubuntu 12.04 and 14.04
[root@web01 ~]# apt-get update
[root@web01 ~]# apt-get install libapache2-mod-proxy-html
[root@web01 ~]# a2enmod proxy proxy_http

There are several ways you can proceed from here. I’ll post them out as ‘options’ below. Each one basically accomplishes the same thing, but one may work better for your environment than another.

So no matter which of the 3 options you go with, always be sure to rigorously test it before implementing it in production!

Option 1: Easy – Define master server based off the URI in each Apache Vhost

This example is simple. In each Apache Vhost, add the following lines on each slave web server to point wp-admin and wp-login.php to your master server, which in this case is 192.168.2.1:

# CentOS 6
[root@web02 ~]# vim /etc/httpd/vhost.d/example.com.conf

# Ubuntu 12.04 and 14.04
[root@web02 ~]# vim /etc/apache2/sites-enabled/example.com.conf
...
ProxyPreserveHost On
        ProxyRequests Off
        ProxyPassMatch ".*/wp-admin.*" "http://192.168.2.1"
        ProxyPassMatch ".*/wp-login.php" "http://192.168.2.1"
...

Option 2: Advanced – Define master server based off URI using location blocks in each Apache Vhost

This example is slightly more advanced. In each Apache Vhost, add the following location blocks to point wp-admin and wp-login.php to your master server, which in this case is 192.168.2.1. We’re also manually defining the host header within these location blocks, which gives you the option to start excluding specific items if needed:

# CentOS 6
[root@web02 ~]# vim /etc/httpd/vhost.d/example.com.conf

# Ubuntu 12.04 and 14.04
[root@web02 ~]# vim /etc/apache2/sites-enabled/example.com.conf
...
ProxyRequests Off
  ProxyPreserveHost Off
  ProxyVia Off
  <Location "/wp-login.php">
    Header set "Host" "www.example.com"
    ProxyPass http://192.168.2.1/wp-login.php
    ProxyPassReverse http://192.168.2.1/wp-login.php
  </Location>
  <Location "/wp-admin">
    Header set "Host" "www.example.com"
    ProxyPass http://192.168.2.1/wp-admin
    ProxyPassReverse http://192.168.2.1/wp-admin
  </Location>

Option 3: Complex – Define master server in global Apache configuration, and only send over POST requests for wp-admin

This example is more complex. You are defining the master server (192.168.2.1) in your global Apache configuration, then configuring each Apache Vhost to only send over POST requests for wp-admin to the master server.

Setup proxypass so it knows which server is the master web server. Be sure to update the IP so its the IP address of your master web server:

# CentOS 6
[root@web01 ~]# vim /etc/sysconfig/httpd
...
OPTIONS="-DSLAVE"
export MASTER_SERVER="192.168.2.1"
...

# Ubuntu 12.04 and 14.04
[root@web01 ~]# /etc/apache2/envvars
...
export APACHE_ARGUMENTS="-DSLAVE"
export MASTER_SERVER="192.168.2.1"
...

Now on your slave web servers, we need to update the site’s vhost configuration to proxy the requests for /wp-admin so they will route to the master web server:

# CentOS 6
[root@web02 ~]# vim /etc/httpd/vhost.d/example.com.conf

# Ubuntu 12.04 and 14.04
[root@web02 ~]# vim /etc/apache2/sites-enabled/example.com.conf
...
<IfDefine SLAVE>
RewriteEngine On
ProxyPreserveHost On
ProxyPass /wp-admin/ http://${MASTER_SERVER}/wp-admin/
     ProxyPassReverse /wp-admin/ http://${MASTER_SERVER}/wp-admin/
RewriteCond %{REQUEST_METHOD} =POST
     RewriteRule . http://${MASTER_SERVER}%{REQUEST_URI} [P]
</IfDefine>
...

# CentOS 6
[root@web02 ~]# service httpd restart

# Ubuntu 12.04 and 14.04
[root@web02 ~]# service apache2 restart

That slave server(s) should now start proxying the /wp-admin requests and sending them over to the master web server. Please be sure to test this out and check your logs to ensure /wp-admin POST requests are now routing to the master web server.