root@pastalabs:~#

~/blog/matrix-with-pubky-ring

A Matrix chat you sign in to with Pubky Ring, and how to run your own

matrix.pubky.pastalabs.dev is a Matrix server with no passwords: you sign in by approving a QR code in Pubky Ring, and your Pubky key becomes your Matrix ID. Here is how to join it, and how to run the same setup with Tuwunel and pubky-oidc.

There is now a Matrix chat server at matrix.pubky.pastalabs.dev, and its sign-in is a little different: no password, no email, no sign-up form. You scan a QR code with Pubky Ring, approve, and you are in.

The idea

On Pubky, your identity is a key you hold in Pubky Ring, not an account on someone's server. This server reuses that identity for chat:

  • your Pubky key becomes your Matrix ID: @<your-pubky-key>:matrix.pubky.pastalabs.dev;
  • your display name comes from your Pubky profile;
  • signing in again with the same key brings you back to the same account;
  • there is nothing to remember and nothing to leak: the server never sees a password, because there is none.

Under the hood, a small sign-in gateway at auth.pubky.pastalabs.dev speaks OpenID Connect to the Matrix server (Tuwunel, written in Rust). Its only sign-in method is the Pubky Ring QR code. The gateway is open source: PastaGringo/pubky-oidc, and it works with any app that supports OpenID Connect, not only Matrix.

Tutorial: join from your browser

You need Pubky Ring with your Pubky identity in it. The steps below use Element, the best-known Matrix client, in its web version.

1. Point Element at the server

Open app.element.io. On the Sign in screen, next to Account provider, click Edit, choose Other account provider, type matrix.pubky.pastalabs.dev and confirm.

Element, with matrix.pubky.pastalabs.dev as the account provider

2. Continue to the Pubky sign-in

Click Continue. Element sends you to the sign-in page of auth.pubky.pastalabs.dev, which shows a QR code. The page is in French for now: its title reads Se connecter avec Pubky Ring ("Sign in with Pubky Ring").

The Pubky Ring sign-in page (QR code blurred)

3. Approve in Pubky Ring

  • On a computer: scan the QR code with Pubky Ring.
  • On your phone: tap the yellow button, Ouvrir Pubky Ring sur ce téléphone ("Open Pubky Ring on this phone").

Approve the request in Pubky Ring. The page waits for you (En attente de l'approbation…, "Waiting for approval…"), then moves on by itself.

4. Confirm, and you are in

The Matrix server shows one last page, Continue sign-in: "You are signing in as @<your-pubky-key>:matrix.pubky.pastalabs.dev to https://app.element.io/". Click Continue and Element opens, signed in. The first time, your account is created on the spot.

5. Start chatting

To talk to someone on the server, start a conversation with their Matrix ID, @<their-pubky-key>:matrix.pubky.pastalabs.dev.

Good to know

  • This server does not federate. You can talk with people who have an account on matrix.pubky.pastalabs.dev, but not with users of matrix.org or other Matrix servers.
  • It is an early version. If the sign-in gateway restarts while you are on the QR page, that sign-in is lost: just start again. Your Matrix account is not affected.
  • Clients: the steps above were tested with Element web.

Run your own: Tuwunel + pubky-oidc

The same setup fits in three containers: Tuwunel for Matrix, pubky-oidc for the Pubky Ring sign-in, and Caddy in front of both for HTTPS.

What you need

  • A Linux server with Docker and Docker Compose, and ports 80 and 443 open.
  • A domain with two DNS records pointing to that server. Below they are matrix.example.com (the Matrix server) and auth.example.com (the sign-in page): replace them everywhere with yours.

Your users' Matrix IDs will be @<pubky-key>:matrix.example.com. The server name cannot be changed once accounts exist, so pick it now.

1. The files

Create a folder, say matrix/, with three files.

compose.yml:

services:
  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy-data:/data

  tuwunel:
    image: ghcr.io/matrix-construct/tuwunel:v1.9.4
    restart: unless-stopped
    environment:
      TUWUNEL_CONFIG: /etc/tuwunel/tuwunel.toml
    volumes:
      - ./tuwunel.toml:/etc/tuwunel/tuwunel.toml:ro
      - ./secrets/pubky_client_secret:/run/secrets/pubky_client_secret:ro
      - tuwunel-data:/var/lib/tuwunel

  pubky-oidc:
    build: https://github.com/PastaGringo/pubky-oidc.git
    restart: unless-stopped
    environment:
      ISSUER: https://auth.example.com
      CLIENT_ID: pubky-ring
      CLIENT_SECRET: ${PUBKY_CLIENT_SECRET}
      REDIRECT_URIS: https://matrix.example.com/_matrix/client/unstable/login/sso/callback/pubky-ring
    volumes:
      - pubky-oidc-data:/data

volumes:
  caddy-data:
  tuwunel-data:
  pubky-oidc-data:

Caddyfile, which also gets the HTTPS certificates for you:

matrix.example.com {
	reverse_proxy tuwunel:8008
}

auth.example.com {
	reverse_proxy pubky-oidc:4000
}

tuwunel.toml:

[global]
server_name = "matrix.example.com"
database_path = "/var/lib/tuwunel"
address = ["0.0.0.0"]
port = 8008
# A private server: no federation, no password sign-up.
allow_federation = false
allow_registration = false
# Display names come from Pubky profiles, as they are.
new_user_displayname_suffix = ""

[global.well_known]
client = "https://matrix.example.com"

[[global.identity_provider]]
brand = "Pubky"
name = "Pubky Ring"
client_id = "pubky-ring"
client_secret_file = "/run/secrets/pubky_client_secret"
issuer_url = "https://auth.example.com"
callback_url = "https://matrix.example.com/_matrix/client/unstable/login/sso/callback/pubky-ring"
scope = ["openid", "profile"]
# The Pubky key (the `sub` claim) becomes the Matrix user ID, with no random fallback.
userid_claims = ["sub"]
unique_id_fallbacks = false

The key line is userid_claims = ["sub"]: pubky-oidc puts the Pubky key in sub, so it becomes the Matrix user ID. unique_id_fallbacks = false makes sure Tuwunel never invents another one.

2. A shared secret

Tuwunel and pubky-oidc must share a client secret. Generate it once, in the matrix/ folder:

mkdir -p secrets
openssl rand -hex 32 | tr -d '\n' > secrets/pubky_client_secret
echo "PUBKY_CLIENT_SECRET=$(cat secrets/pubky_client_secret)" > .env
chmod 600 .env

Tuwunel reads it from the file, pubky-oidc from the .env file that Docker Compose loads.

3. Start

docker compose up -d --build

Docker builds pubky-oidc straight from its GitHub repository, then starts the three containers. Caddy needs a minute to get the certificates.

4. Check

curl https://matrix.example.com/_matrix/client/versions
curl https://auth.example.com/.well-known/openid-configuration

Both must answer with JSON. Tuwunel itself fetches the second URL to find the sign-in page, so your server must be able to reach its own domain.

Then sign in exactly as in the tutorial above, with matrix.example.com as the account provider. The first person to sign in gets the first account; anyone with Pubky Ring can join after them.

Keep in mind

  • Keep the pubky-oidc-data volume. It holds the keys pubky-oidc signs with; losing them signs everyone out.
  • Back up tuwunel-data. It is the Matrix database: accounts, rooms, messages.
  • The files above were tested end to end, from the QR approval to a Matrix account and back, with a throwaway Pubky identity standing in for Pubky Ring, on a local copy of this setup served over plain HTTP. The Caddyfile was checked with caddy validate.

Questions about Pubky Ring itself? Have a look at the Pubky projects on pubky.pastalabs.dev.