Security
Host and Origin allow-lists, user vs operator planes, Console and MCP posture.
Every served request on the backend, Console, and app MCP passes Host / Origin checks before your Flow runs. Private bind addresses are not a substitute for that check.
Reach for this page when you put a public hostname in front of the app, open Console in production, or wire MCP tokens.
The one rule
Pass every public hostname in allowedHosts on createBunRuntime().serve (and the Console / MCP
serve options). Loopback is always allowed; Origin: null is always rejected. Failures return
403.
Smallest Example
Serve with an explicit host allow-list
import { createBunRuntime } from "okengine/http";
import { app } from "./app";
createBunRuntime().serve(app, {
port: Number(process.env.PORT ?? 6530),
hostname: "0.0.0.0",
allowedHosts: ["app.example.com", ".example.com"],
});.example.com allows a.example.com (Vite-style suffix). Always merged with
localhost · 127.0.0.1 · ::1 and the listen hostname when it is not a
wildcard bind.
Confirm a bad Host is refused
curl -i http://127.0.0.1:6530/health -H "Host: evil.example"Expect 403 with body Forbidden: unexpected Host header.
Progressive Patterns
Edge terminates TLS and forwards to the app. The Host the app sees must be on
the allow-list — usually the public name, not the container hostname.
createBunRuntime().serve(app, {
hostname: "0.0.0.0",
allowedHosts: ["app.example.com"],
});Consequence: omitting the public name behind Caddy / nginx / Traefik looks
like a random 403 to browsers that send the site's Host.
Host / Origin Rules
| Check | Behavior |
|---|---|
Host | Required; must match the effective allow-list |
Origin | When present, host must match; Origin: null → 403 |
| Defaults | Always includes loopback + listen hostname (unless 0.0.0.0 / ::) |
| Extras | ServeOptions.allowedHosts — never replaces the mandatory check |
allowedHosts lives on serve options, not in oke.config.ts. Pass it wherever
you call serve for 6530, Console 6533, and app MCP 6535.
Console Posture
| Control | Meaning |
|---|---|
| Host / Origin | Same allow-list logic as the backend |
| CSP | default-src 'self'; frame-ancestors 'none' |
| Cookies | SameSite=Strict |
| Claim code | Printed on oke dev TTY; oke console claim-code while setup is open |
| PII | Store / Call API mask classified fields unless revealPii: true (audited) |
Production Console: set OKE_CONSOLE_SECRET and configure
console.prod in Configuration.
Sessions and Audiences
| Audience | Surface |
|---|---|
oke-app | Application sessions |
oke-console | Console operators |
oke-mcp | App MCP |
Short access JWT (default ~14m) + rotating refresh. Refresh-token reuse revokes the family.
Troubleshooting
The request Host is not on the allow-list. Add the public hostname to allowedHosts, or hit
loopback (localhost / 127.0.0.1) during local oke dev.
Browser sent an Origin whose host is not allowed, or Origin: null. Align the page origin with
allowedHosts, or call same-origin / non-browser clients without a spoofed Origin.
App MCP on 6535 requires a Bearer with audience oke-mcp even on loopback. Docs MCP on
6536 does not — it serves documentation only.
A user-plane token reached an operator Flow (or the reverse). Use the principal that matches the
Flow's plane.
A derived key or invoke-as identity requested a scope the creator cannot grant. Re-issue with a subset of the creator's scopes.
Learn more
- CLI —
oke dev,oke console claim-code - Gate — policies, API keys, tenancy
- The Architecture — one shape, one door, fixed vocabulary
- Errors —
CrossPlaneError,SessionError,AttenuationError - Environment Variables —
OKE_CONSOLE_SECRET