Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs-site/src/content/docs/guides/remote-hub.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,18 @@ 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 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
ocx config set runtimeRole hub
ocx config set hostname 100.64.0.10
Expand Down Expand Up @@ -216,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`.
Expand Down
8 changes: 8 additions & 0 deletions docs-site/src/content/docs/ko/guides/remote-hub.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ ocx sync

데이터 리스너는 허브의 Tailscale 주소에 바인드하고, 허브 자신의 프로세스가 같은 포트를 자격 증명 없이 쓸 수 있도록 루프백 companion을 켜고, 관리 평면은 따로 공개합니다. 아래 값은 예시입니다.

:::danger[전용 단일 테넌트 호스트를 사용하세요]
루프백 companion은 인증이 없습니다. 이 머신의 모든 프로세스와 OS 사용자가 허브의 프로바이더 자격 증명과 계정 쿼터를 사용할 수 있고, 인증된 원격 클라이언트가 의존하는 공유 턴 용량을 고갈시킬 수 있습니다. 공유 또는 다중 테넌트 호스트에서는 활성화하지 마세요. 호스트가 공유라면 `unauthenticatedLoopbackListener` 명령을 생략하고 허브의 로컬 통합을 실행하지 마세요.

`127.0.0.1`에 바인드하면 커널이 원격 접속을 거부하지만 브라우저까지 막지는 못합니다. 방문한 페이지가 브라우저를 통해 `127.0.0.1`에 접속하게 할 수 있습니다. 그래서 리스너는 일반 루프백 바인드와 같은 `Host`/`Origin` 검사를 적용합니다.
:::

```bash
ocx config set runtimeRole hub
ocx config set hostname 100.64.0.10
Expand Down Expand Up @@ -115,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`는 다른 명령입니다. 직접 띄운 프록시 프로세스를 재시작하며, 서비스 관리자가 감독하는 서비스를 다루지 않습니다.
Expand Down
5 changes: 5 additions & 0 deletions skills/ocx/references/05_remote_hub.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions tests/ci-workflows/docs-remote-hub-claims.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,28 @@ 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", "dedicated single-tenant host", "Do not enable"],
["ko", KO_GUIDE, "모든 프로세스와 OS 사용자", "공유 또는 다중 테넌트 호스트에서는 활성화하지 마세요", "전용 단일 테넌트 호스트", "활성화하지 마세요"],
] as const;
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|인증/);
}
});

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();
Expand Down
Loading