Setting up a secure WordPress website can be quite hard with all the scattered information out there on the web.
This article sums up the best methods to properly harden your WordPress website on Amazon Lightsail in combination with Cloudflare.
To defend against 99% of the most common but most impactful threats you need to restrict firewall access on port 22 (ssh). Set up end-to-end encryption using SSL and block everyone from accessing your wp-admin dashboard with a Cloudflare firewall rule.
Down below we dive deeper into the details and explain how to properly harden your WordPress website.
Table of Contents
Prerequisites
For this guide, we’re going to deploy a WordPress instance using Amazon Lightsail. Lightsail is ideal for simpler workloads, quick deployments, and getting started on AWS. It’s designed to help you start small, and then scale as you grow.
So before we begin you’ll need the following:
- Domain name
- An AWS account
- A Cloudflare account
Deploying WordPress on Amazon Lightsail
In the console, you press “create an instance”. In the next window, you get to select the platform and blueprint.
The platform is either Linux or Windows, for our use case, we select Linux. Then we select WordPress as the type of Operating System + Application.
This image is a pre-configured, ready-to-run image for running WordPress on Amazon Lightsail supplied by Bitnami. You get to pick the region to which you want to deploy your instance. Choose a region that is the closest to your audience/readers.
In the next step, we want to choose our instance type (basically the horsepower of the machine).
When you create the instance, it starts to boot up with the WordPress image. You will see that it has an external IP address. But it’s only a dynamic address which means you will lose it once you stop/start the machine.
This can become a problem if you point your DNS’s A record to the IP address and you decide to stop/start the server. The A record will still point to the old dynamic IP address. To fix this, you should assign a static IP address in the networking tab.
Creating a static IP is free of charge when it’s attached to the instance. When you don’t attach it, AWS starts charging you for reserving the IP address.
Before we’re going to optimize the WordPress configuration on the server, we’re going to update the Lightsail instance’s firewall to protect it from outsiders.
By default Port 22/80/443 is allowed for everyone, which is something that’s not considered best practice for security measures!
You should restrict access to port 22 (SSH) for your IP address. Getting your own IP can be fetched using this simple trick in your terminal:
curl ipinfo.io/ip
AWS also supplies terminal access through the browser console, if you’d rather use that, then you can disable port 22 on the firewall.
Optimizing and securing WordPress
We can easily login through the connect tab within the Amazon Lightsail console.
The first you want to do is to obtain the WordPress credentials from the server so you can log in on the wp-admin interface:
cat bitnami_credentials
The command displayed above is also supplied in the Bitnami documentation. The documentation contains lots of information on what has been configured and how you can tweak more settings, so it’s highly recommended that you’d read that.
We want to focus on end-to-end encryption. This requires the WordPress server to have a trusted CA certificate installed.
To enable this we need to install a Let’s Encrypt certificate on this server. Luckily for us, Bitnami supplied us with a tool to generate the certificate from Let’s Encrypt automatically and also automatically renews it for us.
We need to run it as follows:
sudo /opt/bitnami/bncert-tool
You need to follow the wizard in order to generate your Let’s Encrypt certificate. Supply you DNS e.g. example.com
and choose to redirect www to non-www, and redirect HTTP to HTTPS. The details below show the configuration process.
----------------------------------------------------------------------------
Welcome to the Bitnami HTTPS Configuration tool.
----------------------------------------------------------------------------
Domains
Please provide a valid space-separated list of domains for which you wish to
configure your web server.
Domain list []: towardsthecloud.com
The following domains were not included: www.towardsthecloud.com. Do you want to add them? [Y/n]: Y
----------------------------------------------------------------------------
Enable/disable redirections
Please select the redirections you wish to enable or disable on your Bitnami
installation.
Enable HTTP to HTTPS redirection [Y/n]: Y
Enable non-www to www redirection [Y/n]: n
Enable www to non-www redirection [y/N]: y
----------------------------------------------------------------------------
Changes to perform
The following changes will be performed to your Bitnami installation:
1. Stop web server
2. Configure web server to use a free Let's Encrypt certificate for the domains:
towardsthecloud.com www.towardsthecloud.com
3. Configure a cron job to automatically renew the certificate each month
4. Configure web server name to: towardsthecloud.com
5. Enable HTTP to HTTPS redirection (example: redirect
https://towardsthecloud.com to https://towardsthecloud.com)
6. Enable www to non-www redirection (example: redirect www.towardsthecloud.com
to towardsthecloud.com)
7. Start web server once all changes have been performed
Do you agree to these changes? [Y/n]: Y
The config gets saved in the following files /opt/bitnami/apache/conf/vhosts/wordpress-vhost.conf
& /opt/bitnami/apache/conf/bitnami/bitnami.conf
in which we need to make one adjustment in the following block:
# BEGIN: Enable HTTP to HTTPS redirection
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteCond %{HTTP_HOST} !^localhost
RewriteCond %{HTTP_HOST} !^[0-9]+.[0-9]+.[0-9]+.[0-9]+(:[0-9]+)?$
RewriteCond %{REQUEST_URI} !^/\.well-known
#RewriteRule ^/(.*) https://%{SERVER_NAME}/$1 [R,L]
RewriteRule ^/(.*) https://%{SERVER_NAME}/$1 [R=permanent,L]
# END: Enable HTTP to HTTPS redirection
We commented out the original rule RewriteRule ^/(.*) https://%{SERVER_NAME}/$1 [R,L]
and added the rewrite rule below which contains our domain without www and set up a permanent redirect by changing [R,l]
to [R=permanent,L]
.
Now we established an efficient redirect that redirects your request in a single hop to your non-www domain.
Then we need to update our WordPress address and Site address in wp-config:
vim /home/bitnami/stack/wordpress/wp-config.php
Update the following two lines:
define('WP_SITEURL', 'http://' . $_SERVER['HTTP_HOST'] . '/');
define('WP_HOME', 'http://' . $_SERVER['HTTP_HOST'] . '/');
to:
define('WP_SITEURL', 'https://example.com');
define('WP_HOME', 'https://example.com');
Save your changes and restart apache:
sudo /opt/bitnami/ctlscript.sh restart apache
We completed the SSL security for your domain successfully for this WordPress website. The next step is to set up the DNS correctly using Cloudflare.
Cloudflare end-to-end encryption
We’re going to manage our DNS with Cloudflare, it’s free to use and has a lot of performance benefits i.e. caching and security. We dive deeper into the security aspect and set up the best security settings to harden a WordPress website.
In the Cloudflare interface, we have the SSL/TLS tab and the only thing we need to do is; enable Fullencryption mode.
If the apache settings were set up correctly you should now have end-to-end encryption (Cloudflare -> WordPress Lightsail instance) enabled.
Note: Please make sure to disable Cloudflare’s Always Use HTTPS
feature.
Otherwise, Cloudflare will redirect your HTTP to HTTPS which is inefficient if you redirect from www to non-www.
For example, if you go to http://www.example.com
Cloudflare will redirect to https://www.example.com
and then redirect to https://example.com
.
It will redirect twice which is inefficient, so we let our apache server handle the redirection and do it in a single hop.
Setting up security headers
Another important aspect to highlight is the security headers. By default your website is missing a couple of important security headers as you can see here:
The following security headers are missing:
In order to fix the first 3 security headers, we need to update the .htaccess file.
Note: The .htaccess
file is disabled by default on the WordPress Bitnami image for Amazon Lightsail. To enable it, change the following in /opt/bitnami/apache2/conf/vhosts/wordpress-vhost.conf
and /opt/bitnami/apache2/conf/vhosts/wordpress-https-vhost.conf
:
AllowOverride None
to:
AllowOverride All
and then you have to move the contents of the .htaccess file to this config file (/opt/bitnami/apache2/conf/vhosts/htaccess/wordpress-htaccess.conf
), otherwise, apache won’t read it.
<Directory "/opt/bitnami/wordpress">
# Extra Security Headers
<IfModule mod_headers.c>
Header set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
Header set Permissions-Policy "camera=(), microphone=(), geolocation=(), interest-cohort=()"
Header set X-Content-Type-Options nosniff
Header set X-DNS-Prefetch-Control "on"
Header set X-Frame-Options "SAMEORIGIN"
Header set X-XSS-Protection "1; mode=block"
Header set Referrer-Policy "same-origin"
Header set Content-Security-Policy "block-all-mixed-content"
</IfModule>
# END WordPress
</Directory>
After making the change, restart apache: sudo /opt/bitnami/ctlscript.sh restart apache
The next feature that we want to enable is HTTP Strict Transport Security (HSTS)
.
HTTP Strict Transport Security (HSTS) is a header that allows a website to specify and enforce security policy in client web browsers. This policy enforcement protects secure websites from downgrade attacks, SSL stripping, and cookie hijacking.
You can find more information here.
On this site, we can check if HSTS for our domain is enabled. Then we can submit the form and include our domain on the preload list.
Updating the Content-Security-Policy
(CSP) security header is a specific setting that’s custom for your use case. Unfortunately, this can’t be solved with a default rule.
Because this header tells which dynamic sources are allowed for the user. If you need more information on setting the right CSP for your situation please check this source.
If you’ve followed the steps correctly, this should be your end result:
Cloudflare firewall rules
Cloudflare Firewall Rules is a firewall offered by Cloudflare and is a powerful and flexible security tool to filter web application traffic. We can set up 5 firewall rules for free.
A Firewall Rule consists of two parts: Matching and Action.
- Matching: A defined filter that runs and matches your traffic for a string or pattern
- Action: the action performed on the matched traffic (block, challenge, captcha, allow)
You can also order the firewall rules to override the default sequence which is based on the rule’s action.
We want to set up the following 4 firewall rules:
- Allow the owner to access wp-admin
- Block everyone else from accessing wp-admin
- Block bad bots
- Block No-Referer requests to plugins
I’ll be sharing the expression rules, so you can easily copy/paste them into your own firewall rule.
1. Allow the owner to access wp-admin
You can use the following expression:
(lower(http.request.uri.path) contains "/wp-admin" and ip.src in {x.x.x.x}) or (lower(http.request.uri.path) contains "/wp-login.php" and ip.src in {x.x.x.x})
Replace x.x.x.x
with your own IP address. To explain the expression rule we just described: when your request URI contains /wp-admin
or /wp-login
with your own IP address, you will be allowed access.
2. Block everyone else from accessing wp-admin
In the first rule, we allowed access for you, now we want to block the outside world from trying to access your admin interface.
You wouldn’t normally think about it but there are a lot of bots out there that are trying to break into your site.
You can use the following expression to guard against that:
((http.request.uri.path contains "/xmlrpc.php") or (lower(http.request.uri.path) contains "/wp-login.php") or (lower(http.request.uri.path) contains "/wp-admin/" and not http.request.uri.path contains "/wp-admin/admin-ajax.php" and not http.request.uri.path contains " /wp-admin/theme-editor.php"))
Once the request URI is matched with the above private locations it will get blocked, except for the allow rule which we defined as rule number 1. The rule order is important because if we switched the ordering, it would actually block our allow rule before allowing us.
3. Block bad bots
If you go to the settings page of your firewall, there is a feature called Bot Fight Mode
. This will block or slow down non-legitimate bots by challenging them with compute-intensive challenges.
4. Block No-Referrer requests to plugins
Plugins are built by the community and can sometimes be insecure if it’s badly programmed or not maintained/patched well enough. So we want to protect it with the following firewall rule.
(http.request.uri.path contains "/wp-content/plugins/" and not http.referer contains "example.com" and not cf.client.bot)
We block everyone’s access to the plugin directory except for your domain and known bots (e.g. google crawler).
WordPress is hardened now!
We deployed a WordPress instance using Amazon Lightsail, we closed unnecessary ports for the outside world.
We’ve set up a Let’s Encrypt certificate on the server which gets updated automatically and configured Apache to use the new SSL certificate we generated.
On the DNS side, we used Cloudflare to enable Full encryption mode which makes sure we have end-to-end encryption from our browser to our Amazon Lightsail instance.
At last, we set up 4 (free) firewall rules to block access from the bad guys.