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.
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.
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
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:
1 2 3 4 5 6
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:
1 2 3 4 5 6 7
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
1 2 3
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.
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
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.
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
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
Activating your configuration
After you’ve created both the virtual host file and the added configuration file, symlink the virtual host file into the
Then reload Nginx with
sudo /etc/init.d/nginx reload. The configuration is now live.
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:
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:
Save the changes, and then return to editing
conf.sites/wordpress-both.conf. Near the top of the file, uncomment the root
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!