mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-10-19 08:54:05 +00:00
fix(docker): stop handing the management token to unauthenticated clients
Nginx redirected GET / (and a token-less index page) to a URL carrying CCR_WEB_AUTH_TOKEN, so anyone who could reach the published port got the token from the Location header. The root now redirects to the page and only passes on a ccr_web_token the caller already supplied. A generated token is printed once to the container log, as Jupyter does.
This commit is contained in:
parent
499b7f6310
commit
ec0cb4fecc
5 changed files with 38 additions and 26 deletions
|
|
@ -21,7 +21,7 @@ Nginx routes:
|
|||
|
||||
| Public route | Purpose |
|
||||
| --- | --- |
|
||||
| `/` and `/pages/home/index.html` | Browser management UI. `/` redirects to a URL containing the management token. |
|
||||
| `/` and `/pages/home/index.html` | Browser management UI. `/` redirects to the page and keeps a `ccr_web_token` query parameter if the request has one. The token is never added by Nginx. |
|
||||
| `/api/ccr/rpc` | Authenticated management RPC. |
|
||||
| `/health` | Gateway health, not container/UI health. It can return `502` until a provider and model are configured and the gateway starts. |
|
||||
| `/v1/*`, `/v1beta/*`, `/messages`, `/chat/completions`, `/responses`, `/interactions`, `/mcp/*` | Supported model and MCP gateway requests. |
|
||||
|
|
@ -35,7 +35,7 @@ npm run docker:compose:up
|
|||
docker compose logs -f ccr
|
||||
```
|
||||
|
||||
Open <http://127.0.0.1:3458>. On a new volume, the management UI is immediately available. Add a provider and model, create a CCR client key under **API Keys**, and start the gateway from **Server**.
|
||||
Open the management URL that the entrypoint prints in the container log, `http://127.0.0.1:3458/?ccr_web_token=<token>`. It is only printed when the token was generated; if you set `CCR_WEB_AUTH_TOKEN` yourself, add it to the URL. On a new volume, the management UI is immediately available. Add a provider and model, create a CCR client key under **API Keys**, and start the gateway from **Server**.
|
||||
|
||||
The npm Compose script prepares a local `../../next-ai/gateway` checkout before building so the image uses that plugin-capable ai-gateway runtime. If you run `docker compose up -d --build` directly, run `npm run docker:prepare-gateway` first. `npm run docker:build` performs the same preparation automatically.
|
||||
|
||||
|
|
@ -82,10 +82,10 @@ npm run docker:run
|
|||
|
||||
There are two independent authentication layers:
|
||||
|
||||
1. `CCR_WEB_AUTH_TOKEN` protects management RPC. Nginx puts it into the management-page URL, and the browser sends it to RPC as `x-ccr-web-auth`.
|
||||
1. `CCR_WEB_AUTH_TOKEN` protects management RPC. You open the management page with it in the URL (`/?ccr_web_token=<token>`), and the browser sends it to RPC as `x-ccr-web-auth`. Nginx never sends it to a client.
|
||||
2. CCR client API keys created in the **API Keys** page protect model gateway requests. These are separate from upstream provider credentials.
|
||||
|
||||
If `CCR_WEB_AUTH_TOKEN` is unset, the entrypoint generates a new random token on each container start. Opening `/` still works because Nginx redirects to a tokenized URL, but a stable token is recommended for persistent or remote deployments.
|
||||
If `CCR_WEB_AUTH_TOKEN` is unset, the entrypoint generates a new random token on each container start and prints the management URL once to the container log. Opening `/` without a token loads the page, but every management call fails with `401` until the token is supplied. The page keeps the token for the browser tab session. A stable token is recommended for persistent or remote deployments.
|
||||
|
||||
Avoid putting the token directly in shell history. Create a protected environment file instead:
|
||||
|
||||
|
|
@ -237,7 +237,7 @@ docker compose config
|
|||
|
||||
### `/` returns `302`
|
||||
|
||||
This is expected. Nginx redirects the root URL to the management page and URL-encodes the management token.
|
||||
This is expected. Nginx redirects the root URL to the management page and keeps the query string, so a `ccr_web_token` you supply survives. It does not add a token.
|
||||
|
||||
### `/health` returns `502`
|
||||
|
||||
|
|
@ -245,7 +245,7 @@ This is expected. Nginx redirects the root URL to the management page and URL-en
|
|||
|
||||
### The UI returns `401` after a token change
|
||||
|
||||
Open the bare root URL again so Nginx creates a URL with the current token. Close stale tabs and avoid bookmarks that contain an old `ccr_web_token`.
|
||||
Open `/?ccr_web_token=<token>` again with the current token (for a generated token, read it from `docker compose logs ccr`). Close stale tabs and avoid bookmarks that contain an old `ccr_web_token`.
|
||||
|
||||
### Clients still use the old port or hostname
|
||||
|
||||
|
|
@ -278,7 +278,7 @@ npm run docker:compose:up
|
|||
docker compose logs -f ccr
|
||||
```
|
||||
|
||||
打开 <http://127.0.0.1:3458>。首次启动时管理 UI 可以立即访问;添加供应商和模型、在 **API 密钥** 页面创建 CCR 客户端 Key,然后从 **服务** 页面启动网关。
|
||||
打开容器日志中打印的管理地址 `http://127.0.0.1:3458/?ccr_web_token=<token>`(仅在 Token 自动生成时打印;自行设置了 `CCR_WEB_AUTH_TOKEN` 时,请自行拼到地址后)。首次启动时管理 UI 可以立即访问;添加供应商和模型、在 **API 密钥** 页面创建 CCR 客户端 Key,然后从 **服务** 页面启动网关。
|
||||
|
||||
这个 npm Compose 脚本会在构建前准备本机的 `../../next-ai/gateway` checkout,让镜像使用支持插件的 ai-gateway runtime。如果直接运行 `docker compose up -d --build`,请先执行 `npm run docker:prepare-gateway`。`npm run docker:build` 也会自动打包这份本地 ai-gateway。
|
||||
|
||||
|
|
@ -295,7 +295,7 @@ ports:
|
|||
- **API 密钥** 页面创建的 CCR 客户端 Key 用于模型网关请求。
|
||||
- 上游供应商凭据是第三类凭据,不应拿来代替 CCR 客户端 Key。
|
||||
|
||||
根路径会重定向到包含 `ccr_web_token` 的管理 URL。请把该 URL 当作密码。远程部署至少应使用固定强 Token、TLS、主机防火墙或私网,并让反向代理把全部路径转发到 Nginx。流式响应和 SSE 不应被代理缓冲。
|
||||
Nginx 不会向客户端返回 Token:根路径只会跳转到管理页,并保留请求中已有的 `ccr_web_token` 参数。自动生成的 Token 会在容器日志中打印一次,请把含 Token 的 URL 当作密码。远程部署至少应使用固定强 Token、TLS、主机防火墙或私网,并让反向代理把全部路径转发到 Nginx。流式响应和 SSE 不应被代理缓冲。
|
||||
|
||||
外部端口、域名或协议变化时,必须同步设置公开地址:
|
||||
|
||||
|
|
@ -331,9 +331,9 @@ docker compose logs --tail=200 ccr
|
|||
|
||||
### 常见排查
|
||||
|
||||
- `/` 返回 `302`:正常,Nginx 正在跳转到带管理 Token 的页面。
|
||||
- `/` 返回 `302`:正常,Nginx 正在跳转到管理页,并保留请求里的查询参数。
|
||||
- `/health` 返回 `502`:它检查的是模型网关;首次启动尚未配置模型时属于预期行为。
|
||||
- 修改 Token 后 UI 返回 `401`:重新打开不带参数的根地址,关闭仍使用旧 Token 的标签页。
|
||||
- 修改 Token 后 UI 返回 `401`:用当前 Token 重新打开 `/?ccr_web_token=<token>`,关闭仍使用旧 Token 的标签页。
|
||||
- 重建后配置消失:检查是否仍挂载同一个 `/data` 卷;`docker compose down --volumes` 会删除数据卷。
|
||||
- 容器健康但模型请求失败:继续检查服务状态、供应商连通性、CCR 客户端 Key、路由和请求日志;容器健康只表示 Nginx / UI 可访问。
|
||||
|
||||
|
|
|
|||
|
|
@ -12,8 +12,10 @@ CCR_PUBLIC_PORT="${CCR_PUBLIC_PORT:-3458}"
|
|||
CCR_PUBLIC_BASE_URL="${CCR_PUBLIC_BASE_URL:-http://${CCR_PUBLIC_HOST}:${CCR_PUBLIC_PORT}}"
|
||||
CCR_NO_GATEWAY="${CCR_NO_GATEWAY:-0}"
|
||||
|
||||
CCR_WEB_AUTH_TOKEN_GENERATED=0
|
||||
if [ -z "${CCR_WEB_AUTH_TOKEN:-}" ]; then
|
||||
CCR_WEB_AUTH_TOKEN="$(node -e "process.stdout.write(require('node:crypto').randomBytes(32).toString('base64url'))")"
|
||||
CCR_WEB_AUTH_TOKEN_GENERATED=1
|
||||
fi
|
||||
CCR_WEB_AUTH_TOKEN_QUERY="$(node -e "process.stdout.write(encodeURIComponent(process.argv[1] || ''))" "${CCR_WEB_AUTH_TOKEN}")"
|
||||
|
||||
|
|
@ -141,14 +143,13 @@ server {
|
|||
|
||||
client_max_body_size 8m;
|
||||
|
||||
# The management token is never put in a response. A token the user supplies
|
||||
# in the query string is passed on to the page, which reads it from the URL.
|
||||
location = / {
|
||||
return 302 /pages/home/index.html?ccr_web_token=${CCR_WEB_AUTH_TOKEN_QUERY};
|
||||
return 302 /pages/home/index.html\$is_args\$args;
|
||||
}
|
||||
|
||||
location = /pages/home/index.html {
|
||||
if (\$arg_ccr_web_token = "") {
|
||||
return 302 /pages/home/index.html?ccr_web_token=${CCR_WEB_AUTH_TOKEN_QUERY};
|
||||
}
|
||||
try_files /pages/home/index.html =404;
|
||||
}
|
||||
|
||||
|
|
@ -192,6 +193,11 @@ server {
|
|||
}
|
||||
EOF
|
||||
|
||||
# A generated token is only reachable through the container logs.
|
||||
if [ "${CCR_WEB_AUTH_TOKEN_GENERATED}" = "1" ]; then
|
||||
echo "CCR management UI: ${CCR_PUBLIC_BASE_URL%/}/?ccr_web_token=${CCR_WEB_AUTH_TOKEN_QUERY}"
|
||||
fi
|
||||
|
||||
if [ "$#" -gt 0 ]; then
|
||||
exec "$@"
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ Nginx exposes:
|
|||
|
||||
| Path | Purpose |
|
||||
| --- | --- |
|
||||
| `/`, `/pages/home/index.html` | Management UI. The root path redirects to a page URL containing the management token. |
|
||||
| `/`, `/pages/home/index.html` | Management UI. The root path redirects to the page and keeps a `ccr_web_token` query parameter if the request has one. Nginx never adds the token. |
|
||||
| `/api/ccr/rpc` | Management RPC, protected by the management token. |
|
||||
| `/health` | Model gateway health; container or UI status is not reflected here. |
|
||||
| `/v1/*`, `/v1beta/*`, `/messages`, `/chat/completions`, `/responses`, `/interactions`, `/mcp/*` | Model and MCP gateway endpoints. |
|
||||
|
|
@ -45,7 +45,7 @@ npm run docker:compose:up
|
|||
docker compose logs -f ccr
|
||||
```
|
||||
|
||||
Open <http://127.0.0.1:3458>. On a fresh volume the management UI is available immediately; the model gateway only starts working after a provider and a model have been added.
|
||||
Open the management URL that the entrypoint prints in the container log, `http://127.0.0.1:3458/?ccr_web_token=<token>`. It is only printed when the token was generated; if you set `CCR_WEB_AUTH_TOKEN` yourself, add it to the URL. On a fresh volume the management UI is available immediately; the model gateway only starts working after a provider and a model have been added.
|
||||
|
||||
The npm Compose script prepares a local `../../next-ai/gateway` checkout before building so the image uses that plugin-capable ai-gateway runtime. If you run `docker compose up -d --build` directly, run `npm run docker:prepare-gateway` first. `npm run docker:build` performs the same preparation automatically.
|
||||
|
||||
|
|
@ -104,7 +104,7 @@ The repository also provides `npm run docker:run`. It uses `3458` and `ccr-data`
|
|||
| CCR client API key | Model gateway request authentication | **API Keys** page in the UI |
|
||||
| Upstream provider credential | CCR calling model providers | **Providers** page in the UI |
|
||||
|
||||
Without `CCR_WEB_AUTH_TOKEN`, the entrypoint generates a new random token on every container start. Opening the root address still works because Nginx redirects to a URL containing the current token; but persistent and remote deployments should pin a sufficiently long, strong token.
|
||||
Without `CCR_WEB_AUTH_TOKEN`, the entrypoint generates a new random token on every container start and prints the management URL once to the container log. Nginx never sends the token to a client: opening the root address without a token loads the page, but every management call fails with `401` until the token is supplied in the URL. The page keeps it for the browser tab session. Persistent and remote deployments should pin a sufficiently long, strong token.
|
||||
|
||||
Do not write the token directly into shell history. Create an environment file that stays out of version control:
|
||||
|
||||
|
|
@ -258,7 +258,7 @@ docker compose config
|
|||
|
||||
### The root address returns `302`
|
||||
|
||||
This is expected behavior. Nginx is redirecting the root address to a page URL containing the URL-encoded management token.
|
||||
This is expected behavior. Nginx redirects the root address to the management page and keeps the query string, so a `ccr_web_token` you supply survives. It does not add a token.
|
||||
|
||||
### `/health` returns `502`
|
||||
|
||||
|
|
@ -266,7 +266,7 @@ It checks the model gateway, not Nginx or the UI. A fresh volume without a confi
|
|||
|
||||
### The UI returns `401` after a token change
|
||||
|
||||
Reopen the bare root address so Nginx generates a URL containing the new token; close tabs and bookmarks that still use the old `ccr_web_token`.
|
||||
Reopen `/?ccr_web_token=<token>` with the current token (for a generated token, read it from `docker compose logs ccr`); close tabs and bookmarks that still use the old `ccr_web_token`.
|
||||
|
||||
### Clients still use the old port or domain
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ Nginx 对外提供:
|
|||
|
||||
| 路径 | 用途 |
|
||||
| --- | --- |
|
||||
| `/`、`/pages/home/index.html` | 管理 UI。根路径会跳转到带管理 Token 的页面。 |
|
||||
| `/`、`/pages/home/index.html` | 管理 UI。根路径会跳转到管理页,并保留请求中已有的 `ccr_web_token` 参数;Nginx 不会自行添加 Token。 |
|
||||
| `/api/ccr/rpc` | 需要管理 Token 的管理 RPC。 |
|
||||
| `/health` | 模型网关健康状态;容器或 UI 状态不在此接口反映。 |
|
||||
| `/v1/*`、`/v1beta/*`、`/messages`、`/chat/completions`、`/responses`、`/interactions`、`/mcp/*` | 模型和 MCP 网关接口。 |
|
||||
|
|
@ -45,7 +45,7 @@ npm run docker:compose:up
|
|||
docker compose logs -f ccr
|
||||
```
|
||||
|
||||
打开 <http://127.0.0.1:3458>。新数据卷上管理 UI 会立即可用;模型网关要在添加供应商和模型后才能正常启动。
|
||||
打开容器日志中打印的管理地址 `http://127.0.0.1:3458/?ccr_web_token=<token>`(仅在 Token 自动生成时打印;自行设置了 `CCR_WEB_AUTH_TOKEN` 时,请自行拼到地址后)。新数据卷上管理 UI 会立即可用;模型网关要在添加供应商和模型后才能正常启动。
|
||||
|
||||
这个 npm Compose 脚本会在构建前准备本机的 `../../next-ai/gateway` checkout,让镜像使用支持插件的 ai-gateway runtime。如果直接运行 `docker compose up -d --build`,请先执行 `npm run docker:prepare-gateway`。`npm run docker:build` 也会自动完成这一步。
|
||||
|
||||
|
|
@ -104,7 +104,7 @@ docker run -d \
|
|||
| CCR 客户端 API Key | 模型网关请求鉴权 | UI 的 **API 密钥** 页面 |
|
||||
| 上游供应商凭据 | CCR 调用模型供应商 | UI 的 **供应商** 页面 |
|
||||
|
||||
不设置 `CCR_WEB_AUTH_TOKEN` 时,EntryPoint 每次启动容器都会生成新的随机 Token。打开根地址仍可工作,因为 Nginx 会跳转到包含当前 Token 的 URL;但持久部署和远程部署应固定一个足够长的强 Token。
|
||||
不设置 `CCR_WEB_AUTH_TOKEN` 时,EntryPoint 每次启动容器都会生成新的随机 Token,并把管理地址在容器日志中打印一次。Nginx 不会向客户端返回 Token:不带 Token 打开根地址只能加载页面,所有管理请求都会返回 `401`,需要在 URL 中带上 Token。页面会在当前浏览器标签页会话内保留它。持久部署和远程部署应固定一个足够长的强 Token。
|
||||
|
||||
不要把 Token 直接写进 Shell 历史。可以创建不进入版本控制的环境文件:
|
||||
|
||||
|
|
@ -258,7 +258,7 @@ docker compose config
|
|||
|
||||
### 根地址返回 `302`
|
||||
|
||||
这是正常行为。Nginx 正在把根地址跳转到带 URL 编码管理 Token 的页面。
|
||||
这是正常行为。Nginx 把根地址跳转到管理页并保留查询参数,因此你提供的 `ccr_web_token` 不会丢失;Nginx 自身不会添加 Token。
|
||||
|
||||
### `/health` 返回 `502`
|
||||
|
||||
|
|
@ -266,7 +266,7 @@ docker compose config
|
|||
|
||||
### 修改 Token 后 UI 返回 `401`
|
||||
|
||||
重新打开不带参数的根地址,让 Nginx 生成包含新 Token 的 URL;关闭仍使用旧 `ccr_web_token` 的标签页和书签。
|
||||
用当前 Token 重新打开 `/?ccr_web_token=<token>`(自动生成的 Token 可在 `docker compose logs ccr` 中查看);关闭仍使用旧 `ccr_web_token` 的标签页和书签。
|
||||
|
||||
### 客户端仍使用旧端口或域名
|
||||
|
||||
|
|
|
|||
|
|
@ -61,14 +61,20 @@ try {
|
|||
const redirect = await fetch(baseUrl, { redirect: "manual" });
|
||||
assert.equal(redirect.status, 302);
|
||||
const redirectLocation = redirect.headers.get("location");
|
||||
assert.equal(redirectLocation, `/pages/home/index.html?ccr_web_token=${encodeURIComponent(token)}`);
|
||||
assert.equal(redirectLocation, "/pages/home/index.html", "An unauthenticated request must not be handed the management token");
|
||||
|
||||
const page = await fetch(`${baseUrl}${redirectLocation}`);
|
||||
const page = await fetch(`${baseUrl}${redirectLocation}`, { redirect: "manual" });
|
||||
assert.equal(page.status, 200);
|
||||
const pageHtml = await page.text();
|
||||
assert.match(pageHtml, /Claude Code Router/);
|
||||
assert.match(pageHtml, /web-client-bridge\.js/);
|
||||
|
||||
const suppliedToken = `/pages/home/index.html?ccr_web_token=${encodeURIComponent(token)}`;
|
||||
const tokenRedirect = await fetch(`${baseUrl}/?ccr_web_token=${encodeURIComponent(token)}`, { redirect: "manual" });
|
||||
assert.equal(tokenRedirect.status, 302);
|
||||
assert.equal(tokenRedirect.headers.get("location"), suppliedToken, "A token supplied by the user must survive the root redirect");
|
||||
assert.equal((await fetch(`${baseUrl}${suppliedToken}`, { redirect: "manual" })).status, 200);
|
||||
|
||||
const bridge = await fetch(`${baseUrl}/assets/web-client-bridge.js`);
|
||||
assert.equal(bridge.status, 200);
|
||||
assert.match(await bridge.text(), /getProviderPresets/);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue