Frequently Asked Question

Building Password Purgatory with Cloudflare Pages and Workers
Last Updated 2 years ago

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:

    image

    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:

    image

    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":

    image

    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:

    image

    No problem, back over to the Cloudflare Pages app on GitHub and add access to the new repo:

    image

    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:

    image

    Time to set up builds and deployments and I'm just going to stick with all the defaults:

    image

    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!

    image

    The live site is now addressable at password-purgatory.pages.dev:

    image

    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":

    image

    I'd already registered passwordpurgatory.com with DNSimple so the domain exists already, let's drop it in:

    image

    Finding the domain already registered, Cloudflare now needs me to update DNS to point over to their nameservers:

    image

    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:

    image

    Free is good ????

    image

    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):

    image

    Nameserver time! Cloudflare has identified the existing 4 nameservers at DNSimple and given me 2 of their own to replace these with:

    image

    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:

    image

    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:

    image

    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:

    image

    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:

    image

    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:

    image

    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:

    image

    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:

    image

    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:

    image

    Which the running dev environment immediately picks up on:

     Detected changes...
    Script modified; context reset.

    And then reflects upon next request:

    image

    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:

    image

    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:

    image

    That now sits in the public repository out of sight from anyone else, but accessible via the deployment script we're about to create:

    image

    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:

    image

    Let's check it actually exists in Cloudflare by jumping over to the dashboard and drilling down into workers:

    image

    Success! But does it actually work? Let's drill down into it further:

    image

    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:

    image

    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:

    Please Wait!

    Please wait... it will take a second!