DEV Community

Cover image for How to Configure Asymmetric JWTs on Self-Hosted Supabase
Víctor Pérez Cano
Víctor Pérez Cano

Posted on

How to Configure Asymmetric JWTs on Self-Hosted Supabase

I personally find self-hosting Supabase a great option for running my appplications on production. Unfortunately, Supabase docs are tailored to the official hosted version, and most of the newest features are not available on the open source one, or at least, well-documented enough. This is the case of asymmetric signing keys for JWTs, which were introduced last year. In this guide I'll explain step by step how I've managed to set them up on my self-hosted Supabase instance.

🔐 Why asymmetric signing keys?

Beyond security, using asymmetric cryptography algorithms such as RSA or ECC for signing JWTs brings an important performance improvement. The legacy JWT_SECRET uses symmetric HS256 algorithm for signing and verifying JWTs. This means that on each request made by a user, the JWT must be verified against this secret on your Supabase instance. However, with asymmetric keys the JWT is signed with a private key that is stored on your Supabase instance, but can be verified just with the corresponding public key. This one is typically available at /auth/v1/.well-known/jwks.json which is often cached. This allows your application to verify the JWT of the user without making additional requests to Supabase, by calling getClaims instead of getUser. This is particularly important if you store custom claims on the user's JWT, such as user preferences, roles or subscription details...

🚀 How to set them up

I assume you already have a running self-hosted Supabase instance, with a docker-compose.yml set up. If not, there are several guides on how to set it up on a VPS, but I'd recommend you to use Dokploy with the following template.

To keep things simple, I wrote a small script that generates the keys using OpenSSL and converts them to the format supabase expects. In a nutshell, the script does the following:

  1. Generates a pair of public/private keys using the algorithm of your choice, either RS256 (RSA) or ES256 (Elliptic Curve).
  2. Prompts you for an optional Key ID, or generates a random one.
  3. Writes both keys in JSON Web Key (JWK) format.
  4. Prompts you for both your previous ANON_KEY and SERVICE_ROLE_KEY in order to re-sign them with the new keys.

You can run the script with the following command:

curl -sL https://gist.githubusercontent.com/vpcano/28e93b8af3cb36ba3ecd9a397ccf0ab7/raw/33a2769ac9d2ff0b8fa953566ee1fd5a773ec159/supabase-keygen.sh | bash
Enter fullscreen mode Exit fullscreen mode

The script will output the following environment variables, which you should then set in the corresponding supabase service under the docker-compose.yml file:

  • JWT_SIGNING_KEYS: this contains both the private and public keys, and must be set to the auth service GOTRUE_JWT_KEYS env var.
  • JWT_METHODS: this indicates to the auth service that you are now using asymmetric signing algorithms. It must be set to the GOTRUE_JWT_VALID_METHODS env var of the auth service.
  • JWT_JWKS: this contains the public key in JWK format. It must be set to the following services:
    • rest -> PGRST_JWT_SECRET
    • rest -> PGRST_APP_SETTINGS_JWT_SECRET
    • realtime -> API_JWT_JWKS
    • storage -> JWT_JWKS
  • ANON_KEY and SERVICE_ROLE_KEY: these should replace your previous ones on all your Supabase services and your client applications.

The final step is to make the /auth/v1/.well-known/jwks.json endpoint publicly available. This is done on the kong service config file kong.yml, which is typically mounted on a volume in the kong service. On Dokploy, this can be found on the "Advanced" tab at the end of the "Volumes" section. I added the following on the services section under ## Open Auth routes:

services:
  ## Open Auth routes
  # ...

  - name: auth-v1-open-jwks
    url: http://auth:9999/.well-known/jwks.json
    routes:
      - name: auth-v1-open-jwks
        strip_path: true
        paths:
          - /auth/v1/.well-known/jwks.json
    plugins:
      - name: cors

  ## Secure Auth routes  
  # ...
Enter fullscreen mode Exit fullscreen mode

Finally, restart all Supabase services.

🧐 Checking if everything worked fine

First, navigate to https://<your.supabase.url>/auth/v1/.well-known/jwks.json. You should see something similar to this:

{"keys":[{"alg":"ES256","crv":"P-256","key_ops":["verify"],"kid":"...","kty":"EC","use":"sig","x":"...","y":"..."}]}
Enter fullscreen mode Exit fullscreen mode

This is your public key, used to verify JWTs.

To check your users JWTs are being signed with the new key, you can extract a JWT from an HTTP header or a cookie, and decode it in jwt.io. The header of the JWT should look like this:

{
  "alg": "ES256",  // or RS256
  "typ": "JWT",
  "kid": "..."
}
Enter fullscreen mode Exit fullscreen mode

You can also check that the signature can be correctly verified by pasting the above public key on the "JWT Signature Verification" at the bottom.


And that's it! I truly hope this guide was helpful to anyone struggling with self-hosted Supabase. By the way, this is my first time posting on dev.to, so I'd love to hear your thoughts. Thanks for reading! 👋

Top comments (0)