Operators can launch any Docker image cached on a host, not just admin-prepared runtimes #93

Closed
opened 2026-10-09 15:31:06 -04:00 by goddard · 2 comments
Collaborator

Operators can launch a server from any Docker image already present on a host, not only runtimes an admin prepared. The docs and the code comment both say custom runtimes are admin-only.

  • native.ts:68-75 accepts any non-default image found in host.inventory.runtimeImages. Its comment says custom runtimes "must be prepared by an admin from the host page".
  • ec_agent.py:1465-1479 fills runtimeImages from docker image ls, which lists every image on the host. Admin pulls are tracked separately in runtime-images.json (ec_agent.py:2601-2604), but only merged into the same list (ec_agent.py:1480-1483).
  • guard.ts:84 only checks operator launches for keys, and instance.create accepts any well-formed image (ec_agent.py:1620-1622).

On a host that also runs other workloads, an operator can start any of those images with a pool server key and the RCON password in its environment (launch-worker.ts:37-44) and published ports. This contradicts authentication.md:40, which makes runtime preparation admin-only. Viewers can also see each host's full local image list through /api/hosts.

Fix

  • Report admin-prepared images (runtime-images.json plus the configured default) as their own inventory field
  • Check that field instead of runtimeImages in native.ts:71-73 and availability.ts:125
  • Add a test that rejects an image cached locally but never pulled through runtime.pull
Operators can launch a server from any Docker image already present on a host, not only runtimes an admin prepared. The docs and the code comment both say custom runtimes are admin-only. - `native.ts:68-75` accepts any non-default image found in `host.inventory.runtimeImages`. Its comment says custom runtimes "must be prepared by an admin from the host page". - `ec_agent.py:1465-1479` fills `runtimeImages` from `docker image ls`, which lists every image on the host. Admin pulls are tracked separately in `runtime-images.json` (`ec_agent.py:2601-2604`), but only merged into the same list (`ec_agent.py:1480-1483`). - `guard.ts:84` only checks operator launches for keys, and `instance.create` accepts any well-formed image (`ec_agent.py:1620-1622`). On a host that also runs other workloads, an operator can start any of those images with a pool server key and the RCON password in its environment (`launch-worker.ts:37-44`) and published ports. This contradicts `authentication.md:40`, which makes runtime preparation admin-only. Viewers can also see each host's full local image list through `/api/hosts`. **Fix** - [x] Report admin-prepared images (`runtime-images.json` plus the configured default) as their own inventory field - [x] Check that field instead of `runtimeImages` in `native.ts:71-73` and `availability.ts:125` - [x] Add a test that rejects an image cached locally but never pulled through `runtime.pull`
goddard added the
priority
low
type/security
labels 2026-10-09 15:31:06 -04:00
josh was assigned by goddard 2026-10-09 15:31:06 -04:00
Author
Collaborator

Reviewed 596b160be3. Launches now accept only runtimes an admin prepared, and the full Docker image list is no longer shown to anyone, so I'm closing this.

Checklist

  • Report admin-prepared images as their own field. Agent 1.8.6 reports preparedRuntimeImages: the references runtime.pull recorded in runtime-images.json whose image IDs are still cached (ec_agent.py:1488). runtimeImages still lists the whole Docker cache. On the controller, parseHostInventory (request-context.ts:21-38) removes runtimeImages from every host it returns. Its only use of that list is to add the configured default if it's cached, so older agents can still run the default but can't approve a custom runtime. The packaged static/agent/ec-agent.py matches the source, and the manifest is at 1.8.6.
  • Check that field in native.ts and availability.ts. nativeLaunchSelection (native.ts:147) and automatic placement (availability.ts:201) now check preparedRuntimeImages. So do requireReadyHost, host readiness, preparation, capacity, system status and the host picker in NativeHostSelection.svelte. Nothing outside request-context.ts reads runtimeImages any more.
  • Test that a cached but never-pulled image is rejected. runtime-image-policy.test.ts rejects an unprepared custom image at POST /api/servers, whether it's cached, placed automatically or on a legacy agent. It also covers saved previews, collections, requireReadyHost on a host that only has the image cached, and malformed stored fields. test_agent.py shows that an unrelated cached image never appears in preparedRuntimeImages, that a runtime.pull adds only the reference it pulled, and that the list is empty when the records or Docker are unavailable.

Review notes

  • Good extra catch: a new test shows that the host list, host detail and host page responses no longer include unrelated cached images. That covers the side note in this issue about viewers seeing every host's image list.
  • The 6 runtime and host test files pass locally (124 tests), and so do the agent tests (287). CI run 842 is green after a re-run. Its first attempt hit the unrelated timeout now tracked in #102.
  • Minor and non-blocking: the agent counts an alias as prepared while its recorded image ID is still cached anywhere, but container create uses the alias itself (ec_agent.py:1917). If another workload on the host pulls the same tag later, the tag points at the new image, the old ID stays cached as an untagged image, and the alias still counts as prepared. Comparing the alias's current ID with the recorded one, or creating containers from the recorded ID, would close that.
Reviewed 596b160be36b36a64d9148255dc2cd15736a9b0b. Launches now accept only runtimes an admin prepared, and the full Docker image list is no longer shown to anyone, so I'm closing this. **Checklist** - [x] **Report admin-prepared images as their own field.** Agent 1.8.6 reports `preparedRuntimeImages`: the references `runtime.pull` recorded in `runtime-images.json` whose image IDs are still cached (`ec_agent.py:1488`). `runtimeImages` still lists the whole Docker cache. On the controller, `parseHostInventory` (`request-context.ts:21-38`) removes `runtimeImages` from every host it returns. Its only use of that list is to add the configured default if it's cached, so older agents can still run the default but can't approve a custom runtime. The packaged `static/agent/ec-agent.py` matches the source, and the manifest is at 1.8.6. - [x] **Check that field in `native.ts` and `availability.ts`.** `nativeLaunchSelection` (`native.ts:147`) and automatic placement (`availability.ts:201`) now check `preparedRuntimeImages`. So do `requireReadyHost`, host readiness, preparation, capacity, system status and the host picker in `NativeHostSelection.svelte`. Nothing outside `request-context.ts` reads `runtimeImages` any more. - [x] **Test that a cached but never-pulled image is rejected.** `runtime-image-policy.test.ts` rejects an unprepared custom image at `POST /api/servers`, whether it's cached, placed automatically or on a legacy agent. It also covers saved previews, collections, `requireReadyHost` on a host that only has the image cached, and malformed stored fields. `test_agent.py` shows that an unrelated cached image never appears in `preparedRuntimeImages`, that a `runtime.pull` adds only the reference it pulled, and that the list is empty when the records or Docker are unavailable. **Review notes** - Good extra catch: a new test shows that the host list, host detail and host page responses no longer include unrelated cached images. That covers the side note in this issue about viewers seeing every host's image list. - The 6 runtime and host test files pass locally (124 tests), and so do the agent tests (287). CI [run 842](https://gitea.thewrightserver.net/josh/electric-cherry/actions/runs/842) is green after a re-run. Its first attempt hit the unrelated timeout now tracked in #102. - Minor and non-blocking: the agent counts an alias as prepared while its recorded image ID is still cached anywhere, but `container create` uses the alias itself (`ec_agent.py:1917`). If another workload on the host pulls the same tag later, the tag points at the new image, the old ID stays cached as an untagged image, and the alias still counts as prepared. Comparing the alias's current ID with the recorded one, or creating containers from the recorded ID, would close that.
Author
Collaborator

Reviewed d1f581d6a8. This follow-up closes the retagging gap from my last review, and the three fix items are still done, so this stays closed.

Checklist

  • Report admin-prepared images as their own field. Unchanged. preparedRuntimeImages still comes from the runtime.pull records (ec_agent.py:1488).
  • Check that field in native.ts and availability.ts. Unchanged by this commit.
  • Test that a cached but never-pulled image is rejected. Unchanged, and still passing.

Review notes

  • Good extra catch: validate_spec now reads the image's current ID (ec_agent.py:1646) and rejects the launch when it differs from the ID runtime.pull recorded (ec_agent.py:1658). The container is then created from that ID instead of the tag (ec_agent.py:1929), so a retag between validation and creation can't swap the image either. Rebuilds run the same check before the old container is stopped (ec_agent.py:2034, 2044). The configured default with no pull record is still allowed, as the controller expects. Nothing reads a container's image reference: host removal compares the container's Image field (ec_agent.py:869), which Docker always reports as an ID.
  • New agent tests cover a retagged image being rejected and then accepted after a new pull, a retag after validation, a rebuild that leaves the running container alone, and invalid IDs or records. native-hosts.md:256-260 documents the behaviour. The packaged agent matches the source, and its SHA-256 matches the 1.8.7 manifest.
  • The agent tests pass locally (291), and CI run 844 on the current HEAD is green.
  • Minor and non-blocking: the inventory still lists a retagged runtime as prepared (ec_agent.py:1488, and the new test asserts this). So the host page shows it as ready, and automatic placement (availability.ts:201) can still pick that host. The launch then fails on the agent with "Prepared runtime image has changed". Comparing each alias's current ID from the image ls output (ec_agent.py:1474) with the recorded one would keep the inventory in line with the launch check.
Reviewed d1f581d6a80db131016025f2bf2c1cbad168bf71. This follow-up closes the retagging gap from my last review, and the three fix items are still done, so this stays closed. **Checklist** - [x] **Report admin-prepared images as their own field.** Unchanged. `preparedRuntimeImages` still comes from the `runtime.pull` records (`ec_agent.py:1488`). - [x] **Check that field in `native.ts` and `availability.ts`.** Unchanged by this commit. - [x] **Test that a cached but never-pulled image is rejected.** Unchanged, and still passing. **Review notes** - Good extra catch: `validate_spec` now reads the image's current ID (`ec_agent.py:1646`) and rejects the launch when it differs from the ID `runtime.pull` recorded (`ec_agent.py:1658`). The container is then created from that ID instead of the tag (`ec_agent.py:1929`), so a retag between validation and creation can't swap the image either. Rebuilds run the same check before the old container is stopped (`ec_agent.py:2034`, `2044`). The configured default with no pull record is still allowed, as the controller expects. Nothing reads a container's image reference: host removal compares the container's `Image` field (`ec_agent.py:869`), which Docker always reports as an ID. - New agent tests cover a retagged image being rejected and then accepted after a new pull, a retag after validation, a rebuild that leaves the running container alone, and invalid IDs or records. `native-hosts.md:256-260` documents the behaviour. The packaged agent matches the source, and its SHA-256 matches the 1.8.7 manifest. - The agent tests pass locally (291), and CI [run 844](https://gitea.thewrightserver.net/josh/electric-cherry/actions/runs/844) on the current HEAD is green. - Minor and non-blocking: the inventory still lists a retagged runtime as prepared (`ec_agent.py:1488`, and the new test asserts this). So the host page shows it as ready, and automatic placement (`availability.ts:201`) can still pick that host. The launch then fails on the agent with "Prepared runtime image has changed". Comparing each alias's current ID from the `image ls` output (`ec_agent.py:1474`) with the recorded one would keep the inventory in line with the launch check.
Sign in to join this conversation.