# Drasl — self-hosted auth (Yggdrasil + skins) ## Summary Self-hosted, Yggdrasil-compatible auth + skin server. Drop-in Mojang replacement via authlib-injector. Image: `unmojang/drasl:latest`. Reached only through Caddy at `auth.ulicraft.net`; no host port. **OIDC/Keycloak is OUT for now** — drasl runs in **password-login** mode (`AllowPasswordLogin = true`). Admin + guests register with a username/password on the drasl web UI. Re-introducing Keycloak OIDC later is a future task. Config is **TOML only** (no env var support). So `config.toml` is rendered from `config.toml.tmpl` by `tooling/render-config.sh` to inject `BASE_DOMAIN`. ## config.toml.tmpl (key fields) ```toml BaseURL = "https://auth.${BASE_DOMAIN}" # public scheme is HTTPS (nginx TLS) Domain = "auth.${BASE_DOMAIN}" ListenAddress = "0.0.0.0:25585" # internal; Caddy proxies DefaultAdmins = ["admin"] # NOT DefaultAdminUsernames (see gotchas) AllowPasswordLogin = true # Top-level — NOT under any [Section] (see gotchas). CORSAllowOrigins = ["https://${BASE_DOMAIN}"] [ApplicationOwner] # ... # OIDC block intentionally omitted for now (no Keycloak). # [[RegistrationOIDC]] ← future ``` ## Critical gotchas (carry-over) - **1.21+ secure profile**: drasl `SignPublicKeys = false` MUST pair with server `ENFORCE_SECURE_PROFILE=FALSE`. Linked — both or neither. - **authlib endpoint** = `BaseURL` + `/authlib-injector` → `http://auth.ulicraft.net/authlib-injector`. Must match on server and client. Network alias makes this identical inside/outside the stack. - **`Domain` affects skins** — if wrong, authlib clients may not see skins. - **Config keys are validated loosely — wrong keys are silently ignored** (drasl logs `Warning: unknown config option ` and moves on). Two that bit us: - Admin list key is **`DefaultAdmins`** (top-level), NOT `DefaultAdminUsernames` (that key never existed → no admin → admin-only routes like `GET /players` return 403). - **`CORSAllowOrigins` must be top-level**, before any `[Section]`. Placed after a table header it parses as `.CORSAllowOrigins`, is ignored, and the API serves no CORS headers → landing register/account browser calls blocked. - `RateLimit.Burst` is valid in drasl master but warns on `unmojang/drasl:latest` (image lags) — harmless. - **First-admin bootstrap with invite mode on is a chicken-egg.** `DefaultAdmins` only promotes the account at startup reconciliation, and `POST /drasl/api/v2/users` is invite-gated (admins do NOT bypass the *register* endpoint). To create the first admin: temporarily set `RequireInvite = false` in the rendered config, `docker compose restart drasl`, `POST /users` to register `admin`, then revert + restart (always trap-revert). API recap: `POST /login {username,password}` → `{apiToken}`; admin-only `GET /players` with `Authorization: Bearer `. - **NMSR avatars need https + a host-gateway pin** (see `plan/19-routes.md`). Drasl's profile embeds **https** skin URLs (BaseURL is https). NMSR reaches those via `extra_hosts: auth.${BASE_DOMAIN}:host-gateway` (host nginx :443, real LE cert) with https mojank endpoints. Without it the skin fetch Connect-fails to caddy:443 and every avatar renders the DEFAULT skin. ## Tasks - [ ] Create `drasl/config/config.toml.tmpl` (password-login, no OIDC block) - [ ] Set `BaseURL`/`Domain` to `auth.${BASE_DOMAIN}`, `ListenAddress 0.0.0.0:25585` - [ ] Set `SignPublicKeys = false` - [ ] Wire `render-config.sh` → `drasl/config/config.toml` (gitignored) - [ ] Remove drasl host port from compose (internal only) - [ ] Update compose: drasl on `mcnet`, no `25585:25585` publish - [x] Bootstrap admin account (`admin`, done 2026-06-10 via invite-flip; creds in cochi `.env` for the mc-status roster). Create guest accounts as needed. - [ ] Verify authlib endpoint responds through Caddy - [ ] Future: re-add `[[RegistrationOIDC]]` Keycloak block + secret file