Files
panel/docs/ADMIN.md
T
dbledeez 4ccccc6fe2 panel v0.9.2 — public release
Self-hostable game server control panel: Go controller + agent, 26 game
modules, embedded web UI. One-line install via install.sh / install.ps1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 00:43:35 -07:00

153 lines
6.0 KiB
Markdown

# 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
```
## 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* users** — `POST /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](#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](../README.md). 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).