Docs
  • 0.1.0latest

Running it

Build the server, give it a TessariDB node, two key files and its addresses, start it, and make the first operator.

The server ships as the image tessaridb/oauth and can be built from source. Either way it needs a TessariDB node, 0.38.2 or later, reachable over the wire protocol; building it needs Rust 1.98 or newer.

Status. 0.1.0, the first release, tagged v0.1.0.

Build it

git clone https://github.com/tessaridb/tessaridb-oauth
cd tessaridb-oauth && git checkout v0.1.0
cargo build --release -p tessari-oauth-node   # target/release/tessaridb-oauth

What it needs

  • A TessariDB node and an account on it the server signs in with. The server keeps a region's companies in the namespace oauth_<REGION>.
  • Two key files, each readable by its owner only (mode 600 or 400): the 32-byte key-encryption key (KEK) that seals signing keys and other secrets, and the password pepper of at least 32 bytes. Neither is in the store, so neither is in a backup of it: keep both apart from the backups.
  • Two addresses. PUBLIC_URL is where users and clients reach the issuers; ADMIN_URL is the admin console, at another origin.
head -c 32 /dev/urandom > kek.key && chmod 600 kek.key
head -c 32 /dev/urandom > pepper.key && chmod 600 pepper.key

Configuration

The server reads its configuration from the environment when it starts, checks all of it, and refuses to start, naming the variable, when anything is missing or does not fit. A .env file in the working directory is read first, for development only. Every variable starts with TESSARIDB_OAUTH_.

VariableMeaningDefault
PUBLIC_URLthe https base URL; company acme's issuer is <PUBLIC_URL>/acme. Its host is the passkey relying party, so changing the host retires every enrolled passkeyrequired
PUBLIC_ADDRESSwhere the public listener binds0.0.0.0:8443
ADMIN_URLthe admin console's base URL, at an origin other than PUBLIC_URL's: https, or http on 127.0.0.1 or [::1] only; company acme's console is <ADMIN_URL>/admin/acme/required
ADMIN_ADDRESSwhere the admin listener binds0.0.0.0:9443
ADMIN_TLS_CERT, ADMIN_TLS_KEYthe admin console over TLS, PEM, both or neither. Without them it is served in the clear: keep it on a loopback address or behind a TLS terminatorunset
TLS_CERT, TLS_KEYthe public listener over TLS, PEM, both or neither. Without them it is served in the clear for a terminator in front of itunset
TRUSTED_PROXIESthe proxies in front of the server, addresses or address/prefix networks, comma-separated; a request from one of them is counted by the right-most X-Forwarded-For address that is not itself a listed proxynone
KEKpath of the 32-byte key-encryption key filerequired
PEPPERpath of the password pepper filerequired
PASSWORD_MEMORY_KIB, PASSWORD_ITERATIONS, PASSWORD_PARALLELISMthe Argon2id cost of new password hashes; a value below the floor stops the start65536, 3, 1
PASSWORD_CONCURRENCYpassword hashes or checks run at once, each holding the cost's memory4
BREACHED_PASSWORDS_URLan https range-query base ending in / that a new password is looked up in; it is sent the first five hex digits of the password's SHA-1 and nothing else. off turns the lookup offhttps://api.pwnedpasswords.com/range/
STORE, STORE_USER, STORE_PASSWORDthe TessariDB node's wire address and the account the server signs in withrequired
STORE_AUTHORITYthe PEM the node's certificate must lead to; without it only a loopback store is reachednone
STORE_CONNECTIONSconnections to the store at most16
REGIONthe region whose companies this server holdsrequired
TENANTSthe companies created and activated at start, comma-separated; a company an operator suspended stays suspendedrequired
PEERS, PEER_USER, PEER_PASSWORD, PEER_AUTHORITYthe other regions and how to read their directories; see Regionsnone
READ_LIMIT, READ_WINDOW_SECSmetadata, key-set and UserInfo requests one client address may make per window300, 60
TOKEN_LIMIT, TOKEN_WINDOW_SECStoken requests per address per window60, 60
AUTHORIZE_LIMIT, AUTHORIZE_WINDOW_SECSauthorization requests per address per window120, 60
LOGIN_LIMIT, LOGIN_WINDOW_SECSsign-in steps per address per window; console requests count here too30, 60
SMTP_URLthe mail relay: smtps://host[:port], smtp://host[:port] (STARTTLS required), or smtp://<loopback IP>:port?plain on a loopback address only; no credentials in the URL. Without it no mail is sent and a password reset answers 503unset
SMTP_FROMthe sender, address or Name <address>; required with SMTP_URLnone
SMTP_USER, SMTP_PASSWORD_FILEthe relay account, both or neither; the password file is mode 600 or 400none
SMTP_AUTHORITYthe PEM the relay's certificate must lead to; without it the system's roots are usednone

Start it

export TESSARIDB_OAUTH_PUBLIC_URL=https://id.example.com
export TESSARIDB_OAUTH_ADMIN_URL=http://127.0.0.1:9443
export TESSARIDB_OAUTH_ADMIN_ADDRESS=127.0.0.1:9443
export TESSARIDB_OAUTH_KEK=$PWD/kek.key
export TESSARIDB_OAUTH_PEPPER=$PWD/pepper.key
export TESSARIDB_OAUTH_STORE=127.0.0.1:9080
export TESSARIDB_OAUTH_STORE_USER=oauth
export TESSARIDB_OAUTH_STORE_PASSWORD="$STORE_PASSWORD"
export TESSARIDB_OAUTH_REGION=eu
export TESSARIDB_OAUTH_TENANTS=acme
target/release/tessaridb-oauth

At start the server records each configured company, provisions its database, makes a signing key for it and serves its issuer. Discovery answers at once:

curl https://id.example.com/acme/.well-known/openid-configuration

What it does after that, and what it logs, is in How it runs.

In a container

The image tessaridb/oauth carries the server alone, for linux/amd64 and linux/arm64. It listens on 8443 (public) and 9443 (admin console) and runs as the user 65532, which must own the two key files and be the only one able to read them:

head -c 32 /dev/urandom > kek && head -c 32 /dev/urandom > pepper
chmod 400 kek pepper && sudo chown 65532:65532 kek pepper
docker run -d --name oauth -p 8443:8443 -p 127.0.0.1:9443:9443 \
  -v "$PWD/kek:/keys/kek:ro" -v "$PWD/pepper:/keys/pepper:ro" \
  -e TESSARIDB_OAUTH_KEK=/keys/kek -e TESSARIDB_OAUTH_PEPPER=/keys/pepper \
  -e TESSARIDB_OAUTH_PUBLIC_URL=https://id.example.com -e TESSARIDB_OAUTH_ADMIN_URL=https://admin.example.com \
  -e TESSARIDB_OAUTH_STORE=db.example.com:9080 -e TESSARIDB_OAUTH_STORE_USER=oauth \
  -e TESSARIDB_OAUTH_STORE_PASSWORD -e TESSARIDB_OAUTH_STORE_AUTHORITY=/keys/store-ca.pem \
  -v "$PWD/store-ca.pem:/keys/store-ca.pem:ro" \
  -e TESSARIDB_OAUTH_REGION=eu -e TESSARIDB_OAUTH_TENANTS=acme tessaridb/oauth:0.1.0

With a TessariDB node beside it in Compose, the node gets a certificate for the name tessaridb (its key owned by 10001, mode 600) and declares the server's account as its first user, and the server trusts the node through ca.pem. Put the certificate, its key, the authority and the two key files in ./keys, set STORE_PASSWORD, and run docker compose up -d:

services:
  tessaridb:
    image: tessaridb/tessaridb:0.38.2
    restart: unless-stopped
    volumes:
      - tessaridb-data:/var/lib/tessaridb
      - ./keys/store.pem:/keys/store.pem:ro
      - ./keys/store.key:/keys/store.key:ro
    environment:
      TESSARIDB_INITIAL_USER: oauth
      TESSARIDB_INITIAL_PASSWORD: ${STORE_PASSWORD:?set it}
      TESSARIDB_TLS_CERT: /keys/store.pem
      TESSARIDB_TLS_KEY: /keys/store.key

  oauth:
    image: tessaridb/oauth:0.1.0
    restart: unless-stopped
    depends_on: [tessaridb]
    ports:
      - "8443:8443"             # issuers, sign-in pages, account portal
      - "127.0.0.1:9443:9443"   # admin console, this host only
    volumes:
      - ./keys/kek:/keys/kek:ro
      - ./keys/pepper:/keys/pepper:ro
      - ./keys/ca.pem:/keys/ca.pem:ro
    environment:
      TESSARIDB_OAUTH_PUBLIC_URL: https://id.example.com
      TESSARIDB_OAUTH_ADMIN_URL: http://127.0.0.1:9443
      TESSARIDB_OAUTH_KEK: /keys/kek
      TESSARIDB_OAUTH_PEPPER: /keys/pepper
      TESSARIDB_OAUTH_STORE: tessaridb:9080
      TESSARIDB_OAUTH_STORE_USER: oauth
      TESSARIDB_OAUTH_STORE_PASSWORD: ${STORE_PASSWORD:?set it}
      TESSARIDB_OAUTH_STORE_AUTHORITY: /keys/ca.pem
      TESSARIDB_OAUTH_REGION: eu
      TESSARIDB_OAUTH_TENANTS: acme

volumes:
  tessaridb-data:

Discovery then answers at http://127.0.0.1:8443/acme/.well-known/openid-configuration with the issuer https://id.example.com/acme; in production a TLS terminator serves that URL. In a container the first operator is made with docker exec oauth tessaridb-oauth bootstrap-operator --company acme --email root@example.com.

Behind a reverse proxy

The proxy holds the certificate for PUBLIC_URL, passes requests on in the clear and appends the client's address to X-Forwarded-For. List its address in TRUSTED_PROXIES, or every client is counted as the proxy and one client's failed sign-ins hold everyone's. Keep the plain listener reachable from the proxy alone, since passwords and tokens cross it.

The first operator

A new installation has no operator yet. The emergency path makes one; it reads the same environment as the server and needs only the store:

tessaridb-oauth bootstrap-operator --email root@example.com

It creates and activates the system company when there is none, finds or registers the account with that address there, and gives it the operator roles companies and users. When the account has no password yet it prints a one-time enrolment code, shown once and never logged, for POST <PUBLIC_URL>/system/login/api/enroll; run it at a terminal rather than under a supervisor that keeps its output. With --company acme it does the same for a company made since, giving the roles users, clients, policy, keys and audit there. Every run is logged at ERROR as break_glass and recorded on the company's audit trail. See Incidents for the rest of the emergency path.

The operator then sets a password with the code (see Signing in), adds a second factor, and opens the console at <ADMIN_URL>/admin/system/signin.