WordPress for Paranoid Part 1

So, if you are a happy owner of nginx , a noble paranoid and for some hell decided to put wordpress, then ... The first thing that came to mind was "you need to limit this creation to freedom!".

I’ll omit the account settings, as well as the php5-fpm settings, since everyone has their own cockroaches, and someone runs apache in general. But here I will describe the general for Wordpress in this part. I’ll write about what I did, what happened, and why.


  • wp-admin
  • wp-content
  • wp-includes

Php files

  • wp-activate.php
  • wp-blog-header.php
  • wp-comments-post.php
  • wp-config.php
  • wp-config-sample.php
  • wp-cron.php
  • wp-links-opml.php
  • wp-load.php
  • wp-login.php
  • wp-mail.php
  • wp-postpass.php (more on that below )
  • wp-settings.php
  • wp-signup.php
  • wp-trackback.php
  • xmlrpc.php
  • xmlrpc.txt (more about this below )

This is a typical suite for Wordpress 4.0.

What do we need? We need to restrict access to php files and the admin panel, render the statics, close xmlrpc.

We restrict access to the admin panel and to php files

In my version of wordpress, I do not save user comments and do not use xmlrpc. How to safely give access to comments, as well as a number of other pressing issues on nginx and wordpress will be discussed in the second part of this article, which, of course, will be created if there are any. Since there is no apache, the .htaccess file is useless.
Therefore, close the above whistle-blowers:

location ~ * ^ / (\. htaccess | xmlrpc \ .php) $ {
 return 404;

After that, for xmlrpc.php and .htaccess requests, we will have a 404 error. Although it is possible to issue both 403 and 200 trololo, but this is already a matter of taste.

Next, we restrict access to the remaining ones. By restriction, I mean an authorization request, namely  auth_basic .

location ~ * ^ / wp-admin / (. * (? * this code will force nginx to ask for authorization when requesting statics from / wp-admin /, statics are given by nginx. 
Next, we restrict access to files:

location ~ * (/ wp-admin / | / wp-cron \ .php | / wp-config \ .php | / wp-config-sample \ .php | / wp-mail \ .php | / wp-settings \. php | / wp-signup \ .php | / wp-trackback \ .php | / wp-activate \ .php | / wp-links-opml \ .php | / wp-load \ .php | / wp-comments-post \ .php | / wp-blog-header \ .php | / wp-login \ .php | / wp-includes /.*? \. php | / wp-content /.*? \. php) {
   auth_basic "protected by password";
   auth_basic_user_file users / somefile;
   root / path / to / site / root;
   # more options

A record like /wp-includes/.*?\.php includes all php files in wp-includes and below.

Done, we have closed access. Now we will selectively include the elements we need for the public version, and this is further in the text.

Enable secure posts in our secure Wordpress

Since we closed wp-login.php with authorization, then by writing a secure post and dropping the link and password (from the post) to the right user, the user ... will be scared of an unknown window. Since the password is passed to the wp-login.php file as a post request with GET parameters ? Action = postpass .

nginx imposes a number of restrictions:
  • in location from nginx we cannot describe the request parameters;
  • auth_basic cannot be used in an if expression;
  • playing with a variable in the config, which is passed 1 in case of successful authorization will not bring anything, since the variable lives only in the current request .

What to do?

There is a solution! Create a symlink to wp-login.php in the same folder. I have this wp-postpass.php . A symbolic link is needed so that if we update wordpress, then wp-login.php will also be updated and the file will be updated by the link ... That's what I love linux for.

Next in the nginx config we write:

location ~ * (/wp-postpass\.php) {
   if ($ args ~ "^ action = postpass $") {
      set $ wppostpass 1;
   if ($ wppostpass ~ 0) {
      return 403;
   # more options

In this case, when requesting /wp-postpass.php?action=postpass, the wppostpass variable will take the value 1, and location will work until the end. In the case of a bare request wp-postpass.php or with other parameters (as you see here it is checked from the beginning ^ to the end of the $ line) there will be a 403 error, which means access is closed.

For such a scheme to work, we need  ngx_http_substitutions_filter_module  . In the config should be registered

subs_filter 'https://example.com/wp-login.php\?action=postpass' 'https://example.com/wp-postpass.php?action=postpass' gi;

Then nginx will automatically change the wp-login.php? Action-postpass link to wp-postpass.php? Action-postpass, and the user will be able to log in with a password to view the protected record.

We take out the statics on a separate server and connect the CDN

In the load, js, css and small gifs do not play a role, since if there is memory nginx stores them in its cache, and if there is enough memory all the site statics can be transferred to the tmpfs section (3.8 GB read-write and 745k iops' s for example).

But in the case of one server, someone will get the file earlier, someone later, and if we have a lot of clients, then when distributing 1000 files of 1MB, the channel will sag well if you do not enter rate.

For these cases, caching CDN providers were invented. For example, cloudflare .

The principle of work is wonderfully illustrated in their picture:

Without CDN, all requests go to the final site, and from CDN requests go to the CDN of the provider, which acts as an intermediate link. And in this case, if 1000 users request a 1 MB file, then this file will be requested by the CDN provider 1 time for its cache, and then 1000 users will be distributed. À la google docs style DDoS options when they requested big_photo.jpg ? Ver = 1 , then  big_photo.jpg ? Ver = 2 , etc. will not work if moderate caching mode is selected (cloudflare has it) and caching is only static, then when requesting  big_photo.jpgbig_photo.jpg? ver = 1 or  big_photo.jpg? ver = 123 big_photo.jpg is requested from the server and then he and only he is heard, even if the client requests a file with arguments (they are simply ignored). This solves the ddos ​​problem by the cdn provider, which in essence should protect against ddos ​​as well.

I didn’t climb much, but I found that the default statics is stored in:
  • / wp-content / uploads /
  • / wp-content / themes /
  • / wp-content / plugins /
  • / wp-includes / js /
  • / wp-includes / css /
  • / wp-includes / certificates /
  • / wp-includes / fonts /
  • / wp-includes / images /

Accordingly, for them we will make new rules in location and we will use nginx with  ngx_http_substitutions_filter_module .
It is not necessary to install this module, you can do it with rewrite alone, but by itself it is useful and through it you can improve a particular output from the backend.

In the config add:

	subs_filter_types text / html;
	subs_filter_types text / xml;

To filter the output of html and xml documents.


subs_filter 'https://example.com/wp-content/uploads/' 'https://static.example.com/uploads/' gi;
subs_filter 'https://example.com/wp-content/themes/' 'https://static.example.com/themes/' gi;
subs_filter 'https://example.com/wp-content/plugins/' 'https://static.example.com/plugins/' gi;
subs_filter 'https://example.com/wp-includes/js/' 'https://static.example.com/js/' gi;
subs_filter 'https://example.com/wp-includes/css/' 'https://static.example.com/css/' gi;
subs_filter 'https://example.com/wp-includes/certificates/' 'https://static.example.com/certificates/' gi;
subs_filter 'https://example.com/wp-includes/fonts/' 'https://static.example.com/fonts/' gi;
subs_filter 'https://example.com/wp-includes/images/' 'https://static.example.com/images/' gi;

Thus, the links in html and xml will be rewritten. Now it remains to make sure that the end user, knowing the original link, does not stop the server, but is directed to the CDN.

location ~ * ^ / wp-content / themes / (. * (? 
As a result, when requesting any php file, nothing will happen. But when requesting statics (all that is not php in the case of WP, which is logical), the user will be redirected to the server statics: The 
configuration of the nginx profile for the statics server will be discussed below , 
then you just need to create an account on cloudflare (or any other) cdn provider that you are going to use, register their DNS and enable caching of the static.example.com domain, without caching example .com, where wordpress works.

Configure Static Server

Since we transferred the return to the static server, we need to configure it correctly.

allow IPv4 server;
allow IPv6 server;
allow IP / subnet of CDN servers;
allow IP / subnet of CDN servers;
deny all;

It is required to allow access to the localhost, access to the server itself from an external IP (for example, which script) and to the provider's CDN servers. For example, CloudFlare subnets can be found here at this link. And, of course, block access to everyone else. Since if the CDN suddenly decides to put traffic on the line ... leave a free channel.

You also need to make the dummy directory as root for the entire static server.

root / path / to / site / dummy;

In order for requests that came to the static server to location / or = / and which do not match the location specified there go to the same dummy directory. This directory is written inside server {} .

Further location greeting:

location = / {
	default_type text / html;
	return 200 "c'est static, c'est simple: P";

This is the text that the user who requested the root will see. You can write anything, the main thing when using inside is " escape quotes as \" .

Then you should register location 's on statics:

location ~ * ^ / uploads /.* ( ? 
When prompted static.example.com/images/pic.png server will give the file in the directory / wp-includes / images / file pic.png , but when prompted static.example.com/ images / pic.php  location clicks and as a result the user will be given a file from dummy / images / pic.php, which is not there and as a result error 404. 
We also need to add rates for speed.

limit_rate_after 16m;
limit_rate 2m;

After 16 megabytes, the speed decreases to 2 MB per second per stream . This is so that the CDN does not clog the entire channel when caching a huge file.

In the case of cloudflare, the maximum file size (at the time of writing this material) is 512 megabytes , and the supported formats on the free tariff plan include : css, js, jpg, jpeg, gif, ico, png, bmp, pict, csv, doc, pdf , pls, ppt, tif, tiff, eps, ejs, swf, midi, mid, ttf, eot, woff, otf, svg, svgz, webp, docx, xlsx, xls, pptx, ps, class, jar.

Request Filtering

There are two cases at once:
  1. When downloading media files, they get a link like  example.com/?attachment_id=XX , where XX is the page id for this media file. Correspondingly sorting 1, 2, 3 ... the user can pump out all the content, and that part of it that is not intended for him;
  2. php is full of sores. Probably, this is not so much the architecture of the language as the skills of programmers and the settings of the environment into which this creation revolves. But once they put wordpress, we will be preparing for future bugs.

For this, we will write the code in server {} of our config for nginx:

if ($ args ~ * "(attachment_id | eval | duplicate | base64 | substring | preg_replace | create_function)") {
	return 403;

Then if attachment_id, eval, duplicate, base64, substring, preg_replace are encountered in the request arguments  , create_function nginx will return a 403 error, and the request will not be passed to the dynamics to execute a potential vulnerability.

Buns through nginx's subs_filter

The purpose of this module has been reviewed here .

Task: wordpress by default opens links to a media file in the current window. And you need to in the new.

Solution: add a little code to nginx kogfig.

subs_filter ' ' ' ' gi;
subs_filter ' ' ' ' gi;

After that, target = "_ blank" will be added to the links to media files using the frontend.

Task:  everywhere xmlrpc.php links ... need to be removed.

Solution: add a little code to nginx kogfig.

subs_filter 'https://example.com/xmlrpc.php' 'https://example.com/xmlrpc.txt' gi;

Well, in xmlrpc.txt you can stick an easter egg.


  • example.com as well as static.example.com replace with your servers. Examples will work not only with php5-fpm, but also with apache;
  • Only caching of statics in CDN is considered in the article, without caching of dynamics. The reason is very simple: on free tariffs, the cache update time is 30 minutes. That is, your page will be in the cache for half an hour. Although now you can switch to development mode every time you upgrade the site (instead of selectively resetting the cache each time);
  • В location указаны основные моменты. Я ориентируюсь на то, что читатель не ставит первый раз в жизни nginx и wordpress. Воспринимайте статью как hint к действиям и не более;
  • Описания регулярок можно найти тут и прочитать вот эту статью. Я использую negative look behind конструкцию при раздаче статики, то есть регулярка берет значение location, смотрит с конца в начало и если .php не обнаружено, то файл отдается. Если же в запросе обнаружен php, то location неверный, а так как все location исключают php, то будет выбрат root для dummy, в котором php файла нету (просто пустая папка) и который вернет 404;
  • CloudFlare .com is a wonderful CDN provider and already on the free plan you get a pretty functionality that is enough for almost any project.

Good luck to you!

Also popular now: