The moment you spin up a server on the Internet it's going to be an immediate target for a wide range of automated attacks by internet bots. It's such a problem that Cloudflare has recently released its Super Bot Fightmode to counter the threat.
This tutorial uses Cloudflare to protect a Ghost blog from these attacks whilst enabling Zero Trust access to the Ghost admin interface via an email based PIN authentication mechanism.
For an overview of why we're using Cloudflare Tunnels to protect our Blog and VPS checkout my summary post!
Solution Architecture Overview
Just to give you a bit of context of what we're building from a high level before we jump in to the details, were going to:
- Setup up an Outbound only encrypted tunnel to Cloudflare's Edge using a Cloudflared Service on a VPS to route traffic locally to a Ghost Blog running locally on a Docker container.
- Setup a Ghost Blog on Docker using Docker-Compose.
- Setup a Service Provider level firewall to block all IpV4 and IpV6 Incoming requests so that all traffic has to route through Cloudflare's extensive edge network and related performance and security infrastructure before it hit's our origin (Our VPS).
- Setup Cloudflare Teams with its Zero Trust Policies to protect the admin panel of the ghost blog at https://alexgallacher.com/ghost.
Before you begin
Setup the Cloudflared Outbound Tunnel:
Install Cloudflared from Cloudflare's Repository
You can utilise Docker to deploy Cloudflared however on this occasion I've opted to just use Cloudflare's repo and directly install it on the VPS. For those interested in using Docker compose some people have had success with setting this up via the Cloudflare forum.
I'm using Ubuntu 20.04 LTS so I'm going to use 'focal' in the repo add command, in order to check your version you can use the command:
Adding the Cloudflare Repository
At the time of writing the current versions supported are located under Cloudflare's package repo for Cloudflared
Let's go ahead and add the repository for our specific codename release of ubuntu:
echo 'deb http://pkg.cloudflare.com/ focal main' | sudo tee /etc/apt/sources.list.d/cloudflare-main.list
Import the GPG Key:
curl -C - https://pkg.cloudflare.com/pubkey.gpg | sudo apt-key add -
Update the apt Cache:
sudo apt-get update
Install Cloudflared from the repository:
sudo apt-get install cloudflared
Authenticating the created Tunnel with Cloudflare
Once Cloudflared is installed we can go ahead and login for it to generate the account certificate:
cloudflared tunnel login
Once you have executed the command, you'll be presented with a URL to follow to login to your Cloudflare account. The certificate for your VPS will automatically download to your server once you've authenticated. You'll need to go ahead and select the domain that you'll be hosting your blog on, in my case this is alexgallacher.com. Click authorise when prompted and the certificate will be automatically deployed to your VPS.
Now that we've got the certificate deployed to the server we need to create a Cloudflare tunnel with the command:
cloudflared tunnel create <tunnel-name>
The name of the tunnel, in my case is 'devon', this name can be unique and is just used to identify the tunnel in the future along with the UUID of the tunnel.
- It's important to note we haven't yet spun up the tunnel from the VPS to Cloudflare's edge as we haven't setup a configuration file for Cloudflared to understand what traffic we're looking to route back to our Ghost blog running on the VPS.
To confirm the tunnel has been created you can use:
cloudflared tunnel list
Setup and Run Cloudflared as a Service
Before we make the configuration file let's setup Cloudflared as a service so it'll automatically start whenever we reboot our VPS:
Let's first install the service:
cloudflared service install
To ensure the service launches every time the VPS is restarted lets instruct the VPS to enable the service:
systemctl enable cloudflared
Before we boot up our tunnel for the first time, let's configure out traffic pattern routing for Ghost - let's navigate to the cloudflared directory and setup a new config.yml file:
Inside the new config.yml file that you're creating, let's define a few things:
tunnel: devon credentials-file: /home/<username>/.cloudflared/<Certificate_UUID>.json ingress: - hostname: alexgallacher.com service: http://localhost:2368 - service: http_status:404
- tunnel - This is the name of the tunnel you created in the steps above.
- credentials-file - This is the certificate that cloudflared automatically downloaded on to your VPS when you authorised the tunnel and logged in. The certificate ID and the user path (home/<username> will be unique to your VPS.
- ingress / hostname / service - This is us defining the hostname (i.e. the website address) and which locally running service we're pushing traffic to. In my case Ghost is running locally on port 2368 so any visitors through Cloudflare to alexgallacher.com should be forwarded to my local service on the VPS to port 2368.
- The final http_status:404 service - this is designed as a catch all. If no traffic patterns match we'll push the visitor to a 404 page 😋.
Before we go any further with the tunnel to Cloudflare we need to setup our Ghost blog! 👇
Setup Ghost Blog using Docker and Docker Compose:
The easiest way to do this is to follow my quick guide on how to setup a ghost blog on Docker with Docker Compose in my docker series. As we're not using an NGINX proxy to connect the Docker container to the web it's important to take note of the information below:
1) Under the .env file - Remove or exclude the following lines:
#### NGINX Config ####
VIRTUAL_HOST=<Your Domain | alexgallacher.com>
VIRTUAL_PORT=<Port your running ghost on through | 2368>
LETSENCRYPT_HOST=<Your Domain | alexgallacher.com>
2) Don't follow the section which guides you on updating your DNS records as there is a different process.
Setup Cloudflare DNS records to point to our Tunnel
The next step is to instruct Cloudflare to tunnel any traffic to the website. In my case any requests to my website domain which is https://alexgallacher.com should be routed into the Cloudflare Tunnel to the Cloudflared service which will in turn be picked up and routed internally on the VPS to our Ghost Blog service running on port 2368 based on the configuration file we defined above.
To begin with, Navigate to your DNS settings on Cloudflare and a CNAME record:
As a reminder - to find the Unique ID of your tunnel which is the same as the certificate downloaded to your VPS run the following command again:
cloudflared tunnel list
Add a CNAME record pointing to your website domain and target the Unique ID of the tunnel you created earlier. If you are using a root domain, like I am in my case, you can simply add @ to the Name and it will use the full domain name to map to the Target.
Start the Cloudflare Service
Let's go ahead and start the Cloudflare Service and ensure it connects. As we run this command, Cloudflared will look for the closest edge networks from Cloudflare and make 4 direct tunnel connections to start passing traffic.
systemctl start cloudflared
If you execute the below command you should expect to see 4x<Location> connections. As you can see on my VPS I have 4 connections to the IAD datacenter in the US.
At this stage, presuming you've setup everything correctly, you should be able to browse to your website address and it will load like any other website.
Block all incoming Ipv4 and Ipv6 traffic to the VPS
So we've got the basics sorted, we've setup Cloudflare to automatically to route traffic to the encrypted tunnel and we've setup the Cloudflared service to push this traffic internally on our VPS. At this stage traffic to the blog will be routing through Cloudflare's Edge but we're still accessible via our IPv4 on the net which means we're still vulnerable to those pesky 🤖!
Let's fix that and improve our security posture in the process by blocking all incoming Ipv4 and Ipv6 traffic on our Host's hardware level firewall so that all traffic reaching our VPS has to go through Cloudflare!
Navigate to your Service Provider's Firewall settings - in my case this is Vultr's Firewall option under Products:
Let's go ahead and add a Firewall called 'Block All Incoming Traffic'
Go ahead and for the time being accept SSH on port 22 to your own source IP and add an additional rule that will drop any traffic on any port from any source. You'll need to repeat this activity on the Ipv6 rules page.
Once you've added the rulesets, go ahead and link this firewall ruleset to your VPS instance to make it active.
Protect Ghost Admin Login with Cloudflare Teams
Currently you should still be able to login to the ghost admin dashboard via the use of /ghost on your own domain - in my case this is https://alexgallacher.com/ghost.
Currently, Ghost doesn't have 2 factor authentication on this dashboard so let's go ahead and use Cloudflare teams to protect this specific URL with Zero trust rules so I have to enter a valid email to receive a one time PIN via my email prior to be being able to view the login page for Ghost.
Let's go ahead and browse to Cloudflare Teams - this can be reached via dash.teams.cloudflare.com and let's add an application under the 'Access' tab.
On the next page there are a few options to pick from. As we've self hosted this application through a Cloudflare Tunnel lets select 'Self-Hosted'.
On the Application Configuration page, go ahead and add some application settings:
Just to dig a little deeper on what we're defining here:
- Application Name - This can be named anything to remind you of what you're setting up, in my case i've called this the 'Ghost Login Page'
- Session Duration - Once you've got this working you can play around with this but I typically keep this to 24 hours.
- Application domain first input box - There are three input boxes here. As we're not using any sub domains i'm going to leave the subdomain box empty. If you're running your blog on 'blog.alexgallacher.com' for example, you would enter 'blog' here.
- Application domain second input box - This is a drop down selection, simply select the domain you're hosting your blog on.
- Application domain third input box - This is where you need to specifically specify 'ghost' or the page you're looking to protect on your application. If we fail to input a specific URL by defining 'ghost' we'll be applying the functionality across the entire website which means no one will be able to access anything on our website!
- Identity providers - For now we're setting up the Identity Provider at the bottom to be a One Time PIN - should you want to get crafty here you can use an Identity Provider like Okta.
Once finished go ahead and click next to the policy screen. On the policy Screen, let's go ahead and add a policy:
Just to dig a little deeper on what we're defining here:
- Policy Name: This again can be named anything to remind you of what policy you're setting up, in my case i've called this the 'Ghost Admin Page Access Policy'
- Rule action - Allow, as we want to allow access to the page on the condition being met
- Session duration - I leave this as 'Same as application session timeout' to control this from the application level.
- Configure rules - Include - Add include emails and enter your own email that you wish to receive the one time pin (OTP) to.
Once done, click next and move on to the next page. I'm going to leave the CORS, Cookie setting empty for now but i'd strongly advise settings these once you get the functionality up and working. Click add application to close the process.
Once done you should have a new application added under the type 'SELF-HOSTED' with the appropriate /ghost url appended to your domain.
Let's go ahead and test it out. Let's browse to https://alexgallacher.com/ghost and see what we get!
I'm presented with an email input field by Cloudflare to send me a OTP before I can access the page sitting behind.
After entering my email (Which is validated in our policy rule on Cloudflare as being authorised to receive OTP's) I get an email from Cloudflare:
If you click the link you'll be authenticated into the protected page for a period of 24 hours as defined in our policy.
That's all there is to it, the specific URL is now protected by Cloudflare's teams product!
Bit of a hefty article this one as it covers multiple components to get to the final solution, but let us know your thoughts and how you got on with it!