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.
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.
GET /shutdown from server/.../Routes.kt entirely, along with
the scope: CoroutineScope parameter it was the sole user of in
configureSystemRoutes.ShutdownRouteRemovedTest (server/src/jvmTest/.../Routes package)
asserting the route string is gone from Routes.kt, so it can’t silently
come back./shutdown had no client consumer; the operator
path (systemctl stop krill) already existed. Removing it converts
“unused and dangerous” into “gone” rather than adding a same-host check
to preserve a surface nothing used — same call made for /krill-token
in krill#916.testApplication harness isn’t usable in :server:jvmTest today —
the pinned ktor-server-tests:2.3.13 test classpath throws
NoClassDefFoundError against this module’s ktor-server-core 3.4.x
application/environment API. Route-removal regressions in this module need
a source-level guard until that gap is closed.