An empty [[admin]] list leaves the admin API open — the one place ER inverts secure-by-default #54
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Filed because this is a known, deliberate exception that had no issue of its own, so it was only recorded in conversation.
middleware.rsapplies the admin host/IP check only when[[admin]]has entries:With no
[[admin]]block the check is skipped entirely and the admin API — including the dashboard — is reachable from any host that can connect, with API-key auth as the only control. Every other resource in ER treats an empty rule list as deny-all; this is the single inversion of that rule, and CLAUDE.md's "secure by default … an empty rule list is a deny-all, not a no-op" is therefore not true of admin.It became deliberate with the v1.0.1 dashboard fix, so flipping it is a breaking change for anyone relying on the current behaviour.
Compounding factors:
[[admin]]entry withallow_ips = ["127.0.0.1/32", "::1/128"]— opt-in, and not obvious.docker-compose.ymlsetsALLOWED_ADMIN_HOSTS, which nothing reads (#12), so an operator has a visible-but-inert control that reads like it closes this.Options, roughly in order of how much they change:
[[admin]]is absent. Breaking for remote-dashboard users, but matches every other resource and is the behaviour the docs already imply.[[admin]]block required to enable the admin API at all, so absence means off rather than open.v2.0.0 is already breaking, so if this is to change, riding along with it is cheaper than a later major. Not doing it unprompted — it is a posture decision, not a defect fix, and it affects anyone currently reaching the dashboard remotely.
This issue is wrong and I am closing it. Raised by Roku; verified against
df35d28.The premise was that
middleware.rs'sif !admin_entries.is_empty()skips the host/IP check, leaving the admin API reachable from anywhere with only API-key auth. That reads one middleware in isolation. The actual behaviour:admin_host_gate(src/middleware.rs:88) wraps all three API routers —admin_routes,service_routesandsession_routes(src/main.rs:106). It computesadmin_entries.iter().any(|e| e.host == host), andany()over an empty list isfalse, so with no[[admin]]entries every API request is handed toproxy_handlerinstead. The API, the dashboard,/healthand/readyare simply not served.That makes the
is_empty()branch inadmin_ip_middlewareunreachable for the no-entries case: the host gate has already refused. With entries present, the middleware finds the matching one and enforcesaccess_allowed, which returns false for an empty rule list — so an[[admin]]entry carrying no[[admin.access]]rule is deny-all, consistent with every other resource rather than an exception to it.So ER does not invert its secure-by-default rule here. An
[[admin]]entry is what enables the API; its access rules are what restrict it; absent either, nothing is served.Where the claim came from: a note written on 2026-10-20 that was carried forward and repeated — in this issue, in the v2.0.0 release notes, and in two messages to other sessions — without re-reading
admin_host_gate. The release notes have been corrected. A memory note asserting the open-admin behaviour has been deleted.What survives as real, and is tracked elsewhere:
admin_ip_middleware— they require the admin Host and a key but not thevia/allow_ipsrestriction. Rescoped in #55./healthand/readybeing behind the admin Host gate leaves no unauthenticated-and-unrestricted liveness surface for a monitoring probe. Filed as #67.Closing as not a defect.