658bda1d24
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
130 lines
5.0 KiB
Markdown
130 lines
5.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
|
|
```
|
|
|
|
## 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).
|