Microservice to bring 2FA to self hosted PDSes
Rust 90%
Handlebars 9%
Shell 1%
Dockerfile <1%
Just <1%

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

Native PDS 2FA (PDS 0.4.5036 and newer) #

PDS 0.4.5036 (@atproto/pds 0.5.36) added email 2FA to the PDS itself, so the gatekeeper no longer needs to do it.(Everybody say thank you Emelia, who took the time and implemented 2FA upstream) On boot PDS Gatekeeper works out which kind of PDS it is in front of and logs the result:

  • It asks the PDS for its version on /xrpc/_health (retrying for a bit while the PDS starts up). If that is unreachable or reports something it can't read, it falls back to checking account.sqlite for the PDS's account_email_auth_factor table.
  • On a PDS with native 2FA, it does a one time hand over: every account that turned 2FA on through the gatekeeper is turned on in the PDS's own table. Accounts whose email is no longer confirmed are skipped and logged, since the PDS will not allow 2FA on an unconfirmed email. Those users need to confirm their email and turn 2FA back on in the app. Each row is only ever handed over once, so a restart will not turn 2FA back on for someone who has since turned it off.
  • From then on createSession, the OAuth sign-in, getSession and updateEmail are passed straight through to the PDS untouched. You do not need to change your Caddyfile or Traefik labels. The PDS sends its own sign in emails, so GATEKEEPER_EMAIL_TEMPLATES_DIRECTORY and GATEKEEPER_TWO_FACTOR_EMAIL_SUBJECT only apply to older PDS versions.
  • On an older PDS everything works as before.

Look for 2FA mode: Native or 2FA mode: Legacy in the logs to see which was picked, followed by a 2FA hand over to the PDS line with what was migrated.

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. Only used with a PDS older than 0.4.5036, newer ones send their own email.

GATEKEEPER_TWO_FACTOR_EMAIL_SUBJECT - Subject of the email sent to the user when they turn on 2FA. Defaults to Sign in to Bluesky. Only used with a PDS older than 0.4.5036, newer ones send their own email.

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 0.0.0.0

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.