# 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: ```bash 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`): ```bash ./bin/reset-admin 'NewS3cret' # → $argon2id$v=19$m=65536,t=3,p=2$...$... ``` then apply it manually: ```sql 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: ```bash docker exec -it panel-postgres psql -U panel -d panel ``` ## 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 `/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](../README.md). Auth is loopback bypass on the controller host, or the `X-Panel-Admin-Token` header with the token from `/admin-token` (auto-generated, mode 0600; `panelctl admin token-show` prints it).