docs: document the /api proxy contract and env vars
Retire HONCHO_UPSTREAM and the same-origin sentinel; document the per-request X-Honcho-Upstream header model, OPENCONCHO_DEFAULT_HONCHO_URL seeding, and the optional OPENCONCHO_UPSTREAM_ALLOWLIST SSRF guard across compose, README, AGENTS.md, and docs/docker.md.
This commit is contained in:
@@ -64,6 +64,7 @@ Before pushing any change under `packages/desktop/**` or `packages/desktop/src-t
|
|||||||
## Key Constraints
|
## Key Constraints
|
||||||
|
|
||||||
- **No hardcoded URLs** — connection config lives in `localStorage` under `openconcho:instances` (multi-instance store; legacy `openconcho:config` is auto-migrated)
|
- **No hardcoded URLs** — connection config lives in `localStorage` under `openconcho:instances` (multi-instance store; legacy `openconcho:config` is auto-migrated)
|
||||||
|
- **Web CORS via a same-origin `/api` proxy** — the web build issues all Honcho calls to `/api/*` with an `X-Honcho-Upstream` header (the active instance's URL); nginx (docker) and a Vite middleware (dev) forward server-side. Transport is resolved by `dispatchFor` in `src/lib/dispatch.ts`: web → relative `/api` + header; Tauri → absolute URL + reqwest. Optional `OPENCONCHO_UPSTREAM_ALLOWLIST` guards the proxy when exposed.
|
||||||
- **Local git hooks** — `.husky/pre-commit` runs a secret scan + Biome on staged files; `.husky/pre-push` runs `pnpm check`. Your commits and pushes trigger these.
|
- **Local git hooks** — `.husky/pre-commit` runs a secret scan + Biome on staged files; `.husky/pre-push` runs `pnpm check`. Your commits and pushes trigger these.
|
||||||
- **TanStack Router flat-route params** — always cast `params` as `as never` at `navigate()` and `<Link>` callsites
|
- **TanStack Router flat-route params** — always cast `params` as `as never` at `navigate()` and `<Link>` callsites
|
||||||
- **`framer-motion` Variants typing** — import `type Variants` and annotate objects; never use `as const` on variant objects
|
- **`framer-motion` Variants typing** — import `type Variants` and annotate objects; never use `as const` on variant objects
|
||||||
|
|||||||
13
README.md
13
README.md
@@ -91,18 +91,21 @@ pnpm --filter @openconcho/desktop dev
|
|||||||
|
|
||||||
Run the web UI in a container — handy for adding it to a self-hosted Honcho
|
Run the web UI in a container — handy for adding it to a self-hosted Honcho
|
||||||
Compose stack. The image serves the SPA and reverse-proxies the Honcho API under
|
Compose stack. The image serves the SPA and reverse-proxies the Honcho API under
|
||||||
its own origin, so the browser makes same-origin requests (no CORS to configure).
|
its own origin: the browser calls `/api` same-origin and names the upstream in an
|
||||||
|
`X-Honcho-Upstream` header, so there's no browser CORS to configure.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run --rm -p 8080:8080 \
|
docker run --rm -p 8080:8080 \
|
||||||
-e HONCHO_UPSTREAM=http://host.docker.internal:8000 \
|
-e OPENCONCHO_DEFAULT_HONCHO_URL=http://host.docker.internal:8000 \
|
||||||
ghcr.io/offendingcommit/openconcho-web:latest
|
ghcr.io/offendingcommit/openconcho-web:latest
|
||||||
# → http://localhost:8080
|
# → http://localhost:8080
|
||||||
```
|
```
|
||||||
|
|
||||||
To drop it into a Honcho Compose stack, use the `openconcho` service in
|
`OPENCONCHO_DEFAULT_HONCHO_URL` seeds the first instance (absolute URL).
|
||||||
[`docker-compose.yml`](docker-compose.yml). Full details, env vars, and the CORS
|
`OPENCONCHO_UPSTREAM_ALLOWLIST` is an optional SSRF guard (comma-separated host
|
||||||
options are in [`docs/docker.md`](docs/docker.md).
|
globs) for when you expose the proxy. To drop it into a Honcho Compose stack, use
|
||||||
|
the `openconcho` service in [`docker-compose.yml`](docker-compose.yml). Full
|
||||||
|
details and env vars are in [`docs/docker.md`](docs/docker.md).
|
||||||
|
|
||||||
### Connecting to your instance
|
### Connecting to your instance
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,30 @@
|
|||||||
# Run OpenConcho's web UI with `docker compose up`.
|
# Run OpenConcho's web UI with `docker compose up`.
|
||||||
#
|
#
|
||||||
# Standalone: serves the SPA on http://localhost:8080 and reverse-proxies the
|
# Standalone: serves the SPA on http://localhost:8080. The browser issues all
|
||||||
# Honcho API under the same origin (no browser CORS). By default it points at a
|
# Honcho calls same-origin to /api; nginx forwards each to the URL named in the
|
||||||
# Honcho running on the host at :8000 — override HONCHO_UPSTREAM for anything else:
|
# per-request X-Honcho-Upstream header (no browser CORS). Seed the first instance
|
||||||
|
# with OPENCONCHO_DEFAULT_HONCHO_URL — override for anything else:
|
||||||
#
|
#
|
||||||
# HONCHO_UPSTREAM=https://honcho.example.net docker compose up
|
# OPENCONCHO_DEFAULT_HONCHO_URL=https://honcho.example.net docker compose up
|
||||||
#
|
#
|
||||||
# To fold this into an existing Honcho Compose stack, drop the `openconcho`
|
# To fold this into an existing Honcho Compose stack, drop the `openconcho`
|
||||||
# service into that project, set HONCHO_UPSTREAM to the api service
|
# service into that project, set OPENCONCHO_DEFAULT_HONCHO_URL to the api service
|
||||||
# (e.g. http://api:8000), and add `depends_on: { api: { condition: service_healthy } }`.
|
# (e.g. http://api:8000 — nginx resolves it on the compose network), and add
|
||||||
|
# `depends_on: { api: { condition: service_healthy } }`.
|
||||||
services:
|
services:
|
||||||
openconcho:
|
openconcho:
|
||||||
image: ghcr.io/offendingcommit/openconcho-web:latest
|
image: ghcr.io/offendingcommit/openconcho-web:latest
|
||||||
# Or build from this repo instead of pulling the published image:
|
# Or build from this repo instead of pulling the published image:
|
||||||
# build: .
|
# build: .
|
||||||
environment:
|
environment:
|
||||||
# nginx reverse-proxies /v3 and /health to this upstream (the Honcho API).
|
# The SPA seeds its first instance from this absolute URL; the browser then
|
||||||
HONCHO_UPSTREAM: ${HONCHO_UPSTREAM:-http://host.docker.internal:8000}
|
# routes all calls same-origin through /api, and nginx forwards them to the
|
||||||
# The SPA defaults its Honcho base URL to its own origin, so requests flow
|
# URL named per-request in the X-Honcho-Upstream header (no browser CORS).
|
||||||
# through the proxy above — no browser CORS, token never leaves the origin.
|
OPENCONCHO_DEFAULT_HONCHO_URL: ${OPENCONCHO_DEFAULT_HONCHO_URL:-http://host.docker.internal:8000}
|
||||||
OPENCONCHO_DEFAULT_HONCHO_URL: same-origin
|
# Optional SSRF guard. Unset = forward anywhere (safe for the localhost-only
|
||||||
|
# binding below). Set comma-separated host globs before exposing the proxy:
|
||||||
|
# OPENCONCHO_UPSTREAM_ALLOWLIST: honcho.example.net,*.honcho.dev
|
||||||
|
OPENCONCHO_UPSTREAM_ALLOWLIST: ${OPENCONCHO_UPSTREAM_ALLOWLIST:-}
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:8080:8080"
|
- "127.0.0.1:8080:8080"
|
||||||
# Lets the default host.docker.internal upstream resolve on Linux too
|
# Lets the default host.docker.internal upstream resolve on Linux too
|
||||||
|
|||||||
@@ -5,6 +5,16 @@ builds the static bundle, then `nginx-unprivileged` serves it on port `8080` as
|
|||||||
a non-root user) that also **reverse-proxies the Honcho API under its own
|
a non-root user) that also **reverse-proxies the Honcho API under its own
|
||||||
origin**, so the browser never makes a cross-origin request.
|
origin**, so the browser never makes a cross-origin request.
|
||||||
|
|
||||||
|
## How the proxy works
|
||||||
|
|
||||||
|
The browser issues every Honcho call same-origin to `/api/*` and names the real
|
||||||
|
upstream per request in an `X-Honcho-Upstream` header (sourced from the active
|
||||||
|
instance's base URL). nginx strips `/api`, forwards to that upstream server-side,
|
||||||
|
and returns the response. Because the browser→nginx hop is same-origin, **no CORS
|
||||||
|
applies**; the nginx→Honcho hop is server-side, where CORS is irrelevant. The
|
||||||
|
frontend stays the source of truth for which instance to talk to, so the
|
||||||
|
multi-instance switcher and the Fleet view keep working.
|
||||||
|
|
||||||
## Add it to a Honcho Compose stack (recommended)
|
## Add it to a Honcho Compose stack (recommended)
|
||||||
|
|
||||||
Honcho's self-hosting path is Docker Compose. Drop the `openconcho` service from
|
Honcho's self-hosting path is Docker Compose. Drop the `openconcho` service from
|
||||||
@@ -16,8 +26,8 @@ services:
|
|||||||
openconcho:
|
openconcho:
|
||||||
image: ghcr.io/offendingcommit/openconcho-web:latest
|
image: ghcr.io/offendingcommit/openconcho-web:latest
|
||||||
environment:
|
environment:
|
||||||
HONCHO_UPSTREAM: http://api:8000 # nginx proxies /v3 + /health here
|
# Seeds the first instance; nginx resolves this on the compose network.
|
||||||
OPENCONCHO_DEFAULT_HONCHO_URL: same-origin
|
OPENCONCHO_DEFAULT_HONCHO_URL: http://api:8000
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:8080:8080"
|
- "127.0.0.1:8080:8080"
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -26,17 +36,17 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
```
|
```
|
||||||
|
|
||||||
`OPENCONCHO_DEFAULT_HONCHO_URL: same-origin` makes the UI default its Honcho
|
`OPENCONCHO_DEFAULT_HONCHO_URL` seeds the UI's first instance with an absolute
|
||||||
base URL to its own origin, so API calls go through the proxy → **no browser
|
URL. The browser sends that URL in the `X-Honcho-Upstream` header; nginx (on the
|
||||||
CORS, and the API token never leaves the origin.** The published image is
|
compose network) forwards to it — **no browser CORS, and the API token never
|
||||||
multi-arch (amd64 + arm64); the first publish creates a private GHCR package —
|
leaves the origin.** The published image is multi-arch (amd64 + arm64); the first
|
||||||
make it public if you want unauthenticated pulls.
|
publish creates a private GHCR package — make it public for unauthenticated pulls.
|
||||||
|
|
||||||
## Standalone
|
## Standalone
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build -t openconcho-web .
|
docker build -t openconcho-web .
|
||||||
docker run --rm -p 8080:8080 -e HONCHO_UPSTREAM=http://host.docker.internal:8000 openconcho-web
|
docker run --rm -p 8080:8080 -e OPENCONCHO_DEFAULT_HONCHO_URL=http://host.docker.internal:8000 openconcho-web
|
||||||
# → http://localhost:8080 · GET /healthz returns "ok"
|
# → http://localhost:8080 · GET /healthz returns "ok"
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -44,24 +54,26 @@ Runtime knobs (no rebuild needed):
|
|||||||
|
|
||||||
| Env | Default | Meaning |
|
| Env | Default | Meaning |
|
||||||
|-----|---------|---------|
|
|-----|---------|---------|
|
||||||
| `HONCHO_UPSTREAM` | `http://api:8000` | Where nginx proxies `/v3` and `/health` |
|
| `OPENCONCHO_DEFAULT_HONCHO_URL` | _(empty)_ | Absolute URL seeding the first instance; empty = configure in Settings |
|
||||||
| `OPENCONCHO_DEFAULT_HONCHO_URL` | `same-origin` | SPA's default base URL — `same-origin`, an absolute URL, or empty (configure in Settings) |
|
| `OPENCONCHO_UPSTREAM_ALLOWLIST` | _(empty)_ | Optional SSRF guard: comma-separated host globs (e.g. `honcho.example.net,*.honcho.dev`). Empty = forward anywhere |
|
||||||
|
|
||||||
Hardened run adds `--read-only --cap-drop ALL --security-opt no-new-privileges`
|
Hardened run adds `--read-only --cap-drop ALL --security-opt no-new-privileges`
|
||||||
with `--tmpfs /tmp --tmpfs /var/cache/nginx`. Note: the runtime config writes
|
with `--tmpfs /tmp --tmpfs /var/cache/nginx`. Note: the entrypoint writes
|
||||||
`config.js` into the web root at start, which a read-only root blocks — under
|
`config.js` and the allowlist map at start, which a read-only root blocks — under
|
||||||
`--read-only` either bind-mount `config.js` or leave
|
`--read-only` either bind-mount those paths or leave the env empty and configure
|
||||||
`OPENCONCHO_DEFAULT_HONCHO_URL` empty and set the URL in Settings.
|
the URL in Settings.
|
||||||
|
|
||||||
|
## SSRF: when to set the allowlist
|
||||||
|
|
||||||
|
The header-driven proxy forwards to whatever upstream the client names. With the
|
||||||
|
default `127.0.0.1:8080` binding only your own machine can reach nginx, so leaving
|
||||||
|
the allowlist open is fine. **Before exposing the proxy** (e.g. behind a tunnel),
|
||||||
|
set `OPENCONCHO_UPSTREAM_ALLOWLIST` to the host globs you trust — non-matching
|
||||||
|
upstreams are rejected with `403` and an `X-Honcho-Proxy-Reject: allowlist` header.
|
||||||
|
|
||||||
## CORS, the short version
|
## CORS, the short version
|
||||||
|
|
||||||
The desktop app routes HTTP through Rust and bypasses browser CORS; the web
|
The desktop app routes HTTP through Rust (reqwest) and bypasses browser CORS; the
|
||||||
build doesn't. The Compose setup above **solves CORS via the same-origin proxy**
|
web build solves it with the same-origin `/api` proxy above — **nothing to
|
||||||
— nothing to configure on Honcho. If instead you point the UI at a *different*
|
configure on Honcho.** The proxy makes a Honcho-side `CORSMiddleware` unnecessary
|
||||||
origin (absolute `OPENCONCHO_DEFAULT_HONCHO_URL` or a URL typed in Settings),
|
regardless of which instance you point at.
|
||||||
allow that origin in Honcho's FastAPI `CORSMiddleware`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
app.add_middleware(CORSMiddleware, allow_origins=["https://your-ui-origin"],
|
|
||||||
allow_methods=["*"], allow_headers=["*"])
|
|
||||||
```
|
|
||||||
|
|||||||
Reference in New Issue
Block a user