« Back to home

Wordpress on Nginx

Posted on
Wordpress on Nginx

This is an old post. It may contain broken links and outdated information.

Wordpress is the Microsoft Word of blogging platforms—it's overkill for almost everyone, but everyone uses it anyway. It's a popular, monstrous, ugly app that requires regular patching to keep evildoers from doing evil with it, but it's still a top choice for self-hosted blogging because if you can fight your way through its ridiculously complex interface, you can use it to make a good-looking blog without having to know a lot about HTML or CSS.

I'm going to focus on taking common .htaccess-based security practices and turning them into Nginx-specific location directives and rules.

Our blogging platform here at the Bigdino compoud is obviously Octopress—which you're reading right now—but I had occasion to stand up a Wordpress blog recently and wanted to share what I learned doing it. Wordpress's ubiquity means that there are a million-billion-trillion guides out there for getting it working; however, the vast majority of them focus on how to make it work with Apache, not Nginx. What I hope differentiates this post is that I'm going to focus on taking common .htaccess-based security practices and turning them into Nginx-specific location directives and rules.

Prerequisites

This article shares a common heritage with the previous post on MediaWiki, and has mostly the same list of prereqs. You need to have some flavor of PHP installed; for Nginx, the obvious recommendation is PHP-FPM, a bundle of PHP5 and the FastCGI Process Manager. You'll also want Alternative PHP Cache (APC) installed, along with the PHP5-memcache extension to cache PHP sessions instead of having to rely on your filesystem.

You of course need a database. My preferred choice for most things is PostgreSQL, but that's a second-tier option for Wordpress which requires additional hoops to configure. So, I recommend the latest stable release of MariaDB, a fast substitute for MySQL that retains binary compatability but is developed independently from Oracle. They have an Ubuntu PPA available for quick and painless installation. Depending on your level of comfort with MySQL/MariaDB, you might also want to download and install PHPMyAdmin (which you can do from the link or from the command line with a quick sudo aptitude install phpmyadmin).

Finally, you need Wordpress, which you can get from their install instructions page. We're going to pick up right here, after you've downloaded Wordpress and gotten the database set up.

Basic Nginx configuration

We're going to do this in two separate files, as with most of the other configurations I use. The first file will contain the virtual host definitions for the HTTP and HTTPS versions of the site, stored in the sites-available directory and symlinked to the sites-enabled directory per common Apache convention; this file will set up the HTTP and HTTPS sites and contain configuration directives unique to each. The second file will contain configuration directives that both the HTTP and HTTPS sites have in common. This is to save us from having to maintain the config stanzas twice within the same file and lessens the likelihood of typos (since we'll only be typing each config line once).

I'll list the entire configuration file first, and then go through its components:

server {  
    server_name yourblog.yoursite.com;
    root /path/to/wordpress;
    index index.php;
    autoindex off;
    include conf.sites/wordpress-both.conf;

    location ~ ^/.*\.php {
        try_files $uri =404;
        include fastcgi_params;
        fastcgi_pass php5-fpm-sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_intercept_errors on;
    }

}

# HTTPS server
server {  
    listen 443 ssl;
    server_name yourblog.yoursite.com;
    root /path/to/wordpress;
    index index.php;
    autoindex off;
    include conf.sites/wordpress-both.conf;

    ssl on;
    ssl_certificate /path/to/your/ssl/cert.crt;
    ssl_certificate_key /path/to/your/ssl/private.key;
    ssl_protocols TLSv1.2 TLSv1.1 TLSv1 SSLv3;
    ssl_ciphers ECDHE-RSA-AES256-SHA384:AES256-SHA256:RC4:HIGH:!MD5:!aNULL:!EDH:!AESGCM;
    ssl_prefer_server_ciphers on;

    location ~ ^/.*\.php {
        try_files $uri =404;
        include fastcgi_params;
        fastcgi_pass php5-fpm-sock;
        fastcgi_param HTTPS on;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_intercept_errors on;
    }
}

This is a pretty basic configuration—we'll do more fancy stuff in conf.sites/wordpress-both.conf, which is our HTTP and HTTPS common configuration file. Here's what the above does:

server {  
    server_name yourblog.yoursite.com;
    root /path/to/wordpress;
    index index.php;
    autoindex off;
    include conf.sites/wordpress-both.conf;

This is a typical Nginx server configuration block. We define the hostname that Nginx will listen for so it knows when to use this configuration; the file system path to the web root directory; and the index file or files that Nginx will serve up when given a URI that matches a directory. We also disable auto-indexing, which prevents Nginx from displaying the contents of directories that lack index files. Finally, we use the include line to connect to our other configuration file, which we'll get to in a bit.

Next we set up a location to handle PHP files:

    location ~ ^/.*\.php {
        try_files $uri =404;
        include fastcgi_params;
        fastcgi_pass php5-fpm-sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_intercept_errors on;
    }

The first line is a regex that will catch all URLs which end in .php, regardless of where. The try files directive forces Nginx to return a 404 "Not Found" error if the submitted URI doesn't exactly match an existing file; this is to avoid a known configuation pitfall which can cause PHP to inadvertantly execute malicious PHP files that lack PHP extensions. The last three lines tell Nginx how to talk to the FastCGI handler (PHP-FPM for us). We're using Unix sockets (fastcgi pass php5-fpm-sock) instead of TCP ports to pass data between Nginx and PHP, and so should you, unless your PHP installation lives on a separate box from yoiur web server. The php5-fpm-sock variable above points to an upstream location defined in /etc/nginx/conf.d/php5-fpm.conf:

upstream php5-fpm-sock {  
    server unix:/var/run/php5-fpm.soc;
}

Defining it there and referencing it in different sites lets you make changes to it as needed without having to touch all the site definitions that use it.

HTTPS differences

The HTTPS section is mostly the same, except that it includes some additional lines. The server block is modified slightly to add a listening port; there is an SSL- specific block of code to turn on HTTPS and specify all the necessary parameters to make it work, incuding where your certificate and key are located; and finally each of the PHP handler locations have a parameter added (fastcgi_param HTTPS on;) to deal with being run under HTTPS.

The separate included config file

Both sections referenece the conf.sites/wordpress-both.conf file, which contains a bunch of locations and settings that are common to both HTTP and HTTPS servers, but which I keep in a separate file so that if they ever have to be changed, they only have to be changed once instead of twice. Here's that file, with comments to explain each section.

# Common root location
    location / {
#    This try_files directive is used to enable pretty, SEO-friendly URLs
#    and permalinks for Wordpress. Leave it *off* to start with, and then
#    turn it on once you've gotten Wordpress configured!
#    try_files $uri $uri/ /index.php?$args; 
    }

#    This location pevents any requests for the Wordpress admin interface
#    from being accepted if those requests don't come from your LAN. This
#    is optional but recommended.
    location ~* wp-admin {
        try_files $uri $uri/ =404;
        allow 192.168.1.0/24;
        allow 127.0.0.1;
        deny all;
    }

#    Show "Not Found" 404 errors in place of "Forbidden" 403 errors, because
#    forbidden errors allow attackers potential insight into your server's
#    layout and contents
    error_page 403 =404;

#    Prevent access to any files starting with a dot, like .htaccess
#    or text editor temp files
    location ~ /\. { access_log off; log_not_found off; deny all; }

#    Prevent access to any files starting with a $ (usually temp files)
    location ~ ~$ { access_log off; log_not_found off; deny all; }

#    Do not log access to robots.txt, to keep the logs cleaner
    location = /robots.txt { access_log off; log_not_found off; }

#    Do not log access to the favicon, to keep the logs cleaner
    location = /favicon.ico { access_log off; log_not_found off; }

#    Keep images and CSS around in browser cache for as long as possible,
#    to cut down on server load
    location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
        expires max;
        log_not_found off;
    }

#    Common deny or internal locations, to help prevent access to areas of
#    the site that should not be public
    location ~* wp-admin/includes { deny all; }
    location ~* wp-includes/theme-compat/ { deny all; }
    location ~* wp-includes/js/tinymce/langs/.*\.php { deny all; }
    location /wp-content/ { internal; }
    location /wp-includes/ { internal; }
#    The next line protects the wp-config.php file from being accessed, but
#    we need to be able to run the file for the initial site setup. Uncomment
#    the next line after setup is completed and reload Nginx.
#    location ~* wp-config.php { deny all; }

#    Prevent any potentially-executable files in the uploads directory from
#    being executed by forcing their MIME type to text/plain
    location ~* ^/wp-content/uploads/.*.(html|htm|shtml|php)$ {
        types { }
        default_type text/plain;
    }

#    Add trailing slash to */wp-admin requests so the admin interface
#    works correctly
    rewrite /wp-admin$ $scheme://$host$uri/ permanent;

There are several things in the file above that deserve closer scrutiny. First, the root location directive contains a try_files directive that we have commented out for now. This directive takes the place of a large Apache rewrite rule to make the site URLs more human-readable and search engine-friendly. Since we have to set an option in Wordpress's configuration in order to use this, we need to leave it commented out for now.

Secondly, the wp-admin location is set to be inaccessible from anywhere except to hosts coming from your LAN's IP address range (which in this example is 192.168.1.0/24). If you plan to administer Wordpress from outside of your LAN you can remove this section, but you'll then be exposing your Wordpress administrative interface log in screen to the big bad Internet. Don't do this if your password is 12345.

The common deny and internal locations use the deny and internal flags to help keep folks out of places they don't need to go. Like most php applications, there are large chunks of Wordpress that are required for it to work correctly but don't need to be poked and prodded directly by the end users.

The next block puts some restrictions around the uploads directory. If you have multiple writers or editors or admins on your Wordpress site, all of them can put files in uploads. This is nominally a place for images and perhaps music and movies, but it is possible to giet PHP files or HTML files with scripted content into that location. So, just in case this happens, we force Nginx to serve any files with extensions htm, html, shtml, or php with their MIME type set to text/plain, so that their contents cannot be run.

Finally, we toss out a quick rewrite in order to slap a trailing slash on the end of wp-admin, without which admin access won't work correctly.

A note on 'if'

As mentioned in the intro, there are lots and lots of Wordpress setup tutorials out there, with lots of ways to skin the proverbial Wordpress cat. I have tried, however, to avoid using Nginx's if directive in any of my configuration files, because if is evil. For a detailed reason of exactly why Nginx's if directive is evil, read this; the short version is that using if inside of location blocks can lead to "unpredictable" behavior unless you are completely sure of the logic. It's almost always easier to carefully consider why you think you need to use if, and then instead substitute in a much faster try_files statement.

Activating your configuration

After you've created both the virtual host file and the added configuration file, symlink the virtual host file into the sites-enabled directory:

$ sudo ln -s /etc/nginx/sites-available/wordpress /etc/nginx/sites-enabled/wordpress

Then reload Nginx with sudo /etc/init.d/nginx reload. The configuration is now live.

Starting Wordpress

You'll next want to pop back to the Wordpress install instructions and fire off the main Wordpress installation script. This will prompt you for many things and should end up with you logged into your brand new shiny blog's admin panel.

Finalizing the configuration

Now that we're operational, there are two things left to do with Nginx. First, we want to modify the configuration to deny access to the newly-created wp-config.php file, as it contains sensitive site information. So, open the conf.sites/wordpress-both.conf file and uncomment this line:

location ~* wp-config.php { deny all; }  

Then, we want to set up Wordpress to use nicer URLs, rather than always displaying an ugly URL featuring a bunch of PHP arguments. Enter the Wordpress admin area and nagivate to Settings, then click Permalinks. In the pane on the left, choose Custom Structure and paste in the following string:

/%year%/%monthnum%/%day%/%postname%/

Save the changes, and then return to editing conf.sites/wordpress-both.conf. Near the top of the file, uncomment the root try_files directive:

try_files $uri $uri/ /index.php?$args;  

Save the file and reload Nginx with sudo /etc/init.d/nginx reload to make those last two changes effective.

Optional speed tweaks

The presence of APC on your web server will automatically speed up all PHP applications by caching their opcodes, but Wordpress's performance can be further improved by configuring it to use APC as its cache for objects as well as opcodes. This can be done with the APC Object Cache Backend plugin.

Further, APC can be used in conjunction with Batcache for whole page caching under load. For single-site servers like we've constructed, this is very much preferred over configuring and maintaining memcache for lots of reasons (not the least of which is that we can pass data to APC with a Unix socket instead of through the TCP stack).

For larger multi-site setups, it might be worthwhile to investigate more complex caching solutions like WP Super Cache or W3 Total Cache, or even a standalone Varnish instance to soak up static asset load, but for a single-user single-blog site like we've constructed in this example, APC will be more than sufficient as long as you've alloted it enough RAM (I'd recommend either 128 or 256 MB if possible).

Further reading and things

You now have a live, secure Wordpress blog. There are lots of other resources you can consult on where to go from here—following this post will get you most of the way through the web server side of the Hardening Wordpress guide. It will also be worth your while to ensure that you don't have any settings enabled in /etc/php5/fpm/php.ini that shouldn't be (some basic searching for "php.ini security" should do the trick). Lastly, if you plan on having comments enabled on your blog, you should consider signing up for a free Akismet account to stop spammers from posting.

As always, suggestions for improvement are very welcome, and please let me know if you spot any mistakes or typos in this post!

Discuss this post on the BigDinosaur forums