What happened

GET /shutdown called exitProcess(0) after a short delay, terminating the whole server process. It sat behind authenticate("auth-api-key"), so it required the shared cluster bearer token — but as a side-effecting GET, it violated the basic REST safety guarantee that GET requests don’t change server state. Found during the public-demo exposure audit (krill#915/#916); demo mode already hard-blocks the route, but a normal (non-demo) server still exposed it.

Root cause

Mutating operations were modeled as plain GET routes instead of POST, so anything that can induce a GET against the server with the right credential attached — a browser prefetch, a crawler following a stray link, a same-origin script — could trigger a full process exit with no confirmation step. The route also had no consumer anywhere in composeApp/androidApp/iosApp or krill-oss (krill-mcp operates on node CRUD via the authenticated API, not a raw HTTP shutdown probe), and systemctl stop krill already covers the operator use case the route was meant to serve.

Fix

Prevention