From a0233ca5d7ec2b838f039dd265ec30bbc09c5203 Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Mon, 21 Sep 2026 18:44:01 +0900 Subject: [PATCH 1/2] docs(remote-hub): warn about shared-host unauthenticated loopback companion --- docs-site/src/content/docs/guides/remote-hub.md | 7 +++++++ docs-site/src/content/docs/ko/guides/remote-hub.md | 4 ++++ tests/ci-workflows/docs-remote-hub-claims.test.ts | 12 ++++++++++++ 3 files changed, 23 insertions(+) diff --git a/docs-site/src/content/docs/guides/remote-hub.md b/docs-site/src/content/docs/guides/remote-hub.md index d0399c213cf..07d1dbb55bc 100644 --- a/docs-site/src/content/docs/guides/remote-hub.md +++ b/docs-site/src/content/docs/guides/remote-hub.md @@ -111,6 +111,13 @@ Bind the data listener to the hub's Tailscale address, enable the loopback compa own processes reach that same port without a credential, and publish management separately. The values below are examples: +:::danger[Use a dedicated single-tenant host] +The loopback companion is unauthenticated: every process and OS user on this machine can use the +hub's provider credentials and account quota, and can starve authenticated remote clients. Do not +enable it on a shared or multi-tenant host. If the host is shared, omit the +`unauthenticatedLoopbackListener` command and do not run the hub's local integrations. +::: + ```bash ocx config set runtimeRole hub ocx config set hostname 100.64.0.10 diff --git a/docs-site/src/content/docs/ko/guides/remote-hub.md b/docs-site/src/content/docs/ko/guides/remote-hub.md index 0b1cb95941c..edd5824e28f 100644 --- a/docs-site/src/content/docs/ko/guides/remote-hub.md +++ b/docs-site/src/content/docs/ko/guides/remote-hub.md @@ -51,6 +51,10 @@ ocx sync 데이터 리스너는 허브의 Tailscale 주소에 바인드하고, 허브 자신의 프로세스가 같은 포트를 자격 증명 없이 쓸 수 있도록 루프백 companion을 켜고, 관리 평면은 따로 공개합니다. 아래 값은 예시입니다. +:::danger[전용 단일 테넌트 호스트를 사용하세요] +루프백 companion은 인증이 없습니다. 이 머신의 모든 프로세스와 OS 사용자가 허브의 프로바이더 자격 증명과 계정 쿼터를 사용할 수 있고, 인증된 원격 클라이언트를 굶길 수 있습니다. 공유 또는 다중 테넌트 호스트에서는 활성화하지 마세요. 호스트가 공유라면 `unauthenticatedLoopbackListener` 명령을 생략하고 허브의 로컬 통합을 실행하지 마세요. +::: + ```bash ocx config set runtimeRole hub ocx config set hostname 100.64.0.10 diff --git a/tests/ci-workflows/docs-remote-hub-claims.test.ts b/tests/ci-workflows/docs-remote-hub-claims.test.ts index e6812f31fb5..a9e32d732c2 100644 --- a/tests/ci-workflows/docs-remote-hub-claims.test.ts +++ b/tests/ci-workflows/docs-remote-hub-claims.test.ts @@ -123,6 +123,18 @@ describe("the one-port hub recipe", () => { } }); + test("both locales warn that the companion requires a dedicated host", async () => { + const warnings = [ + ["en", GUIDE, "every process and OS user", "shared or multi-tenant host"], + ["ko", KO_GUIDE, "모든 프로세스와 OS 사용자", "공유 또는 다중 테넌트 호스트에서는 활성화하지 마세요"], + ] as const; + for (const [locale, file, localAccess, sharedHost] of warnings) { + const source = await Bun.file(file).text(); + expect(source, locale).toContain(localAccess); + expect(source, locale).toContain(sharedHost); + } + }); + test("no locale tells the operator to export a data-plane token by hand", async () => { for (const [locale, file] of LOCALES) { const source = await Bun.file(file).text(); From b00ed1ebf2c0d7d8029dd1172ef156a56ad1ab61 Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Tue, 22 Sep 2026 06:45:53 +0900 Subject: [PATCH 2/2] docs(remote-hub): align the loopback warning with the reference and cover the ported form --- docs-site/src/content/docs/guides/remote-hub.md | 14 +++++++++++--- .../src/content/docs/ko/guides/remote-hub.md | 6 +++++- skills/ocx/references/05_remote_hub.md | 5 +++++ .../ci-workflows/docs-remote-hub-claims.test.ts | 16 +++++++++++++--- 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/docs-site/src/content/docs/guides/remote-hub.md b/docs-site/src/content/docs/guides/remote-hub.md index 07d1dbb55bc..882344538e2 100644 --- a/docs-site/src/content/docs/guides/remote-hub.md +++ b/docs-site/src/content/docs/guides/remote-hub.md @@ -113,9 +113,14 @@ values below are examples: :::danger[Use a dedicated single-tenant host] The loopback companion is unauthenticated: every process and OS user on this machine can use the -hub's provider credentials and account quota, and can starve authenticated remote clients. Do not -enable it on a shared or multi-tenant host. If the host is shared, omit the -`unauthenticatedLoopbackListener` command and do not run the hub's local integrations. +hub's provider credentials and account quota, and can exhaust the shared turn capacity that +authenticated remote clients depend on. Do not enable it on a shared or multi-tenant host. If the +host is shared, omit the `unauthenticatedLoopbackListener` command and do not run the hub's local +integrations. + +Binding to `127.0.0.1` means the kernel refuses remote connections, but it does not stop a browser: +a page you visit can make your browser connect to `127.0.0.1`. The listener therefore applies the +same `Host` and `Origin` checks as an ordinary loopback bind. ::: ```bash @@ -223,6 +228,9 @@ separate ports: ocx config set unauthenticatedLoopbackListener '{"enabled":true,"port":10104}' ``` +The ported form is the same unauthenticated surface: the dedicated-host warning above applies to +this command too. + With a `port` set, the local integrations follow the listener and write `http://127.0.0.1:10104` instead. The port must differ from the proxy port and is never OS-assigned: an ephemeral port would change across restarts while already-running app-servers kept the previous `base_url`. diff --git a/docs-site/src/content/docs/ko/guides/remote-hub.md b/docs-site/src/content/docs/ko/guides/remote-hub.md index edd5824e28f..5034af5d0ce 100644 --- a/docs-site/src/content/docs/ko/guides/remote-hub.md +++ b/docs-site/src/content/docs/ko/guides/remote-hub.md @@ -52,7 +52,9 @@ ocx sync 데이터 리스너는 허브의 Tailscale 주소에 바인드하고, 허브 자신의 프로세스가 같은 포트를 자격 증명 없이 쓸 수 있도록 루프백 companion을 켜고, 관리 평면은 따로 공개합니다. 아래 값은 예시입니다. :::danger[전용 단일 테넌트 호스트를 사용하세요] -루프백 companion은 인증이 없습니다. 이 머신의 모든 프로세스와 OS 사용자가 허브의 프로바이더 자격 증명과 계정 쿼터를 사용할 수 있고, 인증된 원격 클라이언트를 굶길 수 있습니다. 공유 또는 다중 테넌트 호스트에서는 활성화하지 마세요. 호스트가 공유라면 `unauthenticatedLoopbackListener` 명령을 생략하고 허브의 로컬 통합을 실행하지 마세요. +루프백 companion은 인증이 없습니다. 이 머신의 모든 프로세스와 OS 사용자가 허브의 프로바이더 자격 증명과 계정 쿼터를 사용할 수 있고, 인증된 원격 클라이언트가 의존하는 공유 턴 용량을 고갈시킬 수 있습니다. 공유 또는 다중 테넌트 호스트에서는 활성화하지 마세요. 호스트가 공유라면 `unauthenticatedLoopbackListener` 명령을 생략하고 허브의 로컬 통합을 실행하지 마세요. + +`127.0.0.1`에 바인드하면 커널이 원격 접속을 거부하지만 브라우저까지 막지는 못합니다. 방문한 페이지가 브라우저를 통해 `127.0.0.1`에 접속하게 할 수 있습니다. 그래서 리스너는 일반 루프백 바인드와 같은 `Host`/`Origin` 검사를 적용합니다. ::: ```bash @@ -119,6 +121,8 @@ companion 형태는 `hostname`이 루프백도 와일드카드도 아닌 구체 ocx config set unauthenticatedLoopbackListener '{"enabled":true,"port":10104}' ``` +포트 지정 형태도 같은 인증 없는 표면입니다. 위의 전용 호스트 경고가 이 명령에도 적용됩니다. + `port`를 지정하면 로컬 통합이 리스너를 따라 `http://127.0.0.1:10104`를 기록합니다. 이 포트는 프록시 포트와 달라야 하고 OS가 자동 할당하지 않습니다. 임시 포트는 재시작 때마다 바뀌는데 이미 실행 중인 app-server는 예전 `base_url`을 들고 있기 때문입니다. **이 필드를 바꾸면 프록시를 재시작하세요.** 소켓은 시작할 때 한 번 바인드되고 로컬 클라이언트 파일도 그때 결정된 값으로 기록되므로, 실행 중인 허브는 예전 답을 유지합니다. 포트 지정 허브에서는 이것이 `ocx claude`가 리스너에 닿는지 `404`를 받는지의 차이입니다. 백그라운드 서비스라면 명령은 항상 재시작하는 `ocx service restart`입니다. [macOS 서비스 운영](#macos-서비스-운영)을 보세요. `ocx restart`는 다른 명령입니다. 직접 띄운 프록시 프로세스를 재시작하며, 서비스 관리자가 감독하는 서비스를 다루지 않습니다. diff --git a/skills/ocx/references/05_remote_hub.md b/skills/ocx/references/05_remote_hub.md index 0d8042fe615..aaaf36a1e3d 100644 --- a/skills/ocx/references/05_remote_hub.md +++ b/skills/ocx/references/05_remote_hub.md @@ -26,6 +26,11 @@ has to learn a new port. Setting a `port` (`{ "enabled": true, "port": 10104 }`) works and puts the two surfaces on separate ports; local integrations then follow the listener's port. +This listener is unauthenticated: every process and OS user on the hub machine can spend +its provider credentials and quota, and can exhaust the shared turn capacity remote +clients depend on. Enable it only on a dedicated single-tenant host — on a shared or +multi-tenant host, omit `unauthenticatedLoopbackListener` entirely. + The port-less form is refused on a loopback or wildcard `hostname` — `127.0.0.1`, `localhost`, `0.0.0.0`, `::` — because the public listener already holds that loopback address. The refusal happens at write time and again at startup, naming the collision. On diff --git a/tests/ci-workflows/docs-remote-hub-claims.test.ts b/tests/ci-workflows/docs-remote-hub-claims.test.ts index a9e32d732c2..2f116e86b09 100644 --- a/tests/ci-workflows/docs-remote-hub-claims.test.ts +++ b/tests/ci-workflows/docs-remote-hub-claims.test.ts @@ -125,13 +125,23 @@ describe("the one-port hub recipe", () => { test("both locales warn that the companion requires a dedicated host", async () => { const warnings = [ - ["en", GUIDE, "every process and OS user", "shared or multi-tenant host"], - ["ko", KO_GUIDE, "모든 프로세스와 OS 사용자", "공유 또는 다중 테넌트 호스트에서는 활성화하지 마세요"], + ["en", GUIDE, "every process and OS user", "shared or multi-tenant host", "dedicated single-tenant host", "Do not enable"], + ["ko", KO_GUIDE, "모든 프로세스와 OS 사용자", "공유 또는 다중 테넌트 호스트에서는 활성화하지 마세요", "전용 단일 테넌트 호스트", "활성화하지 마세요"], ] as const; - for (const [locale, file, localAccess, sharedHost] of warnings) { + for (const [locale, file, localAccess, sharedHost, dedicated, doNotEnable] of warnings) { const source = await Bun.file(file).text(); expect(source, locale).toContain(localAccess); expect(source, locale).toContain(sharedHost); + expect(source, locale).toContain(dedicated); + expect(source, locale).toContain(doNotEnable); + // The warning must render as a danger box, not flow past as ordinary prose. + expect(source, locale).toContain(":::danger"); + // The same unauthenticated surface is offered again by the ported form; the warning + // must reach that command too, or a reader following only that section misses it. + const ported = source.indexOf('"port":10104'); + expect(ported, locale).toBeGreaterThan(-1); + const after = source.slice(ported, ported + 600); + expect(after, locale).toMatch(/unauthenticated|인증/); } });