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_URLis where users and clients reach the issuers;ADMIN_URLis 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_.
| Variable | Meaning | Default |
|---|---|---|
PUBLIC_URL | the 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 passkey | required |
PUBLIC_ADDRESS | where the public listener binds | 0.0.0.0:8443 |
ADMIN_URL | the 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_ADDRESS | where the admin listener binds | 0.0.0.0:9443 |
ADMIN_TLS_CERT, ADMIN_TLS_KEY | the 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 terminator | unset |
TLS_CERT, TLS_KEY | the public listener over TLS, PEM, both or neither. Without them it is served in the clear for a terminator in front of it | unset |
TRUSTED_PROXIES | the 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 proxy | none |
KEK | path of the 32-byte key-encryption key file | required |
PEPPER | path of the password pepper file | required |
PASSWORD_MEMORY_KIB, PASSWORD_ITERATIONS, PASSWORD_PARALLELISM | the Argon2id cost of new password hashes; a value below the floor stops the start | 65536, 3, 1 |
PASSWORD_CONCURRENCY | password hashes or checks run at once, each holding the cost's memory | 4 |
BREACHED_PASSWORDS_URL | an 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 off | https://api.pwnedpasswords.com/range/ |
STORE, STORE_USER, STORE_PASSWORD | the TessariDB node's wire address and the account the server signs in with | required |
STORE_AUTHORITY | the PEM the node's certificate must lead to; without it only a loopback store is reached | none |
STORE_CONNECTIONS | connections to the store at most | 16 |
REGION | the region whose companies this server holds | required |
TENANTS | the companies created and activated at start, comma-separated; a company an operator suspended stays suspended | required |
PEERS, PEER_USER, PEER_PASSWORD, PEER_AUTHORITY | the other regions and how to read their directories; see Regions | none |
READ_LIMIT, READ_WINDOW_SECS | metadata, key-set and UserInfo requests one client address may make per window | 300, 60 |
TOKEN_LIMIT, TOKEN_WINDOW_SECS | token requests per address per window | 60, 60 |
AUTHORIZE_LIMIT, AUTHORIZE_WINDOW_SECS | authorization requests per address per window | 120, 60 |
LOGIN_LIMIT, LOGIN_WINDOW_SECS | sign-in steps per address per window; console requests count here too | 30, 60 |
SMTP_URL | the 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 503 | unset |
SMTP_FROM | the sender, address or Name <address>; required with SMTP_URL | none |
SMTP_USER, SMTP_PASSWORD_FILE | the relay account, both or neither; the password file is mode 600 or 400 | none |
SMTP_AUTHORITY | the PEM the relay's certificate must lead to; without it the system's roots are used | none |
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.