Sidecar: auth token for the website-facing API

config.rs loads all runtime settings from an external sidecar.toml (path via
$UOLINK_CONFIG), with env-var overrides (UOLINK_WEB_TOKEN, UOLINK_WEB_BIND,
UOLINK_SHARD_BIND, UOLINK_DB_PATH). Nothing is compiled into the binary. On first
run the file is generated with a random 24-byte auth token, so the sidecar is
secured out of the box and the operator just copies the token to the website.

An axum middleware rejects any request to a non-/health route that does not
present the token, as Authorization: Bearer, X-Api-Key, or ?token= (the last so
browser WebSocket clients, which cannot set handshake headers, can authenticate).
The comparison is constant-time. An empty token disables auth and is only
tolerated on a loopback bind; binding to 0.0.0.0 with no token logs a warning.

Verified: /health open (200); /history 401 without a token, 401 with a wrong one,
200 with the right one via either Bearer or X-Api-Key; an authed shard query
falls through to 503 when no shard is connected; WS rejected (401) with a bad
?token= and upgraded (101) with the right one.

sidecar.toml is gitignored (holds the secret); sidecar.toml.example is committed
as the reference.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 16:56:55 -05:00
parent 946ba7027b
commit c4de5fa8ad
9 changed files with 469 additions and 13 deletions

View File

@@ -0,0 +1,30 @@
# uo-link sidecar configuration — example.
#
# The sidecar reads `sidecar.toml` (override the path with $UOLINK_CONFIG). If that file
# is absent on first run, one is generated automatically with a random auth_token, so you
# normally do not create this by hand — just start the sidecar and edit the file it writes.
# Nothing here is compiled into the binary.
#
# Environment variables override the file:
# UOLINK_SHARD_BIND, UOLINK_WEB_BIND, UOLINK_WEB_TOKEN, UOLINK_DB_PATH
[shard]
# Loopback address the shard dials out to. Keep this on localhost — the game must not
# be reachable from anywhere else.
bind = "127.0.0.1:7788"
[web]
# Address the website connects to (WebSocket + REST).
# 127.0.0.1:8080 -> same host only
# 0.0.0.0:8080 -> accept remote clients (then auth_token is mandatory)
bind = "127.0.0.1:8080"
# Shared secret the website must present on every request:
# REST: Authorization: Bearer <token> (or X-Api-Key: <token>)
# WebSocket: add ?token=<token> to the connect URL
# Empty disables auth, which is only allowed on a loopback bind (a warning is logged).
# Rotate by changing this value and restarting.
auth_token = "replace-with-a-long-random-secret"
[store]
path = "uo-link.db"