A member raised this on Loomio. Nathan's read is that it's an ops decision rather than a governance one, so it lives here rather than as a poll. Filing the facts I verified live, so the discussion doesn't start from guesses.
What was asked
Whether gateway.inference.coop should send CORS exceptions for more apps — specifically, whether to open it to all origins (LITELLM_CORS_ORIGINS=*) so members can use browser-based apps we don't host, versus naming apps as needed.
What is configured today (verified 2026-10-30, not from memory)
LITELLM_CORS_ORIGINS=https://chat.inference.coop — one origin, not two.
Live preflight against https://gateway.inference.coop/v1/chat/completions:
Origin: https://evil.example → 400, no allow-origin header
Origin: null → 400 — correctly refused (null is the sandboxed-iframe / file:// origin and a standard bypass attempt)
Correction worth noting:chat-legacy.inference.coop is not in the list — it returns 400. It is also not an older LibreChat: it runs com.openwebui.cloudronapp (Open WebUI), a separate app that is up and serving. [#14] records that this env var was renamed (LITELLM_CORS_ALLOWED_ORIGINS → LITELLM_CORS_ORIGINS) with "re-verify CORS after the upgrade" as a follow-up, which is the likely origin of the belief that two origins are set. Open WebUI normally calls the gateway server-side, where CORS does not apply, so this is only a real gap if members are using its Direct Connections mode. Worth confirming rather than assuming.
The security reasoning (which I think is sound)
CORS is not an authentication boundary here. Requests carry a per-member bearer key; the browser does not attach anything implicitly.
A leaked key is curl-able regardless of the allow-list. If a member pastes a key into a third-party web app, that app can use it directly — an origin list does not change that.
What actually protects the co-op is per-key budgets, model scoping and revocation, all of which we already enforce. With *, the budget is the thing standing between a leaked key and an unbounded bill.
Most large providers do send permissive CORS. The notable exception: Anthropic requires an explicit anthropic-dangerous-direct-browser-access opt-in rather than opening up. So a narrow allow-list is a defensible choice, not an odd one.
The honest downside of opening it: it removes friction, which makes casual key-sharing more likely. That argues for a docs note, not for keeping the list closed.
If we open it — the change and its risks
One env var on the gateway app. Two things to know:
It restarts the app (a few seconds of gateway downtime). Doing it during the migration window costs nothing, since the box restarts anyway.
The gateway also sends access-control-allow-credentials: true, and * together with that is normally invalid in browsers — which would break chat if the middleware sent a literal *. I checked Starlette 1.3.1's CORSMiddleware in the running container: with allow_origins=["*"]and credentials enabled it echoes the requesting origin instead of sending *, which stays valid. So the change should work without breaking chat — but it must be re-verified, because that behaviour is a library implementation detail, not a contract.
Acceptance criteria (if we proceed)
Preflight from an unrelated origin returns 200 with access-control-allow-origin echoing that origin
chat.inference.coop still returns 200 with its origin echoed, and a real browser completion from chat still works
Noted, not necessarily fixed: null will also be echoed under * — reachable only by a page that already holds a key
A docs line stating that pasting a key into a third-party web app exposes it; use budgeted, revocable keys
chat-legacy.inference.coop explicitly added or its Direct-Connections use confirmed unnecessary
Options
Open to all origins (recommended): one var, matches mainstream providers, no real loss given bearer keys + budgets.
Keep it app-specific, adding chat-legacy. if that use is intended: least change; leaves members unable to use third-party browser apps.
Request-and-add process: middle ground, negligible security gain for ongoing ops cost.
Reversible either way, and small. [#14]: #14
[#24]: #24
A member raised this on Loomio. Nathan's read is that it's an **ops decision** rather than a governance one, so it lives here rather than as a poll. Filing the facts I verified live, so the discussion doesn't start from guesses.
## What was asked
Whether `gateway.inference.coop` should send CORS exceptions for more apps — specifically, whether to open it to **all origins** (`LITELLM_CORS_ORIGINS=*`) so members can use browser-based apps we don't host, versus naming apps as needed.
## What is configured today (verified 2026-10-30, not from memory)
`LITELLM_CORS_ORIGINS=https://chat.inference.coop` — **one** origin, not two.
Live preflight against `https://gateway.inference.coop/v1/chat/completions`:
- `Origin: https://chat.inference.coop` → **200**, `access-control-allow-origin: https://chat.inference.coop`
- `Origin: https://evil.example` → **400**, no allow-origin header
- `Origin: null` → **400** — correctly refused (`null` is the sandboxed-iframe / `file://` origin and a standard bypass attempt)
**Correction worth noting:** `chat-legacy.inference.coop` is **not** in the list — it returns 400. It is also not an older LibreChat: it runs `com.openwebui.cloudronapp` (Open WebUI), a separate app that is up and serving. [#14] records that this env var was *renamed* (`LITELLM_CORS_ALLOWED_ORIGINS` → `LITELLM_CORS_ORIGINS`) with "re-verify CORS after the upgrade" as a follow-up, which is the likely origin of the belief that two origins are set. Open WebUI normally calls the gateway **server-side**, where CORS does not apply, so this is only a real gap if members are using its *Direct Connections* mode. Worth confirming rather than assuming.
## The security reasoning (which I think is sound)
- **CORS is not an authentication boundary here.** Requests carry a per-member bearer key; the browser does not attach anything implicitly.
- **A leaked key is curl-able regardless of the allow-list.** If a member pastes a key into a third-party web app, that app can use it directly — an origin list does not change that.
- **What actually protects the co-op is per-key budgets, model scoping and revocation**, all of which we already enforce. With `*`, the budget is the thing standing between a leaked key and an unbounded bill.
- Most large providers do send permissive CORS. The notable exception: **Anthropic requires an explicit `anthropic-dangerous-direct-browser-access` opt-in** rather than opening up. So a narrow allow-list is a defensible choice, not an odd one.
- **The honest downside of opening it:** it removes friction, which makes casual key-sharing more likely. That argues for a docs note, not for keeping the list closed.
## If we open it — the change and its risks
One env var on the gateway app. Two things to know:
1. **It restarts the app** (a few seconds of gateway downtime). Doing it during the migration window costs nothing, since the box restarts anyway.
2. **The gateway also sends `access-control-allow-credentials: true`**, and `*` together with that is normally invalid in browsers — which would break chat if the middleware sent a literal `*`. I checked Starlette 1.3.1's `CORSMiddleware` in the running container: with `allow_origins=["*"]` **and** credentials enabled it **echoes the requesting origin** instead of sending `*`, which stays valid. So the change should work without breaking chat — but it must be re-verified, because that behaviour is a library implementation detail, not a contract.
## Acceptance criteria (if we proceed)
- [ ] Preflight from an unrelated origin returns 200 with `access-control-allow-origin` echoing that origin
- [ ] `chat.inference.coop` still returns 200 with its origin echoed, and a real browser completion from chat still works
- [ ] Noted, not necessarily fixed: `null` will also be echoed under `*` — reachable only by a page that already holds a key
- [ ] A docs line stating that pasting a key into a third-party web app exposes it; use budgeted, revocable keys
- [ ] `chat-legacy.inference.coop` explicitly added **or** its Direct-Connections use confirmed unnecessary
## Options
1. **Open to all origins** (recommended): one var, matches mainstream providers, no real loss given bearer keys + budgets.
2. **Keep it app-specific**, adding `chat-legacy.` if that use is intended: least change; leaves members unable to use third-party browser apps.
3. **Request-and-add process**: middle ground, negligible security gain for ongoing ops cost.
Reversible either way, and small. [#14]: https://git.inference.coop/co-op/support/issues/14
[#24]: https://git.inference.coop/co-op/support/issues/24
Implemented and verified on the gateway (image 1.0.0-1793467213663).
Why the earlier attempt didn't work, which is worth recording:start.sh in the app hardcoded export LITELLM_CORS_ORIGINS="https://chat.inference.coop". The export overrode the Cloudron app
env, so setting LITELLM_CORS_ORIGINS on the app read back correctly and did nothing. It now reads ${LITELLM_CORS_ORIGINS:-https://chat.inference.coop}, so the app env drives it and the old lock
remains only as a fallback. Thanks to @staeiou for raising it and to @bo for the reasoning on CORS
and cookies.
Decision: allow all origins. Set via the app env, and the comment in start.sh now says why —
CORS is not an auth boundary here; every request carries a per-member bearer key the browser never
attaches implicitly, so an origin list buys nothing against a party that already holds a key. The
control that matters is the per-key budget, model scoping and revocation.
Verified live (before → after):
Origin: https://some-other-app.example — HTTP 400, no allow-origin → 200, *
Origin: https://chat-legacy.inference.coop — HTTP 400 → 200, * (the Open WebUI app now works cross-origin, which closes that part of the report)
Origin: https://chat.inference.coop — HTTP 200 → 200, * (no regression)
On the credentials caveat: a bare * is invalid in browsers if Access-Control-Allow-Credentials
is also sent — and it is not sent now, so * is valid and chat keeps working. Had it been, this
change would have broken the existing setup rather than widened it.
Implemented and verified on the gateway (image `1.0.0-1793467213663`).
**Why the earlier attempt didn't work, which is worth recording:** `start.sh` in the app hardcoded
`export LITELLM_CORS_ORIGINS="https://chat.inference.coop"`. The `export` overrode the Cloudron app
env, so setting `LITELLM_CORS_ORIGINS` on the app read back correctly and did nothing. It now reads
`${LITELLM_CORS_ORIGINS:-https://chat.inference.coop}`, so the app env drives it and the old lock
remains only as a fallback. Thanks to @staeiou for raising it and to @bo for the reasoning on CORS
and cookies.
**Decision: allow all origins.** Set via the app env, and the comment in `start.sh` now says why —
CORS is not an auth boundary here; every request carries a per-member bearer key the browser never
attaches implicitly, so an origin list buys nothing against a party that already holds a key. The
control that matters is the per-key budget, model scoping and revocation.
**Verified live (before → after):**
- `Origin: https://some-other-app.example` — HTTP **400, no allow-origin** → **200, `*`**
- `Origin: https://chat-legacy.inference.coop` — HTTP **400** → **200, `*`** (the Open WebUI app now works cross-origin, which closes that part of the report)
- `Origin: https://chat.inference.coop` — HTTP **200** → **200, `*`** (no regression)
**On the credentials caveat:** a bare `*` is invalid in browsers if `Access-Control-Allow-Credentials`
is also sent — and it is *not* sent now, so `*` is valid and chat keeps working. Had it been, this
change would have broken the existing setup rather than widened it.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
A member raised this on Loomio. Nathan's read is that it's an ops decision rather than a governance one, so it lives here rather than as a poll. Filing the facts I verified live, so the discussion doesn't start from guesses.
What was asked
Whether
gateway.inference.coopshould send CORS exceptions for more apps — specifically, whether to open it to all origins (LITELLM_CORS_ORIGINS=*) so members can use browser-based apps we don't host, versus naming apps as needed.What is configured today (verified 2026-10-30, not from memory)
LITELLM_CORS_ORIGINS=https://chat.inference.coop— one origin, not two.Live preflight against
https://gateway.inference.coop/v1/chat/completions:Origin: https://chat.inference.coop→ 200,access-control-allow-origin: https://chat.inference.coopOrigin: https://evil.example→ 400, no allow-origin headerOrigin: null→ 400 — correctly refused (nullis the sandboxed-iframe /file://origin and a standard bypass attempt)Correction worth noting:
chat-legacy.inference.coopis not in the list — it returns 400. It is also not an older LibreChat: it runscom.openwebui.cloudronapp(Open WebUI), a separate app that is up and serving. [#14] records that this env var was renamed (LITELLM_CORS_ALLOWED_ORIGINS→LITELLM_CORS_ORIGINS) with "re-verify CORS after the upgrade" as a follow-up, which is the likely origin of the belief that two origins are set. Open WebUI normally calls the gateway server-side, where CORS does not apply, so this is only a real gap if members are using its Direct Connections mode. Worth confirming rather than assuming.The security reasoning (which I think is sound)
*, the budget is the thing standing between a leaked key and an unbounded bill.anthropic-dangerous-direct-browser-accessopt-in rather than opening up. So a narrow allow-list is a defensible choice, not an odd one.If we open it — the change and its risks
One env var on the gateway app. Two things to know:
access-control-allow-credentials: true, and*together with that is normally invalid in browsers — which would break chat if the middleware sent a literal*. I checked Starlette 1.3.1'sCORSMiddlewarein the running container: withallow_origins=["*"]and credentials enabled it echoes the requesting origin instead of sending*, which stays valid. So the change should work without breaking chat — but it must be re-verified, because that behaviour is a library implementation detail, not a contract.Acceptance criteria (if we proceed)
access-control-allow-originechoing that originchat.inference.coopstill returns 200 with its origin echoed, and a real browser completion from chat still worksnullwill also be echoed under*— reachable only by a page that already holds a keychat-legacy.inference.coopexplicitly added or its Direct-Connections use confirmed unnecessaryOptions
chat-legacy.if that use is intended: least change; leaves members unable to use third-party browser apps.Reversible either way, and small. [#14]: #14
[#24]: #24
I agree with this proposal. As I understand it, CORS is designed for cookies and adds almost no security for API keys anyways.
Implemented and verified on the gateway (image
1.0.0-1793467213663).Why the earlier attempt didn't work, which is worth recording:
start.shin the app hardcodedexport LITELLM_CORS_ORIGINS="https://chat.inference.coop". Theexportoverrode the Cloudron appenv, so setting
LITELLM_CORS_ORIGINSon the app read back correctly and did nothing. It now reads${LITELLM_CORS_ORIGINS:-https://chat.inference.coop}, so the app env drives it and the old lockremains only as a fallback. Thanks to @staeiou for raising it and to @bo for the reasoning on CORS
and cookies.
Decision: allow all origins. Set via the app env, and the comment in
start.shnow says why —CORS is not an auth boundary here; every request carries a per-member bearer key the browser never
attaches implicitly, so an origin list buys nothing against a party that already holds a key. The
control that matters is the per-key budget, model scoping and revocation.
Verified live (before → after):
Origin: https://some-other-app.example— HTTP 400, no allow-origin → 200,*Origin: https://chat-legacy.inference.coop— HTTP 400 → 200,*(the Open WebUI app now works cross-origin, which closes that part of the report)Origin: https://chat.inference.coop— HTTP 200 → 200,*(no regression)On the credentials caveat: a bare
*is invalid in browsers ifAccess-Control-Allow-Credentialsis also sent — and it is not sent now, so
*is valid and chat keeps working. Had it been, thischange would have broken the existing setup rather than widened it.