Frequently Asked Question
Building Password Purgatory with Cloudflare Pages and Workers
10 MARCH 2022
I have lots of little ideas for various pet projects, most of which go nowhere (Have I Been Pwned being the exception), so I'm always looking for the fastest, cheapest way to get up and running. Last month as part of my blog post on How Everything We're Told About Website Identity Assurance is Wrong, I spun up a Cloudflare Pages website for the first time and hosted digicert-secured.com there (the page has a seal on it so you know you can trust it). Instantly, I fell in love with this method of building websites so when I came up with an idea just yesterday, I knew exactly how I wanted to build it.
Here's the idea: I've been pondering for some time how to deal with spam like this:
Somehow, this made it all the way through the Microsoft 365 spam filters, landed in my inbox and consumed some of my precious, limited, highly-valued time. I, in turn, have been considering how to not only consume the time of these spammers, but make it entertaining for all. Which led me to a moment of clarity just yesterday as I was pondering revenge tactics and, in a flash of inspiration, came up with the idea of Password Purgatory:
purgatory: a place or state of temporary suffering or misery
You know how we all hate password complexity criteria? The kind that asks for uppercase characters, numbers, but only limited special characters and so on and so forth? That's what I'm now referring to as Password Purgatory - that temporary state of misery - and that's what we're going to do to the spammers ????
The end product will be a page on troyhunt.com where the Michelles of the world will be directed to pitch their content. All they have to do first is create a password... The idea of the Password Purgatory service is that it's an API designed to take a password, find something wrong with it and send that back in the response. It'll start out gentle (for example, minimum length) and get increasingly bizarre. A separate service will log each attempt the spammer makes to satisfy the inane criteria and once they've finally given up in agony (fingers crossed), I'll share the results publicly. Naturally I'll ensure there's no PII involved and given folks like Michelle write about cybersecurity, it won't be a password they've reused anywhere else, so no problems there (????).
To really get into the community spirit (let's face it, we all bond together when screwing with spammers), I wanted to make it all open source, take PRs and make the API accessible to anyone from anywhere. My hope is that we build something awesome together and collectively make the lives of spammers just that little bit more miserable.
So, here's the whole thing, end to end and step by step.
Creating a GitHub Repo for Pages
Everything is going to deploy out of GitHub so I've spun up a public repo called "password-purgatory" under my personal account:
Clone it locally, drop in an index.html file with a logo and push it back up. This is now effectively a working website (ok, a really basic one) sitting in source control. This will be a standalone website for people that just want to play with the API I build later on (this isn't where I'll be sending Michelle). I'll also add API documentation there in due course.
Let's get it deployed!
Setting up Cloudflare Pages
Super easy stuff here, just hit the "Pages" link then "Create a project":
The first time I used Cloudflare Pages, I had to authorise it to access the GitHub repository by virtue of the Cloudflare App. Jumping back in there now, that authorisation is still in place but it's the only repo I can see:
No problem, back over to the Cloudflare Pages app on GitHub and add access to the new repo:
I could just give it access to "All repositories", but I like the idea of the principle of least privilege in terms of reducing risk so I'm sticking with "Only select repositories". Back to creating the new Cloudflare Pages site and the repo is now visible:
Time to set up builds and deployments and I'm just going to stick with all the defaults:
Maybe you don't want to deploy from main, maybe you want to choose a framework and customise the build command, whatever. For now, I just want to push a single page website so it's straight to "Save and Deploy". Thinking, thinking, 4 seconds later... done!
The live site is now addressable at password-purgatory.pages.dev:
Binding the Domain
The dev URL would normally be fine for dev purposes, but I'm live-tweeting this as I go and I want the whole thing up and working on the official domain. After the initial build above, I'm over on the project page and now into "Custom domains":
I'd already registered passwordpurgatory.com with DNSimple so the domain exists already, let's drop it in:
Finding the domain already registered, Cloudflare now needs me to update DNS to point over to their nameservers:
This is the one part of the process that feels a bit rough TBH; the "Add Site" page pops in a new tab and we're now embarking on the "classic" path to add a site to Cloudflare:
Free is good ????
Often when you're moving a domain over to Cloudflare there'll be existing DNS records. When that's the case, they're listed in the screen below but as this is a brand new domain that's presently doing absolutely nothing, we'll ignore that and just continue (we'll add DNS records later when the domain is bound to the Cloudflare Pages resource):
Nameserver time! Cloudflare has identified the existing 4 nameservers at DNSimple and given me 2 of their own to replace these with:
How you update nameservers will depend on who your domain registrar is, but it's all pretty much the same process and it looks like this on DNSimple:
And that's it. Well, other than waiting for DNS magic to do its thing and propagate, let's head back to Cloudflare and hit that "Done, check nameservers" button. (Whilst writing that sentence, email confirmation comes through that the domain is now active.) I won't screen cap everything here, the tl;dr is that I turned on all the security things, all the auto minification things, completed the wizard then repeated the "Add a custom domain" process from earlier on. With the domain now added to Cloudflare, they're able to add the appropriate CNAME and activate it against the Pages resource:
Just pausing here for a moment, one of the really cool things about Cloudflare pages compared to more traditional hosting models is that I have no idea where it's hosted! Well, kinda - it's hosted on the hundreds of Cloudflare's edge nodes spread around the world, the point is it's not deployed to, say, the West US data centre like HIBP is. You can see this illustrated via the cf-ray response header which indicates the edge node the request was served from:
Brisbane is just up the road from me so my connection has gone a very short distance to a local instance of the site. Fire up NordVPN, connect to a Norway exit node and suddenly we're getting content from Copenhagen:
What did I have to do to get a massively geographically distributed website up and running? Nothing more than spend about 15 mins and $0 ????
Creating a Cloudflare Worker with Wrangler
Just before we jump into this, a quick note: As I completed the entire code, deployment and blog post and prepared for a celebratory beer, I learned that there's a beta of Functions within Cloudflare Pages that would simplify my implementation. Do check that out if you're going to follow in my footsteps.
This is going to be the heart of Password Purgatory, the API that receives a password and puts people through hell by demanding increasingly bizarre and infeasible criteria in order for it to be accepted. It never will be accepted - there has to be no acceptable password - hence being in purgatory ????
Before we go further, the pre-req is Cloudflare's Workers CLI known as "Wrangler". Installation with Node is easy and is documented on the getting started guide for workers. Get it, install it then continue. Make sure you allow Wrangler to manage the Workers in your Cloudflare account:
From the CLI on my local machine, I can now create a brand new project:
wrangler generate password-purgatory-api
This generates all the content required not just to build your own local worker, but inits a new Git repository as well:
The index.js file contains the Worker code and will look familiar if you've previously just created Workers in the browser (check out Serverless to the Max: Doing Big Things for Small Dollars with Cloudflare Workers and Azure Functions and Creating a LaMetric App with Cloudflare Workers and KV for previous examples of Workers).
Let's get to the really cool bit:
wrangler dev
This command fires up the local dev environment which by default, listens on port 8787:
Listening on http://127.0.0.1:8787
Which now means we can do this:
And there we have it - a working worker! That request is then tracked in the CLI:
[2022-03-10 11:38:24] GET password-purgatory-api.troyhunt.workers.dev/ HTTP/1.1 200 OK
Once up and running, you can jump into the Worker code and make edits:
Which the running dev environment immediately picks up on:
Detected changes...
Script modified; context reset.
And then reflects upon next request:
As this is going to be an API returning JSON, let's tweak it a little to return the right content type in the right format:
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
async function handleRequest(request) {
const data = {
message: 'Hello worker!',
};
const json = JSON.stringify(data, null, 2);
return new Response(json, {
headers: { 'content-type': 'application/json;charset=UTF-8' },
})
}
Lastly, I'm going to push this up to GitHub into a brand new repo located at https://github.com/troyhunt/password-purgatory-api
Which now means we can do the next really cool bit:
Deploying the Cloudflare Worker from GitHub
Over in the GitHub marketplace, there's an action called Deploy to Cloudflare Workers with Wrangler which does, well, it's kinda self-explanatory. What we're going to do now is configure GitHub to watch for changes and then publish those over to the live running Worker on Cloudflare. As with the Cloudflare Pages example before, I'm going to configure the simplest possible implementation which will mean deploying from "master" (Wrangler's default branch name as opposed to "main" seen earlier when creating the repository directly in GitHub).
Before doing that, we need to give GitHub the secret Cloudflare uses to orchestrate deployments. Cloudflare's API tokens page allows you to create secrets based on templates and there's a handy one ready to go for Workers:
In configuring this, I've named the token "GitHub deployment of Password Purgatory API", selected my Cloudflare account and for the zone, the passwordpurgatory.com account I created earlier on. Once the token is created it's back to GitHub and I've created a secret called "CF_API_TOKEN" with the appropriate value:
That now sits in the public repository out of sight from anyone else, but accessible via the deployment script we're about to create:
Now for the GitHub action itself and again, all this is going to be the bare basics, simple easy go-fast version:
name: Deploy
on:
push:
branches:
- master
jobs:
deploy:
runs-on: ubuntu-latest
name: Deploy
steps:
- uses: actions/checkout@v2
- name: Publish
uses: cloudflare/[email protected]
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
Note the reference on the last line to the secret I created in the previous step. Save this into the ".github\workflows\main.yml", commit, push and then over in the "Actions" section of the GitHub repo where the magic has already happened:
Let's check it actually exists in Cloudflare by jumping over to the dashboard and drilling down into workers:
Success! But does it actually work? Let's drill down into it further:
The route of password-purgatory-api.troyhunt.workers.dev is generated by Cloudflare and a quick check shows exactly what we'd expect to see:
There's just one more step - defining a custom route. What I really want to do is access this API via api.passwordpurgatory.com and the first thing I'm going to need to do that is a CNAME DNS entry to create the subdomain. Over to Cloudflare's DNS settings and add a new record: