Skip to main content

Security: the green endpoints required no session

Maintainers

GET /api/v1/green/summary, /green/budgets and /green/report were registered without the session middleware that every other signal route uses. On an install with authentication enabled, all three answered 200 to a caller with no session at all.

What was exposed​

For any project named in the request header, without signing in:

  • per-service energy in watt-hours and carbon in gCO2e,
  • monthly carbon budget usage,
  • the CSRD-ready report export.

The routes are read-only, so nothing could be changed through them. But the data is a fair map of what an estate runs and how hard it works — service names, relative load, and the shape of a workload over time.

Nothing else was affected: every other signal route — traces, logs, metrics, service map, health, alerting, error tracking, infrastructure — was already behind the session middleware and enforcing per-project grants.

Why it happened​

The routes were wired with the bare handler wrapper rather than the guarded one, so no identity was ever attached to the request. The per-project scope check then treats "no identity" as "authentication is switched off" — the branch that exists so an auth.enabled=false install keeps working — and allowed the request through.

Am I affected?​

You are if all of these hold:

  • the green module is enabled (it is off by default, so an install that never turned it on was never exposed),
  • authentication is enabled, and
  • the hub's API is reachable by someone you would not grant a viewer role.

An install where the hub is only reachable inside the cluster was not exposed to anyone who could not already reach far more.

The fix​

All three routes now require the viewer role and honour per-project grants, exactly like every other signal. Upgrade to a build containing this change.

A test now asserts that every project-data route answers 401 without a session, rather than checking them one at a time — the gap survived because nothing had ever asserted over the whole set. It was found while indexing every route's guard to build a permissions view for Settings, which is the same idea applied to the product: derive what the rules are from what the server enforces, instead of writing them down twice.