Require signed agent releases (Refs #64)

This commit is contained in:
2026-10-09 20:36:18 -04:00
parent df0d395138
commit 6f71822d41
24 changed files with 984 additions and 107 deletions
+21 -3
View File
@@ -9,6 +9,8 @@ HTTPS. Running game containers continue through controller or agent outages.
- A controller machine with Docker Compose and an HTTPS reverse proxy.
- Debian 12 or 13 amd64 game hosts with storage for game files and instances.
- A signed agent release, plus its publisher's public key and reviewed installer
obtained through a trusted channel independent of the controller.
- Your complete T5 or T6 game folders, including the DLC you intend to host.
- Plutonium server keys for each game, with one unused key per new concurrent lobby.
@@ -37,6 +39,13 @@ For an existing installation, retain its original data directory. Compose
defaults to `/root/zombies/.electric-cherry`; choose a dedicated directory for a
new controller. See [.env.example](../../.env.example) for the other settings.
Before building for game hosts, obtain the publisher's signed `static/agent`
artifacts and matching source revision, or follow
[Publishing signed releases](native-hosts.md#publishing-signed-releases)
for your own build. Normal builds preserve signatures for identical artifacts;
source changes require a new offline signature. An unsigned controller can run,
but hosts reject its agent installation and updates.
```sh
docker compose up -d --build
```
@@ -60,15 +69,24 @@ password recovery.
1. Open **Releases → Game files** and upload the complete local folder for T5 or
T6, including DLC. Uploads can run before any host is enrolled. The latest
verified upload becomes that game's default files.
2. Open **Fleet → Hosts → Add Host**, configure the host's game-port range and
bind address, and run the generated enrollment command on the host.
3. Wait for automatic setup to prepare the Plutainer runtime and available game
2. [Provision release trust](native-hosts.md#provision-release-trust): independently
verify the publisher key's full fingerprint, install its binary public key at
`/etc/electric-cherry/release-signing.gpg`, and copy the reviewed installer to
`./electric-cherry-install.sh` on the host. Both come from the trusted publisher.
3. Open **Fleet → Hosts → Add Host**, configure the host's game-port range and
bind address, and run the generated enrollment command from the directory
containing that local installer. Do not pipe controller-downloaded code into
a root shell.
4. Wait for automatic setup to prepare the Plutainer runtime and available game
files. The host page reports download, verification, and readiness progress.
The installer sets up the Python agent as a systemd service and enables automatic
agent updates. It installs Docker if needed. Multiple instances share read-only
base game files; each instance has its own writable data. See
[Native hosts](native-hosts.md) for storage layout, network access, and recovery.
Existing hosts using updater protocol 1 need a
[trusted installer migration](native-hosts.md#automatic-agent-updates) before
controller-independent release verification takes effect.
## Launch your first server
+152 -34
View File
@@ -12,9 +12,11 @@ the agent owns local execution and its durable operation journal.
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 and updater SHA-256 over HTTPS,
and generate an enrollment. First [provision release trust](#provision-release-trust)
and copy the reviewed installer to `./electric-cherry-install.sh` on the Debian
12/13 amd64 host. Then run the displayed command from that directory.
3. The installer validates the platform, installs Python 3, `gpgv` and an absent Docker
installation, verifies the release signature and agent/updater SHA-256 checksums,
enrolls it and enables `electric-cherry-agent.service` and its automatic update
timer. It needs no Git, Node.js or local build.
4. In **Game Files**, upload your complete local folder under T5 or T6. Include
@@ -41,6 +43,37 @@ the agent owns local execution and its durable operation journal.
A fresh destination host downloads centrally stored game files without manual
copying or per-container setup. EC does not supply the game content itself.
### Provision release trust
Obtain the release publisher's public key and reviewed installer through a
trusted channel independent of the controller. Verify the full public-key
fingerprint with the publisher through that independent channel. On a trusted
administrator workstation with GnuPG, inspect the supplied binary public key:
```sh
gpg --show-keys --with-fingerprint ./release-signing.gpg
```
Transfer that verified public key and installer to the host, with the installer
named `electric-cherry-install.sh`. Provision the key before running enrollment:
```sh
sudo install -d -o root -g root -m 0700 /etc/electric-cherry
sudo install -o root -g root -m 0644 ./release-signing.gpg /etc/electric-cherry/release-signing.gpg
```
The keyring and every parent directory must be owned by root, must not be
symlinks, and must not be writable by other users. Install only the publisher
keys you intend to trust. The generated enrollment command runs the local
installer with `sudo bash ./electric-cherry-install.sh`; never replace this with
a controller download piped into a root shell. An installer supplied by a
compromised controller could bypass its own verification. The host never obtains
or replaces its trust key through controller APIs.
If you build your own controller releases, act as the publisher using the
[signing workflow below](#publishing-signed-releases). Hosts reject unsigned
releases, including ordinary development builds that have not been signed.
### Docker installation
The installer uses Debian's Docker packages: `docker.io` on Debian 12, plus
@@ -169,15 +202,25 @@ directory, while file writes reject symlinks and paths outside that directory.
New installations enable automatic agent updates by default. Each host checks
its controller's release over HTTPS about every five minutes, beginning about
two minutes after boot, with up to 30 seconds of jitter. Updating the controller
therefore makes its compatible, newer agent release available to enrolled hosts.
therefore makes its compatible, newer signed agent release available to enrolled hosts.
There is no controller command to schedule and no update button to press.
**Existing hosts need the current installer once.** From the host's **Agent
access**, generate an installer command and rerun it on the same machine, with
the same controller and data directory. Do not use `--reenroll` for this upgrade.
The installer preserves host identity, credentials, game files and running
containers. Agent version 1.2.0 includes automatic update support; the host page
shows **Setup required** until a fresh heartbeat confirms the updater is installed.
**Existing hosts must migrate to updater protocol 2 with the trusted installer.**
Protocol 1 bootstraps trust controller-provided checksums and remain exposed to
controller-forged updates until this migration is complete. Publishing a signed
release does not upgrade those bootstraps: they request an installer rerun when
they see protocol 2, but a compromised controller could still serve protocol 1.
First provision release trust as described above. From the host's **Agent
access**, generate a command and run it using the reviewed local installer, with
the same controller and data directory. Preserve a custom root with `--data-dir`;
do not use `--reenroll` for this upgrade. To pause old automatic checks during
migration, run `sudo systemctl stop electric-cherry-update.timer`. This does not
interrupt a running update worker; let any active update or journal recovery
finish before installing. The installer preserves host identity, credentials,
game files and running containers, replaces the bootstrap, and enables its timer
on success. Hosts without an updater show **Setup required** until a heartbeat
confirms installation.
The **Automatic agent updates** panel reports the running version, target
version, last check, progress and any error. Offline hosts show their last
@@ -185,27 +228,91 @@ reported status. Deferred updates report why they are waiting: for example an
active agent operation, an unavailable controller or a newer updater bootstrap.
Checks also defer while the agent service is stopped; an update does not start a
service that was stopped outside the update workflow.
The independent updater holds the same execution lock, verifies the release
The independent updater holds the same execution lock, authenticates the exact
manifest bytes with the pinned keyring before parsing them, verifies the release
checksum, version and supported protocol, replaces agent code, and restarts the
agent service. It never restarts Docker or game containers. It confirms an
authenticated heartbeat from the new agent process before accepting the update;
failure restores the previous agent code. Its durable journal permits recovery
after a host reboot or interrupted update.
The updater only installs newer compatible versions; rolling the controller back
does not downgrade agents. A rejected version/checksum pair stays blocked until
The updater only installs newer compatible versions; replaying an older signed
release or rolling the controller back does not downgrade agents. Signing cannot
force a controller to publish newer releases or prevent it withholding updates.
A rejected version/checksum pair stays blocked until
the controller publishes a different pair that is still newer than the installed
agent. Inspect the reported error when a host
shows **Needs attention**. The updater itself is a stable bootstrap component:
it changes only when the installer is rerun. If a future release requires an
unsupported updater protocol, rerun that release's installer on the host.
### Publishing releases and inspecting updates
### Publishing signed releases
When publishing changed agent code, bump its numeric `VERSION` in
`agent/ec_agent.py` and rebuild the controller. Packaging derives the manifest
version and checksums from the sources; an already installed version is never
replaced just because its bytes changed.
`agent/ec_agent.py`. Finalize and review all source changes before packaging and
signing. An already installed version is never replaced just because its bytes
changed.
Use a separate offline publisher machine with GnuPG. Keep the private key and its
GnuPG home outside the checkout, Docker build context, controller, and CI. For
example, create a dedicated signing key and inspect its full fingerprint:
```sh
ec_signing_home=/secure/electric-cherry-signing
install -d -m 0700 "$ec_signing_home"
gpg --homedir "$ec_signing_home" --quick-generate-key 'Electric Cherry Release Signing' ed25519 sign 2y
gpg --homedir "$ec_signing_home" --list-keys --with-fingerprint
```
Protect the key with a passphrase and keep its backup and revocation certificate
outside release artifacts. Set the full fingerprint of the signing key, then
export only its public key for independent host provisioning:
```sh
ec_signing_fingerprint=REPLACE_WITH_FULL_SIGNING_KEY_FINGERPRINT
gpg --homedir "$ec_signing_home" --export "$ec_signing_fingerprint" > release-signing.gpg
```
From the reviewed checkout on the publisher machine, package and explicitly sign:
```sh
npm run agent:package
python3 agent/sign_release.py --gnupghome "$ec_signing_home" --fingerprint "$ec_signing_fingerprint"
```
The signer defaults to `static/agent`; `--release-dir` selects another packaged
release directory. It checks that the manifest matches the agent, updater and
installer bytes, then produces the ASCII-armored detached `manifest.json.sig`.
The manifest records their SHA-256 hashes as `sha256`, `updaterSha256`, and
`installerSha256`, along with version, platform and protocol metadata. The
installer hash lets recipients check a publisher-distributed installer against
the authenticated manifest before running it.
Commit or distribute the finalized `static/agent` public artifacts together,
including the signature, before building the controller image from the same
reviewed source revision. Never include private keys or the signing home.
Normal `npm run build`, development startup,
and Docker builds never sign: packaging preserves a signature only when the
manifest remains byte-for-byte identical and removes it after any covered source
change. Repackage, review and sign again after such a change. Unsigned builds can
run the controller, but hosts fail closed and leave their installed code intact.
### Signing-key rotation and removal
Verify new fingerprints independently and replace each host's root-owned keyring
through trusted administration before publishing releases signed by a new key.
For a planned transition, provision both approved public keys temporarily, then
remove the retired key once every host can verify the new signer. The controller
cannot authorize key rotation.
Remove compromised or retired keys from every host's keyring. Publishing a
revocation certificate or allowing a key to expire is insufficient: [GnuPG's
`gpgv` documentation](https://www.gnupg.org/documentation/manuals/gnupg/gpgv.html)
states that it does not check key expiration or revocation. Host deletion removes
the pinned keyring with `/etc/electric-cherry`; provision trust again before
reenrolling that machine.
### Inspecting updates
The installer places the updater at
`/usr/local/libexec/electric-cherry-update.py` and enables
@@ -370,8 +477,10 @@ The agent uses HTTPS, operating-system certificate validation, outbound heartbea
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.
no credentials. HTTPS protects transport; an offline publisher signature over the
release manifest authenticates agent and updater code using the independently
provisioned host keyring. Controller credentials do not grant release-signing
authority.
Browser sessions do not authorize agents. Similarly, host credentials never grant
access to operator pages. Game telemetry uses a separate `EC_TELEMETRY_TOKEN`,
@@ -397,9 +506,11 @@ explicit installer `--reenroll` when local credentials already exist.
or `409` when the agent needs updating or unresolved ownership remains.
- `POST /api/agent/enroll`: consume enrollment token and return host credentials.
- `GET /api/agent/release/manifest.json`: public agent version, protocol and
checksum plus `updaterProtocolVersion` and `updaterSha256` for installer bootstrap.
checksum plus `updaterProtocolVersion`, `updaterSha256`, and `installerSha256`.
- `GET /api/agent/release/manifest.json.sig`: detached publisher signature over
the exact manifest bytes; absent for an unsigned build.
- `GET /api/agent/release/ec-agent.py` and `ec-agent-update.py`: public release
source, verified against that HTTPS manifest before installation.
source, verified against the signed manifest before installation.
- `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.
@@ -576,10 +687,11 @@ and invalidates unused installer links. A separate cleanup service removes the
host's downloaded game libraries, instance files, agent journal and configuration,
owned containers, and recorded runtime images unused by other containers. It
stops and removes the agent service and executable, automatic update timer,
update service, updater executable and update state, then removes its own files.
update service, updater executable, update state and pinned signing keyring, then
removes its own files.
The controller hides the host and revokes its ordinary credentials only after
receiving the final uninstall receipt. Adding the machine again requires a new
host enrollment.
host enrollment and independently reprovisioned release trust.
Game folders uploaded through Electric Cherry belong to the controller. Those
original uploads remain available for other hosts and future deployments; only
@@ -834,14 +946,18 @@ npm run build
```
The build packages the agent, standalone updater and installer into the static
release, generates the agent/updater checksum manifest and serves those same
assets in the Node-adapter image. Development serves the freshly packaged static
release even when an older production build exists. The Python programs use only
the standard library.
release, generates their checksum manifest and serves those same assets in the
Node-adapter image. Sign production artifacts using the
[offline workflow](#publishing-signed-releases) before building. Development
serves the freshly packaged static release even when an older production build
exists. The Python programs use only the standard library; signature verification
also requires the host's `gpgv` executable. Real signing tests require `gpg` and
`gpgv`, installed by CI; local runs skip those tests when either tool is absent.
Automated tests cover enrollment/authentication, rotation/revocation, host
readiness, scoped ports, immutable library publication, streaming/ranges,
checksums, interrupted downloads, instance generation ownership, local occupancy
signed manifests, forged checksums, unknown signing keys, interrupted downloads,
instance generation ownership, local occupancy
holds, controller restart/reconnect, ambiguous results, occupied campaign staging,
activation before images, collections and draining. Host deletion tests cover
ownership guards, interrupted cleanup, uninstall receipts, credential revocation
@@ -852,13 +968,15 @@ A real Debian/Docker/Plutonium acceptance run is still required before productio
rollout. Automated simulations do not establish successful deployment on real
game hosts. Run:
- Installer on Debian 12 and 13 amd64, then rerun without changing identity/data.
- Bootstrap an existing host with the current installer, then publish a newer
compatible agent release. Verify a timer-driven update, its authenticated
- Independently provision release trust and run the reviewed installer on Debian
12 and 13 amd64, then rerun without changing identity/data.
- Migrate a protocol 1 host with the trusted installer, then publish a newer
compatible signed agent release. Verify a timer-driven update, its authenticated
heartbeat and unchanged game containers. Exercise an active-operation hold,
bad checksum, failed startup/heartbeat rollback, controller downgrade, network
loss and host reboot during update. Verify updater protocol mismatch requests
an installer rerun.
missing/invalid signatures, an unknown key, a forged manifest matching changed
code, bad checksum, failed startup/heartbeat rollback, controller downgrade,
network loss and host reboot during update. Verify updater protocol mismatch
requests an installer rerun and test trusted keyring replacement for rotation.
- T5 and T6 deployments and a mixed collection from a fresh destination host.
- Warm the Plutonium runtime cache, then create and rebuild T5 and T6 lobbies;
verify the running revision and compare first-start downloads and launch times