Files
ulicraft-server-v1/plan/03-drasl.md
Oier Bravo Urtasun c233b1bfbc docs(drasl): record admin bootstrap + config-key/CORS/NMSR gotchas
Capture what this session learned the hard way, in plan/03-drasl.md and CLAUDE.md:

- Correct admin key is DefaultAdmins (not DefaultAdminUsernames); wrong/unknown
  keys are silently ignored by drasl (logged as "unknown config option").
- CORSAllowOrigins must be top-level, before any [Section], or it's ignored.
- First-admin bootstrap under invite mode is a chicken-egg → temp-flip
  RequireInvite=false, register, revert.
- NMSR needs the auth. host-gateway pin + https mojank endpoints or avatars
  render the default skin (cross-ref plan/19-routes.md).
- Fix the stale example config and mark the admin-bootstrap task done.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 21:20:54 +02:00

3.9 KiB

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)

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-injectorhttp://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 <key> 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 <table>.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 <token>.
  • 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.shdrasl/config/config.toml (gitignored)
  • Remove drasl host port from compose (internal only)
  • Update compose: drasl on mcnet, no 25585:25585 publish
  • 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