WordPress runs an absurd amount of websites nowadays. 64% of all CMS’s. 40% of all websites. Let that sink in! It also means it has a giant target on its back from malicious actors. This post will explore ways of strengthening a WordPress installation on Ubuntu 22.04 for your site or your customers. The WordPress Core itself (and WooCommerce to almost the same extent) has many eyeballs on them to find security issues and fix them. They are very robust platforms just by the sheer number of people interested in making it secure. Most likely, the problems arise from 2 sources:
- Plugin weaknesses
- Badly configured servers
Let’s review what we can do to mitigate the issues. Keep in mind; security is as good as your weakest link. You have a Fort Knox lock on your wooden door… it won’t help that the lock is unpickable!
Who is this guide for?
This article is intended for DevOps programmers looking for tips and tricks on how to get better WordPress security…hygiene for one server = one WordPress installation. There are lots of good guides, but also a lot of crappy guides out there. We’re going to assume a standard Ubuntu distribution with Nginx and PHP8.0+ for this tutorial.
We’ll explore the details of how to handle multiple installations of WordPress on the same server in the next article of this series! Subscribe to stay alerted!
Mitigating plugin related issues
A word on paid plugins
It’s impossible to ensure 100% of plugins are 100% secure. Furthermore, you have sketchy plugins that install backdoors on purpose; the site owner will never know. First is the source of the plugin. Don’t get your paid plugins from Nulled sites. Although it’s their right to do it because WordPress is licensed under GPL and GPL is “copyleft,” all derivative work (aka plugins) must have a compatible license. Therefore it’s legal for them to redistribute it.
Firstly, after auditing such “nulled” plugins, I found most of them contain some sort of backdoor/trojan that would leave a gaping hole in your site.
Secondly, you’re not encouraging the developers to continue their work. This is maybe the most important aspect of it. Most plugins are really cheap for the value you’ll get out of them. Custom development equivalent for a $150 plugin would often cost you easily 50x or 100x more (sometimes even more than that). Not only do you keep the light on for the developer who built something of value to you, but you also get updates and support, which is incredible value for the price. Pay for your plugins 🙂
Plugin attack vector mitigation
No code is immune to attacks, but what can we do about it to limit the problems if a malicious actor finds a breach? There are a few things we can do to help some attack profiles.
Defcon 5 – Make sure the file permissions are correct
99% of WordPress installations are installed under the “apache” or “www-data” user (www-data in Ubuntu, which we’ll use from now on), which is the user running Nginx and PHP. This means that by default, PHP is allowed to read but, more importantly, WRITE and, in some cases, EXECUTE files. Yikes. Perfect segway to tell you if you see a guide of any sort telling you to simply do on your WordPress installation:
$ sudo chmod -R 0777 /var/www/wordpress <--- DO NOT DO THIS!!!!!!!
Runaway and never trust that source again!
PHP should have ZERO rights to execute any files of any kind. If a plugin tells me it needs to execute permission, it should trigger massive red alarms.
Only directories should be executable (to open them). How to ensure this? Here you go:
$ sudo chmod -R a=r,u+w,a+X /path/to/your/wordpress/install
This line does the following:
- All users get READ only (a=r, 0444)
- We add WRITE permissions to the OWNER (u+w, 0644)
- We finally add EXECUTE TO DIRECTORIES ONLY for all users. That’s the UPPERCASE X, not to be mistaken with a lower case which affects everything! (a+X, dirs become 0755, files stay 0644)
Great! so now, no PHP files are executable. That’s one less worry to have.
Defcon 4 – Limit php functions
PHP functions have no business being allowed on a standard, run-of-the-mill, WordPress installation (or WooCommerce). They are instrumental in many situations, but for WP, we can most likely disallow them. If a plugin requests access to one of those, I would seriously consider alternatives that don’t – or you know exactly why they absolutely need it.
The big nono:
It SHOULD be disabled by default on your install. As of PHP 7.4, this is disabled by default AND deprecated – make sure it is:
$ php -i | grep allow_url_include allow_url_include => Off => Off
This allows your PHP script to include (as a PHP script!) a remote file. I’m sure there is no need to explain why this is complete madness to allow this! If it IS allowed, as your host, why and raise hell about it.
Sane PHP.ini to include
Create a file (as root) in the conf dir.
Note the 99 in front of the file name. This is important because it determines the order in which the files are loaded. The higher the number, the later it’s read. This ensures that any of the settings we want to set are NOT overridden by another file.
$ sudo vim /etc/php/8.0/fpm/conf.d/99-my-settings.ini
Add the following settings:
#Dangerous functions that should be disabled on a regular WP install disable_functions = exec,passthru,shell_exec,system,proc_open,popen,show_source,syslog,openlog #Don't expose PHP version, don't display errors, log them expose_php = Off display_errors = Off display_startup_errors = Off #Log everything except deprectate and strict on a production server. error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT log_errors = On # Custom PHP settings for WordPress and WooCommerce # Allow a maximum POST size of 25MB. post_max_size = 25M # Allow a maximum size file of 25MB upload_max_filesize = 25M # Allow up to 5 files to be uploaded in one request max_file_uploads = 5 #Make sure everything is in UTF-8 to avoid problems. default_charset = "UTF-8" #A PHP script can't consume more 256MB of ram... memory_limit = 256M #...for up to 30 seconds... max_execution_time = 30 # The worker will wait for 60 seconds to receive the complete request before closing. max_input_time = 60
Ok Great. This, with the default PHP settings, make for a pretty safe PHP runtime.
Defcon 3 – Install Fail2ban
While not strictly related to plugin mitigation, Fail2Ban is a great little firewall that monitors logs and blocks what you want when something you don’t like occurs. For example, let’s ban any IP from reaching the server if it fails 5 times to login into the admin of your WordPress installation. Let’s do that. We install it along with ufw and make sure the service is enabled (aka. survives restarts)
$ sudo apt install fail2ban ufw $ sudo systemctl enable fail2ban
fail2ban works with 2 distinct configuration files:
- What to look for in a log (filter)
- And what do to once we found what we were looking for (jail)
Let’s create those. first, the filter
# /etc/fail2ban/filter.d/wordpress.conf # We check for any POST request attempts on the login page or the XML RPC API entry point. Note: it would be even better to entirely disable xmlrpc if you don't use Jetpack and the likes. [Definition] failregex = ^<HOST> .* "POST .*wp-login.php ^<HOST> .* "POST .*xmlrpc.php ignoreregex =
Now let’s create the jail:
# /etc/fail2ban/jail.d/wordpress.conf [wordpress] enabled = true #explicit, isn't it? port = http,https #check both protocols filter = wordpress #use the filter we defined above logpath = /var/log/nginx/access.log #the log to apply the filter against maxretry = 10 #how many times we allow the filter to find a match.... findtime = 600 #...over the last 10 minutes (10x60seconds) ... banaction = ufw #...before we ban this IP with UFW.... UFW is much simpler than iptables, but as powerful (as it uses iptables underneath) ignoreip = #...unless it comes from this IP (aka: yours)
We’ll use more of Fail2ban in the next episode of that series.
Defcon 2 – Make your entire WordPress installation read-only for the webserver.
This is the pre-nuclear option, which we see below, but we PREVENT PHP from touching the code on the server.
Let’s get cracking; we’ve got a lot to do!
Step 1 – install wp-cli if you haven’t already.
$ sudo curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar $ sudo mv wp-cli.phar /usr/local/bin/wp
Make sure we keep wpcli updated with crontab. Since we installed it as ROOT, it must be root running the cron.
$ sudo crontab -e #Checking for cli updates at 4am every Sundays. 0 4 * * 7 /usr/local/bin/wp cli update
Step 2 – Create a user-specific for wp-cli.
We don’t want any random user account, however. We don’t need (or want) a home folder for that user, and we certainly don’t want anybody to be able to log in using it. However, we still need it to have shell access.
$ sudo adduser --system --no-create-home --group wpcli
Step 3 – Create a common group for PHP and WP-CLI
Here we’re creating a group called “web,” and we add it as an extra group to the user wpcli, then to user www-data. Note the uppercase G here, in -G. This means we want it as the extra group, not the main group, which would be lower case -g.
$ sudo groupadd web $ sudo usermod -a -G web wpcli $ sudo usermod -a -G web www-data
Step 4 – Change ownership of the WordPress Installation
Assuming your WordPress installation is /var/www/html/wordpress
$ sudo chown wpcli:wpcli -R /var/www/html/wordpress
Now that’s all nice and good, but uploads won’t work this way since the folder belongs to wpcli now.
$ sudo chown wpcli:web -R /var/www/html/wordpress/wp-content/uploads $ sudo chmod g+rws -R /var/www/html/wordpress/wp-content/uploads
We need to put the group of the upload folder to the shared group we created in step 3. The astute reader will notice this unusual “s” flag on the permission, which does show now on the folders:
$ ls -al /var/www/html/wordpress/wp-content drwxrwsr-x 14 wpcli web 4096 Feb 22 20:59 uploads
The S flag is a setgid flag. It’s used to manipulate the umask on a specific folder. In effect, it makes sure that any files created by either wpcli OR www-data are under their secondary group “web” instead of their own primary group. Otherwise, without it, the default permissions would make files created by wpcli untouchable by www-data in that folder and vice versa.
While this setup suffices for the vast majority of plugins, some plugins like object cache and web cache plugins need to write files OUTSIDE the upload folder – generally in the wp-content folder. In these situations, I recommend manually creating the file using `touch` and give them permissions of wpcli:web. This is a great way to see if you have plugins that want to write outside what’s allowed to them.
Step 5 – Use WPCLI for routine maintenance
Now you can use wpcli for your maintenance!
$ cd /var/www/html/wordpress # To update wordpress: $ sudo -u wpcli wp core update # To update all the plugins: $ sudo -u wpcli wp plugin update --all
You can even automate minor WordPress updates (or major also if you want!) this with a cronjob, under the wpcli user, of course:
$ sudo -u wpcli crontab -e 0 */2 * * * wp core update --minor --path=/var/www/html/wordpress 0 3 * * 1 wp plugin update --all --path=/var/www/html/wordpress
This will check every two hours for a WordPress update, and every Monday at 3 am for plugin updates. Adjust the timing as needed!
This will ensure you keep a tight leash on your WordPress installation. Plugins have nowhere to write except their intended upload folder. They cannot execute more risky PHP functions, and they can’t modify themselves either. Security is never a perfect solution, and determined attackers are always able to find a way. You’re just limiting your exposure to more blatant exploits that are in the wild using those techniques.
In the next episode of this series, we’ll go closer to the metal on how to configure your web server as well.
Defcon 1 – Use Ymir
Ymir is built by Carl Alexander; it runs WordPress serverless on AWS. It means that every time a request hits your site, an image of your site is being loaded and served by AWS Lambda. In effect, it means that even if an attacker was able to modify code, it would not affect anyone because the source is immutable. That’s one of the huge advantages over traditional web servers! Not that it doesn’t protect against SQL injection, of course, so vulnerable plugins there will still be in Ymir, but you significantly lessen the surface of attack.
Woogo Stores is proud to host all its customers on the Ymir platform and benefits from AWS serverless environment!
If you have any questions about it, contact Carl on Twitter. He’ll be more than happy to chat with you!