Microservice to bring 2FA to self hosted PDSes
Rust 83.3%
Handlebars 16.2%
Dockerfile 0.3%
Just 0.2%
18 4 0

Clone this repository

https://tangled.org/mackuba.eu/pds-gatekeeper
git@tangled.org:mackuba.eu/pds-gatekeeper

For self-hosted knots, clone URLs may differ based on your setup.

README.md

PDS gatekeeper#

A microservice that sits on the same server as the PDS to add some of the security that the entryway does.

Picture in black and white of a grassy hill with a gate at the top

PDS gatekeeper works by overriding some of the PDS endpoints inside your Caddyfile to provide gatekeeping to certain endpoints. Mainly, the ability to have 2FA on a self hosted PDS like it does on a Bluesky mushroom(PDS). Most of the logic of these endpoints still happens on the PDS via a proxied request, just some are gatekept.

Features#

2FA#

  • Overrides The login endpoint to add 2FA for both Bluesky client logged in and OAuth logins
  • Overrides the settings endpoints as well. As long as you have a confirmed email you can turn on 2FA

Captcha on account creation#

Require a verificationCode set on the createAccount request. This is gotten from completing a captcha challenge hosted on the PDS mimicking what the Bluesky Entryway does. Migration tools will need to support this, but social-apps will support and redirect to GATEKEEPER_DEFAULT_CAPTCHA_REDIRECT. This is how the clients know to get the code to prove a captcha was successful.

  • Requires GATEKEEPER_CREATE_ACCOUNT_CAPTCHA to be set to true.
  • Requires PDS_HCAPTCHA_SITE_KEY and PDS_HCAPTCHA_SECRET_KEY to be set. Can sign up at https://www.hcaptcha.com/
  • Requires proxying /xrpc/com.atproto.server.describeServer, /xrpc/com.atproto.server.createAccount and /gate/* to PDS Gatekeeper
  • Optional GATEKEEPER_JWE_KEY key to encrypt the captcha verification code. Defaults to a random 32 byte key. Not strictly needed unless you're scaling
  • OptionalGATEKEEPER_DEFAULT_CAPTCHA_REDIRECT default redirect on captcha success. Defaults to https://bsky.app.
  • Optional GATEKEEPER_CAPTCHA_SUCCESS_REDIRECTS allowed redirect urls for captcha success. You want these to match the url showing the captcha. Defaults are:

Block account creation unless it's a migration#

You can set GATEKEEPER_ALLOW_ONLY_MIGRATIONS to block createAccount unless it's via a migration. This does not require a change for migration tools, but social-apps create a new account will no longer work and to create a brand new account users will need to do this via the Oauth account create screen on the PDS. We recommend setting PDS_HCAPTCHA_SITE_KEY and PDS_HCAPTCHA_SECRET_KEY so the OAuth screen is protected by a captcha if you use this with invite codes turned off.

Setup#

PDS Gatekeeper has 2 parts to its setup, docker compose file and a reverse proxy (Caddy in this case). I will be assuming you setup the PDS following the directions found here, but if yours is different, or you have questions, feel free to let me know, and we can figure it out.

Docker compose#

The pds gatekeeper container can be found on docker hub under the name fatfingers23/pds_gatekeeper. The container does need access to the /pds root folder to access the same db's as your PDS. The part you need to add would look a bit like below. You can find a full example of what I use for my pds at ./examples/compose.yml. This is usually found at /pds/compose.yamlon your PDS>

  gatekeeper:
    container_name: gatekeeper
    image: fatfingers23/pds_gatekeeper:latest
    network_mode: host
    restart: unless-stopped
    #This gives the container to the access to the PDS folder. Source is the location on your server of that directory
    volumes:
      - type: bind
        source: /pds
        target: /pds
    depends_on:
      - pds

For Coolify, if you're using Traefik as your proxy you'll need to make sure the labels for the container are set up correctly. A full example can be found at ./examples/coolify-compose.yml.

gatekeeper:
  container_name: gatekeeper
  image: 'fatfingers23/pds_gatekeeper:latest'
  restart: unless-stopped
  volumes:
    - '/pds:/pds'
  environment:
    - 'PDS_DATA_DIRECTORY=${PDS_DATA_DIRECTORY:-/pds}'
    - 'PDS_BASE_URL=http://pds:3000'
    - GATEKEEPER_HOST=0.0.0.0
  depends_on:
    - pds
  healthcheck:
    test:
      - CMD
      - timeout
      - '1'
      - bash
      - '-c'
      - 'cat < /dev/null > /dev/tcp/0.0.0.0/8080'
    interval: 10s
    timeout: 5s
    retries: 3
    start_period: 10s
  labels:
    - traefik.enable=true
    - 'traefik.http.routers.pds-gatekeeper.rule=Host(`yourpds.com`) && (Path(`/xrpc/com.atproto.server.getSession`) || Path(`/xrpc/com.atproto.server.updateEmail`) || Path(`/xrpc/com.atproto.server.createSession`) || Path(`/xrpc/com.atproto.server.createAccount`) || Path(`/@atproto/oauth-provider/~api/sign-in`))'
    - traefik.http.routers.pds-gatekeeper.entrypoints=https
    - traefik.http.routers.pds-gatekeeper.tls=true
    - traefik.http.routers.pds-gatekeeper.priority=100
    - traefik.http.routers.pds-gatekeeper.middlewares=gatekeeper-cors
    - traefik.http.services.pds-gatekeeper.loadbalancer.server.port=8080
    - traefik.http.services.pds-gatekeeper.loadbalancer.server.scheme=http
    - 'traefik.http.middlewares.gatekeeper-cors.headers.accesscontrolallowmethods=GET,POST,PUT,DELETE,OPTIONS,PATCH'
    - 'traefik.http.middlewares.gatekeeper-cors.headers.accesscontrolallowheaders=*'
    - 'traefik.http.middlewares.gatekeeper-cors.headers.accesscontrolalloworiginlist=*'
    - traefik.http.middlewares.gatekeeper-cors.headers.accesscontrolmaxage=100
    - traefik.http.middlewares.gatekeeper-cors.headers.addvaryheader=true
    - traefik.http.middlewares.gatekeeper-cors.headers.accesscontrolallowcredentials=true

Caddy setup#

For the reverse proxy I use caddy. This part is what overwrites the endpoints and proxies them to PDS gatekeeper to add in extra functionality. The main part is below, for a full example see ./examples/Caddyfile. This is usually found at /pds/caddy/etc/caddy/Caddyfile on your PDS.

    @gatekeeper {
                path /xrpc/com.atproto.server.getSession
                path /xrpc/com.atproto.server.describeServer
                path /xrpc/com.atproto.server.updateEmail
                path /xrpc/com.atproto.server.createSession
                path /xrpc/com.atproto.server.createAccount
                path /@atproto/oauth-provider/~api/sign-in
                path /gate/*
    }

    handle @gatekeeper {
       reverse_proxy http://localhost:8080
   }

   reverse_proxy http://localhost:3000

If you use a cloudflare tunnel then your caddyfile would look a bit more like below with your tunnel proxying to localhost:8081 (or w/e port you want).

http://*.localhost:8082, http://localhost:8082 {
    @gatekeeper {
        path /xrpc/com.atproto.server.getSession
        path /xrpc/com.atproto.server.describeServer
        path /xrpc/com.atproto.server.updateEmail
        path /xrpc/com.atproto.server.createSession
        path /xrpc/com.atproto.server.createAccount
        path /@atproto/oauth-provider/~api/sign-in
        path /gate/*
    }

    handle @gatekeeper {
             #This is the address for PDS gatekeeper, default is 8080
             reverse_proxy http://localhost:8080
             #Makes sure the cloudflare ip is proxied and able to be picked up by pds gatekeeper
             header_up X-Forwarded-For {http.request.header.CF-Connecting-IP}
    }
    reverse_proxy http://localhost:3000
}

Environment variables and bonuses#

Every environment variable can be set in the pds.env and shared between PDS and gatekeeper and the PDS, with the exception of PDS_ENV_LOCATION. This can be set to load the pds.env, by default it checks /pds/pds.env and is recommended to mount the /pds folder on the server to /pds in the pds gatekeeper container.

PDS_DATA_DIRECTORY - Root directory of the PDS. Same as the one found in pds.env this is how pds gatekeeper knows knows the rest of the environment variables.

GATEKEEPER_EMAIL_TEMPLATES_DIRECTORY - The folder for templates of the emails PDS gatekeeper sends. You can find them in ./email_templates. You are free to edit them as you please and set this variable to a location in the pds gateekeper container and it will use them in place of the default ones. Just make sure ot keep the names the same.

GATEKEEPER_TWO_FACTOR_EMAIL_SUBJECT - Subject of the email sent to the user when they turn on 2FA. Defaults to Sign in to Bluesky

PDS_BASE_URL - Base url of the PDS. You most likely want https://localhost:3000 which is also the default

GATEKEEPER_HOST - Host for pds gatekeeper. Defaults to 127.0.0.1

GATEKEEPER_PORT - Port for pds gatekeeper. Defaults to 8080

GATEKEEPER_CREATE_ACCOUNT_PER_SECOND - Sets how often it takes a count off the limiter. example if you hit the rate limit of 5 and set to 60, then in 60 seconds you will be able to make one more. Or in 5 minutes be able to make 5 more.

GATEKEEPER_CREATE_ACCOUNT_BURST - Sets how many requests can be made in a burst. In the prior example this is where the 5 comes from. Example can set this to 10 to allow for 10 requests in a burst, and after 60 seconds it will drop one off.

GATEKEEPER_ALLOW_ONLY_MIGRATIONS - Defaults false. If set to true, will only allow the /xrpc/com.atproto.server.createAccount endpoint to be used for migrations. Meaning it will check for the serviceAuth token and verify it is valid.