Add native host management and role-based operator authentication

Introduce an outbound Python host agent, Debian installer, durable remote
operations, immutable game-file libraries, host-aware deployments and ports,
and occupancy-safe campaigns and draining. Native hosts do not require Komodo.

Add administrator, operator and viewer accounts, protected management routes,
secure sessions, password recovery, audit history and separate telemetry tokens.
Document first-admin setup, reporter migration and live-host acceptance checks.

Validation: 1,343 TypeScript tests and 39 agent tests passed, along with type
checking, the production build, compiled SSR/API smoke tests and administrator
recovery. Real Debian/Docker T5/T6 acceptance remains unrun.
This commit is contained in:
2026-10-03 11:42:36 -04:00
parent a2e2311aa9
commit e5c0fda1ed
127 changed files with 8485 additions and 657 deletions
+125
View File
@@ -0,0 +1,125 @@
# Users and access
Electric Cherry requires a user session for operator pages and management APIs.
HTTPS protects the connection; server-side role checks protect each operation.
There is no anonymous management mode or public registration.
## First administrator and upgrades
1. Generate a setup secret: `openssl rand -hex 32`.
2. Set `EC_SETUP_TOKEN` to that value in the controller environment (or Compose
`.env`), set `EC_PUBLIC_URL` to the external HTTPS URL, and start EC. For a
direct Node deployment, set `ORIGIN` to that same URL.
3. Visit EC. `/setup` asks for the setup token, a username, display name and
password. Only the holder of the token can create the first administrator.
4. Remove `EC_SETUP_TOKEN` after setup and restart/recreate the controller.
Setup remains closed once any user exists, even if the token is still set.
5. Use **Settings → Users** to create additional accounts. Share temporary
passwords privately; each user must change theirs on first login.
Existing installations become protected on upgrade. Existing game servers and
agent enrollment credentials continue working independently of browser login.
Preserve the current database when upgrading: accounts and sessions are stored
there alongside the fleet metadata. Never publish a blank setup page with a
guessable token. An unset setup token holds setup closed.
## Roles
| Capability | Viewer | Operator | Administrator |
| --- | --- | --- | --- |
| Fleet, metrics, matches and operation summaries | Yes | Yes | Yes |
| Raw configs, file diffs, logs and telemetry diagnostics | No | Yes | Yes |
| Deploy/start/stop/restart/rebuild/remove servers | No | Yes | Yes |
| Scripts, plugins, configs, campaigns, betas and collections | Read summaries | Manage | Manage |
| Host setup, enrollment, credentials, runtime preparation and game-file import/cache | Read status | Read status | Manage |
| Server keys and global settings/maintenance policies | No | No | Manage |
| Create users, change roles, disable accounts and reset passwords | No | No | Yes |
| Own password and sign out | Yes | Yes | Yes |
Operators can launch using server keys already saved by an administrator. They
cannot save keys indirectly through a launch request. An operator's content
permissions include executable scripts and plugins; assign that role only to
people trusted to operate the game fleet.
Permissions are checked on the server for API calls, normal pages and Svelte
page-data requests. Hiding UI controls is a convenience, not the access boundary.
The controller prevents disabling or demoting the last enabled administrator.
Accounts are disabled rather than deleted so history retains their identity.
## Passwords, sessions and recovery
Passwords are 15–128 characters and stored using salted scrypt hashes. Temporary
passwords created or reset by administrators must be changed before any fleet
access. Password changes require the current password.
Sessions use random tokens in HTTP-only cookies, with only token hashes stored in
SQLite. HTTPS uses a Secure `__Host-` cookie. Sessions expire after 12 hours, or
30 minutes without authenticated requests; background dashboard polling counts
as activity. Login replaces the existing browser session. Logout revokes it.
Changing a password, changing a role, disabling an account, or revoking that
user's sessions invalidates their existing sessions. Live event streams check
revocation every 15 seconds and then close.
Login, initial setup and password verification are rate-limited with persistent
15-minute windows. The database stores audit entries for login results, account
changes and successful management requests without recording passwords, tokens
or request bodies. Administrators can review recent entries in Users.
An administrator can reset another user's password from Users. If the only
administrator forgets their password, an operator with local controller/container
access can run the recovery command in an interactive terminal:
```sh
docker compose exec electric-cherry node scripts/reset-admin.mjs admin
# Direct Node deployment, using the correct database:
DB_PATH=./data/cherry.db node scripts/reset-admin.mjs admin
```
Replace `admin` with the existing administrator's username. The command prompts
without echoing the password, resets that enabled administrator only, revokes
their sessions and records the recovery. It does not expose a remote recovery
endpoint or accept passwords as command arguments.
## Agent and telemetry connections
Host agents retain enrollment/host-token authentication under `/api/agent/`.
Installer and release files remain public so a new machine can enroll. A browser
login never substitutes for a host credential, and host tokens cannot manage EC.
Game reporters must send a separate secret of at least 32 characters configured
as `EC_TELEMETRY_TOKEN`:
```http
POST /api/telemetry
Authorization: Bearer <telemetry-token>
Content-Type: application/json
```
`X-EC-Telemetry-Token` is also supported. Cookies, query parameters and JSON fields
cannot supply it. A missing server token disables ingestion; a missing or wrong
request token is rejected before parsing or recording the payload. Existing JSON
event fields are unchanged. Configure your uploaded reporter/plugin, or a trusted
relay if that reporter cannot set headers, before upgrading. Use HTTPS for the
reporter-facing endpoint. Rotating this shared token requires updating reporters.
## Proxy and local development
Keep Node's internal port behind your TLS proxy and configure `ORIGIN` correctly.
Management writes require an exact matching `Origin` header, including login,
JSON requests and multipart uploads. Do not configure a proxy to replace arbitrary
incoming origins with your trusted origin. Machine endpoints use their own token
checks and do not require a browser Origin.
Use the adapter's real client-address configuration only with a trusted proxy
that strips spoofed forwarding headers; login throttling otherwise uses the direct
peer address. Forwarded host or user headers do not authenticate users.
Local HTTP browser development requires `EC_AUTH_ALLOW_HTTP=true` and a loopback
hostname (`localhost`, `127.0.0.1` or `::1`). This uses a separate development
cookie. It never enables HTTP login for a LAN/public hostname. Agent HTTP
development remains separately controlled by `EC_AGENT_ALLOW_HTTP` and the agent's
loopback-only option.
The implementation follows OWASP's guidance for [authorization on every request](https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html#validate-the-permissions-on-every-request),
[password hashing](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#scrypt),
and [session management](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html).
+27 -5
View File
@@ -6,7 +6,18 @@ beta on that server.
## Game reporter protocol
Send JSON to `POST /api/telemetry` with `Content-Type: application/json`.
Set `EC_TELEMETRY_TOKEN` on the Electric Cherry controller to a dedicated random
secret of at least 32 characters (for example, generate 64 hexadecimal characters
with `openssl rand -hex 32`). Configure the same secret in each game reporter or
its trusted local relay. Use a production HTTPS URL or a reverse proxy that
terminates TLS; browser login cookies do not authorize game telemetry.
Send JSON to `POST /api/telemetry` with `Content-Type: application/json` and either
`Authorization: Bearer <EC_TELEMETRY_TOKEN>` or
`X-EC-Telemetry-Token: <EC_TELEMETRY_TOKEN>`. If both headers are present, they must
contain the same secret. Keep the secret out of URLs, payloads and browser code,
and use a different secret from host-agent credentials or account passwords.
The JSON event format is unchanged.
There are four events. Send them whether or not a beta is active.
| Event | Fields |
@@ -74,8 +85,10 @@ Send the final round and each player's final stats when the game ends.
Success returns HTTP 200 with `{ "ok": true, "telemetry": { ... } }`.
The returned telemetry contains the event and an Electric Cherry `receivedAt`
timestamp. Invalid JSON or fields return 400, an unknown/destroyed server 404,
and a non-JSON content type 415.
timestamp. A missing/invalid machine token returns 401; an unset or invalid
controller `EC_TELEMETRY_TOKEN` returns 503. Authentication happens before payload
validation. Authenticated invalid JSON or fields return 400, an unknown/destroyed
server 404, and a non-JSON content type 415.
The script sends each event once, in gameplay order, directly to Electric Cherry.
There is no discovery request, beta token, boot/session ID, sequence number,
@@ -93,7 +106,12 @@ of beta activity. That page documents cumulative stat accounting, round credit,
and the limits of identifying matches from this protocol.
The game scripts are uploaded artifacts, not source maintained in this
repository. Their reporter should emit the four payloads above.
repository. Their reporter should emit the four payloads above and add the
authentication header. Existing anonymous reporters must be updated before
telemetry is accepted; if a reporter cannot set HTTP headers, configure a trusted
host-local relay to add the header. This repository does not bundle that relay or
assume a particular GSC HTTP implementation. Rotating the shared secret requires
updating the controller environment and all reporters/relays together.
### Telemetry diagnostics
@@ -102,7 +120,9 @@ Open a server's **Diagnostics → Telemetry API requests** to inspect incoming
name. Each entry includes the Electric Cherry receipt time, HTTP result, error
reason, event, map, round, player count, content type, and submitted JSON. Rejected
reports are included, even when the content type is wrong but the body is readable
JSON. Processing failures return 500 and record the internal error when diagnostic
JSON after authentication. Unauthorized requests are rejected before their bodies
are read and are not retained in per-server diagnostics. Authentication headers
and their secrets are never stored. Processing failures return 500 and record the internal error when diagnostic
storage is available; failed database transactions do not update the scoreboard.
The panel refreshes every five seconds while visible and has a manual refresh
@@ -118,6 +138,8 @@ newest first, with caching disabled.
Check the reporter's destination and server field. Calls that never reach
Electric Cherry, malformed JSON, and bodies without a usable server name
cannot appear in this server's log. Earlier requests are not backfilled.
Missing/invalid telemetry credentials also produce no retained request; inspect
the reporter's HTTP result for 401 or an unconfigured-controller 503.
- **400 / 415:** inspect the error and payload. One invalid player's stats or a
duplicate GUID rejects the entire report. These requests do not update the
scoreboard.
+174
View File
@@ -0,0 +1,174 @@
# Native host management
Electric Cherry controls Docker/Plutainer through an outbound host agent. A native
instance never uses Komodo or a filesystem mounted into the controller. The
controller owns desired state, campaigns, collections, safety queues and history;
the agent owns local execution and its durable operation journal.
## Install and launch
1. Run the controller with persistent `/data` storage. Set `EC_PUBLIC_URL` to its
HTTPS origin and proxy TLS to port 3000. Set a random `EC_SETUP_TOKEN` and create
the first administrator at `/setup`. Management pages/APIs require user login
and roles; agent endpoints authenticate their own host credentials. See
[Users and access](authentication.md) for setup and password recovery.
2. Open **Hosts → Add Host**, choose a name, local game-port range and bind address,
and generate an enrollment. Run the displayed command on Debian 12/13 amd64.
3. The installer validates the platform, installs Python 3 and an absent Docker
installation, verifies the downloaded agent SHA-256 over HTTPS, enrolls it and
enables `electric-cherry-agent.service`. It needs no Git, Node.js or local build.
4. In **Hosts**, prepare a Plutainer runtime image. In **Game Files**, import your
existing `t5_game_files` or `t6_game_files` Docker volume on an enrolled source
host. Name the library and version and record its DLC metadata.
5. Cache the published version on each destination host. Wait for verification.
Hosts report connection and readiness separately; deployment also checks the
particular library, image, capacity and ports you selected.
6. Launch a server or collection. Select the host, immutable game-file version(s)
and runtime image. EC selects lobby IDs and reserves UDP/TCP ports atomically.
Saved Plutonium server keys remain associated with their exact server names.
A fresh destination host downloads centrally stored game files without manual
copying or per-container setup. EC does not supply the game content itself.
The default host root is `/var/lib/electric-cherry`; installer `--data-dir` changes
it. Data is separated into `libraries/<game>/<version>/content`,
`instances/<UUID>/<generation>/app`, and `agent/operations.sqlite3`. Base libraries
are mounted read-only and instance deletion removes only that instance's files.
The controller stores large files under `GAME_FILES_ROOT` (default beside the
SQLite database), with manifests/checksums and operation metadata in SQLite.
The agent uses Plutainer's documented [game-file and app mounts](https://github.com/Ayymoss/Plutainer/blob/main/docs/volumes-and-configs.md).
Its log reader supports relative log links that resolve inside the instance app
directory, while file writes reject symlinks and paths outside that directory.
## Identity, transport and credentials
Enrollment tokens are random, hashed at rest, single-use and expire after 15
minutes. Consumption and credential issuance are transactional. Enrollment only
works at the enrollment endpoint. Each host receives a distinct bearer credential
and the controller only stores its hash. Host and enrollment bearer credentials
are rejected outside `/api/agent/`.
The agent uses HTTPS, operating-system certificate validation, outbound heartbeats
and polling; Docker has no exposed remote API. Agent endpoints reject HTTP unless
`EC_AGENT_ALLOW_HTTP=true` is explicitly enabled for development. The agent's
insecure development option is limited to loopback. Public release assets contain
no credentials. The release checksum is authenticated by the controller's HTTPS
origin; there is no separate offline release-signing infrastructure.
Browser sessions do not authorize agents. Similarly, host credentials never grant
access to operator pages. Game telemetry uses a separate `EC_TELEMETRY_TOKEN`,
configured in each reporter's authorization header; see the
[telemetry contract](beta-test-plans.md#game-reporter-protocol).
Hosts offers credential rotation and revocation. Rotation persists the new secret
on the agent before switching controller credentials, so a lost response can be
retried using the same proposed secret. Revocation affects one host only and does
not stop its running game containers. Re-enrollment requires a fresh token and an
explicit installer `--reenroll` when local credentials already exist.
API outline:
- `POST /api/agent/enroll`: consume enrollment token and return host credentials.
- `POST /api/agent/heartbeat`: authenticated inventory, current operation and caches.
- `GET /api/agent/operations`: deliver/reconcile durable operation IDs.
- `POST /api/agent/operations/:id`: authenticated acceptance, progress or result.
- `/api/agent/imports/...` and `/api/agent/libraries/...`: host-scoped uploads and
authenticated game-file downloads.
The operation envelope is independent of polling and can be carried over a
persistent transport later. No unrestricted shell operation is available.
## Safety and durable operations
Each remote request has an immutable ID and payload, plus an instance UUID and
generation where applicable. Reusing a key with a different request is rejected.
The controller records remote IDs before dispatch. The agent persists accepted,
running, completed, failed and unknown states in SQLite. Lost acknowledgments
return the recorded result; restarting the controller never creates a new
mutation simply because the previous reply was lost.
The agent reconciles ambiguous results using owned Docker labels, generations and
persisted execution checkpoints when those observations prove the outcome.
Otherwise the result remains **Unknown** and holds dependent work. Running alone
is insufficient evidence that a particular restart happened. EC neither assumes
an unreachable server stopped nor launches a duplicate replacement elsewhere.
Campaign bytes are staged and verified while players remain connected. Only
Provisioning can dispatch activation. It checks source eligibility and shared
capacity, and the agent repeats a fresh local RCON occupancy check immediately
before activation/restart, stop, removal or rebuild. Busy or unverifiable lobbies
wait indefinitely, including multi-hour Zombies sessions. Before images returned
by the agent populate existing campaign/beta rollback history. A beta cannot be
closed while its remote activation receipt is still unresolved.
Rebuild uses a new generation, fresh instance storage and the latest promoted
configuration/scripts/plugins. It retains the selected game-file version and
runtime image. Old generation commands cannot target the replacement. Start,
restart and create complete only after post-operation health evidence; creation
and rebuild also verify the selected map and running Plutonium revision. The
base-game library and runtime revision are independent records.
**Drain** blocks new deployments and queues safe retirement of existing lobbies.
Active beta ownership and other operations continue to gate retirement. Players
may still join until an eligible empty moment; draining does not kick them.
**Disable** holds agent dispatch. Neither mode, controller downtime nor agent
communication loss stops unrelated running containers.
## Libraries and ports
Imports scan the source volume and generate per-file SHA-256 manifests. Uploads
stream to temporary controller storage, are bounded by their manifest sizes and
verified before atomic publication. Published versions cannot be updated;
changing files requires a new version. Hosts download with HTTP Range resumption,
report progress, verify all checksums and atomically promote the complete cache.
Corrupt or incomplete content never becomes Ready. Cache inventory is reconciled
on heartbeats, including vanished caches. Older immutable versions remain usable
when their exact manifest is verified.
Reservations include host ID, local bind address, protocol and port. Wildcard
binds conflict with concrete addresses on the same host. UDP and TCP reservations
for one launch commit together; the same number can be used independently on
another host. Optional public/NAT address and port are recorded separately and do
not change local Docker bindings. EC does not configure external NAT/firewalls.
## Development and validation
```sh
npm run agent:package
npm run check
npm test
npm run test:agent
npm run build
```
The build packages `agent/ec_agent.py` and `agent/install.sh` into the static
release, generates its checksum manifest and serves those same assets in the
Node-adapter image. The Python agent uses only its standard library.
Automated tests cover enrollment/authentication, rotation/revocation, host
readiness, scoped ports, immutable library publication, streaming/ranges,
checksums, interrupted downloads, instance generation ownership, local occupancy
holds, controller restart/reconnect, ambiguous results, occupied campaign staging,
activation before images, collections and draining. Docker/UDP and host
observations are simulated in these tests.
A real Debian/Docker/Plutonium acceptance run is still required before production
rollout. This workspace has no Docker engine or Debian game host, so automated
coverage is not evidence that a real T5/T6 server has been deployed here. Run:
- Installer on Debian 12 and 13 amd64, then rerun without changing identity/data.
- T5 and T6 deployments and a mixed collection from a fresh destination host.
- Start, stop, restart, rebuild, destroy; verify RCON, map and runtime revision.
- Read console and game logs, including Plutainer's relative game-log links.
- File campaign and rollback, with a player joining between controller check and
agent activation, and during file activation before restart; verify that the
agent holds disruption.
- Drain occupied lobbies and verify shared libraries survive their retirement.
- Corrupt/interrupted library downloads, resumed transfer and verified promotion.
- Controller restart, agent restart and network loss before execution, during
execution and after execution but before acknowledgement. Verify the same
operation IDs and absence of duplicate containers/restarts.
Legacy backend code remains for existing database registrations. New native
launches do not require its configuration, API, network or mounted directories.