How to configure Cloudflare Tunnels for a secure Ghost blog

Use Cloudflared Tunnels and Cloudflare Teams to protect a self hosted Ghost Blog or any application on the web running on your own server from bad bots on the internet.

ยท 11 min read ยท
How to configure Cloudflare Tunnels for a secure Ghost blog


UPDATE 15/6/22: Since writing this tutorial, Cloudflare has released an update where Cloudflare Tunnels can be configured directly through the web UI - more info here

Update 25/6/22: A viewer pointed out that if you're leveraging the Admin API then using Zero Trust Access to protect the admin endpoint it will cause access issues. 

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 Fight mode 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 check out my summary post!

Solution Architecture Overview

Overview Architecture Diagram for using Cloudflare Tunnel with Ghost Blog
Solution Overview for using Cloudflare tunnel and Cloudflare Teams to Protect a self-hosted Ghost blog

Just to give you a bit of context of what we're building from a high level before we jump into the details, we're 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.
  • Set up a Ghost Blog on Docker using Docker-Compose.
  • Set up 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 hits our origin (Our VPS).
  • Set up Cloudflare Teams with its Zero Trust Policies to protect the admin panel of the ghost blog at 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, to check the version you can use the command:

lsb_release -a
Release version with Codename for the server

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 focal main' |
sudo tee /etc/apt/sources.list.d/cloudflare-main.list

Import the GPG Key:

curl -C - | 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 log in 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 log in 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 Click authorise when prompted and the certificate will be automatically deployed to your VPS.

Cloudflare page for Authorisation of an Argo Tunnel
Selection of the domain that we'll be hosting the Ghost blog on. In my case this is

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, '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 set up 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
Terminal showing listed Cloudflared tunnels
Cloudflared shows the list of created tunnels on my own VPS.

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 let's instruct the VPS to enable the service:

systemctl enable cloudflared 

Before we boot up our tunnel for the first time, let's configure our traffic pattern routing for Ghost - let's navigate to the cloudflared directory and set up a new config.yml file:

cd /etc/cloudflared/ 
nano config.yml

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

  - hostname:
    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 onto 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 should be forwarded to my local service on the VPS to port 2368.
Important to note all services we're passing traffic to inside the VPS should be HTTP as opposed to HTTPS - e.g. 'http://localhost:2368'
  • The final http_status:404 service - 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 set up 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 set up 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:

As the linked guide is built for an NGINX proxy as opposed to Cloudflared's tunnel you'll need to exclude two components of the setup:

1) Under the .env file - Remove or exclude the following lines:
#### NGINX Config ####
VIRTUAL_HOST=<Your Domain |>
VIRTUAL_PORT=<Port your running ghost on through | 2368>

2) Don't follow the section which guides you on updating your DNS records as there is a different process.
People have been struggling with setting up Ghost on Docker specifically for Cloudflare. I've included a clean version of my docker-compose file below without a .env file.

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 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
Terminal showing listed Cloudflared tunnels
Cloudflared shows the list of created tunnels on my own VPS.

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.

Cloudflare page defining DNS records for Argo Tunnel for Cloudflared
Setup Cloudflare DNS to Route traffic from your website domain to the Cloudflare tunnel Unique ID

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 data centre in the US.

Terminal showing listed Cloudflared tunnels
Cloudflared shows the list of created tunnels on my own VPS.

At this stage, presuming you've set up 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 set up Cloudflare to automatically route traffic to the encrypted tunnel and we've set up the Cloudflared service to push this traffic internally on our VPS. At this stage traffic to the blog will be routed 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:

Vultr Firewall Group Rules
Navigate to Vultr's Firewall Group Settings

Let's go ahead and add a Firewall called 'Block All Incoming Traffic'.

Vultr Firewall Group Name
Add a name of your choosing to the Firewall rule.

Go ahead and for the time being accept SSH on port 22 to your 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.

Vultr Firewall Dashboard
Adding Firewall rules to Vultr's Dashboard

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 log in to the ghost admin dashboard via the use of /ghost on your own domain - in my case this is

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 I have to enter a valid email to receive a one-time PIN via my email before being able to view the login page for Ghost.

Let's go ahead and browse to Cloudflare Teams - this can be reached via and let's add an application under the 'Access' tab.

Cloudflare Teams Adding an Application
Cloudflare Teams - Adding an Application

On the next page, there are a few options to pick from. As we've self-hosted this application through a Cloudflare Tunnel let's select 'Self-Hosted'.

Selecting the type of Application on Cloudflare Teams
Select Self-hosted on the application type page.

On the Application Configuration page, go ahead and add some application settings:

Configuring app on Cloudflare Teams
Add the Application Name, Session Duration and Application Domain on the Configuration App

Just to dig a little deeper into 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 subdomains I'm going to leave the subdomain box empty. If you're running your blog on '' 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:

Configure policies for Cloudflare Teams Application
Add the Policy Name, Rule action, Session and appropriate rules

Just to dig a little deeper into 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 the 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, and Cookie setting empty for now but I'd strongly advise settings these once you get the functionality up and working. Click add an 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.

Cloudflare Teams Application List
The Ghost Blog Application should be listed with the correct Application URL and Application Type

Let's go ahead and test it out. Let's browse to and see what we get!

I'm presented with an email input field by Cloudflare to send me an OTP before I can access the page sitting behind.

Cloudflare For Teams Access Control Page Presented
As expected when I Browse to the specified URL I'm prompted to enter an email to retrieve a login code

After entering my email (Which is validated in our policy rule on Cloudflare as being authorised to receive OTPs) I get an email from Cloudflare:

Cloudflare OTP email to the approved email address

If you click the link you'll be authenticated into the protected page for 24 hours as defined in our policy.

Alex Gallacher Ghost page
The Protected page is shown once the PIN is Authenticated

That's all there is to it, the specific URL is now protected by Cloudflare's team's 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!