Files
dbledeez 295eb22826 panel public release
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 01:26:41 -07:00

6.0 KiB

Admin accounts, password reset, and Steam

First-admin bootstrap

On every start the controller checks the users table (controller/cmd/controller/auth.go, bootstrapAdmin). If it's empty, it creates:

  • email: admin@panel.local
  • role: admin
  • password: randomly generated and printed to the log inside a loud ADMIN BOOTSTRAP banner (systemd: journalctl -u panel-controller; compose: docker compose logs controller). Log in once and change it via the UI.

To pre-seed a stable password instead (dev, CI, unattended installs), set PANEL_ADMIN_PASSWORD in the controller's environment before first boot. The banner then says the password came from the env var and never prints it.

Sessions are cookie-based (panel_session); passwords are stored as argon2id hashes.

Resetting a lost admin password

Use the reset-admin tool (controller/cmd/reset-admin). It writes the DB directly, so it works even when you can't log in:

go build -o bin/reset-admin ./controller/cmd/reset-admin   # or use a prebuilt copy

./bin/reset-admin --email admin@panel.local --password 'NewS3cret' \
  --db-url 'postgres://panel:...@127.0.0.1:5432/panel?sslmode=disable'
# → reset-admin: password updated for admin@panel.local

--db-url falls back to $PANEL_DB_URL, then to the controller's dev default DSN. The controller does not need a restart — the next login verifies against the new hash. Existing sessions stay valid; log out / delete rows from sessions if you want them revoked.

Hash-only mode (no DB access from the tool — e.g. you only have psql):

./bin/reset-admin 'NewS3cret'
# → $argon2id$v=19$m=65536,t=3,p=2$...$...

then apply it manually:

UPDATE users
   SET password_hash = '$argon2id$v=19$m=65536,t=3,p=2$...$...',
       updated_at    = NOW()
 WHERE email = 'admin@panel.local';

(Quote the hash in single quotes — it contains $.)

With the managed panel-postgres container:

docker exec -it panel-postgres psql -U panel -d panel

Adding more users

The account you set at install is the first admin. To add teammates, open the 🎮 Steam Accounts panel (sidebar) — this is the user-management screen. As an admin you can:

  • List every panel user and their linked Steam ID.
  • Add a user by their 17-digit SteamID64, as role user or admin (POST /api/users). They then log in with Sign in with Steam — no password for you to manage or hand out.
  • Link / unlink a Steam ID on any account.

The intended model: you (the install admin) sign in with your email + password; everyone else you add is Steam-based. That's ideal for a gaming community — players already have Steam.

Limitation, stated plainly: there is currently no UI/API to create additional email + password usersPOST /api/users is Steam-only. If you need a second non-Steam login, add it at the database level or with reset-admin --email <addr> --password <pw> (see Resetting a lost admin password). A proper "invite a password user" flow is a known follow-up.

Sign in with Steam (operator login)

Optional OpenID login for the dashboard. Two controller flags:

  • -public-url https://panel.example.com — the public base URL of the dashboard. Steam's OpenID flow redirects back to <public-url>/api/auth/steam/callback, so this must be the URL your browser actually uses (reverse-proxied name included).
  • -initial-admin-steam-id 7656119… — your 17-digit SteamID64.
    • Empty DB → the bootstrap admin is created with this Steam ID linked; "Sign in with Steam" works from the first boot.
    • Existing DB → the first admin gets retro-linked, if it has no Steam ID yet (an existing different link is never clobbered).

Users can also link/unlink Steam on their own account from the UI (/api/auth/steam/link-start). Steam-linked users may sign in with either method; Steam-only users (created by an admin via POST /api/users) have no password at all.

Find your SteamID64 at steamid.io or via your profile URL.

Steam credentials for paid games (requires_steam_login)

Some dedicated servers refuse SteamCMD's +login anonymous — DayZ (app 223350) is the shipped example; its provider declares requires_steam_login: true in modules/dayz/module.yaml. Those downloads need a real Steam account that owns the game.

How you supply them — you don't pre-configure anything:

  1. Click Update (or create) on such an instance.
  2. No cached credential → the API answers {"steam_login_required": true, ...} and the dashboard pops the Steam login modal.
  3. Enter Steam username + password. If the account has Steam Guard, the first attempt returns {"need_guard": true} and the modal re-prompts for the emailed / mobile-authenticator code.
  4. The controller runs a one-shot SteamCMD sidecar to validate the login, then re-runs your original update with the now-cached credential.

Storage (see the header of controller/cmd/controller/steamauth.go):

  • The password is encrypted at rest with AES-GCM; the key is HKDF-derived from the internal CA private key (so restoring data/ca + the Postgres dump restores decryptability — no separate key file).
  • The Steam Guard sentry blob is stored and mounted into every later SteamCMD run, so 2FA is a one-time ceremony per account.
  • The password is never logged and never sent back to the UI.

Managing stored accounts: GET /api/steam/accounts lists them (no secrets), DELETE /api/steam/accounts removes one. Use a throwaway account that owns only the games you host if you're uncomfortable storing your main account's credential.

Out-of-band admin (dashboard is broken)

A separate token-authed HTTP surface (/admin/v1/*) plus panelctl admin exist for repair when the UI is down — see the "Admin HTTP" section of the README. Auth is loopback bypass on the controller host, or the X-Panel-Admin-Token header with the token from <data-dir>/admin-token (auto-generated, mode 0600; panelctl admin token-show prints it).