Skip to content

Commit 9e69113

Browse files
qin-ctxclaude
andauthored
fix(server): 未配置 root_api_key 时仅允许 localhost 绑定 (#310)
当 root_api_key 未配置时 resolve_identity() 将所有请求解析为 ROOT, 结合默认绑定 0.0.0.0 会导致任何网络请求均可执行管理员操作。 - 将默认 host 从 0.0.0.0 改为 127.0.0.1 - 添加 validate_server_config() 启动校验:无 key + 非 localhost 时拒绝启动 - 将 dev mode 日志从 info 升级为 warning - 更新中英文认证文档的开发模式段落 Closes #302 Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1a40839 commit 9e69113

File tree

5 files changed

+100
-9
lines changed

5 files changed

+100
-9
lines changed

docs/en/guides/04-authentication.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,17 +101,19 @@ client = ov.SyncHTTPClient(
101101

102102
## Development Mode
103103

104-
When no `root_api_key` is configured, authentication is disabled. All requests are accepted as ROOT with the default account.
104+
When no `root_api_key` is configured, authentication is disabled. All requests are accepted as ROOT with the default account. **This is only allowed when the server binds to localhost** (`127.0.0.1`, `localhost`, or `::1`). If `host` is set to a non-loopback address (e.g. `0.0.0.0`) without a `root_api_key`, the server will refuse to start.
105105

106106
```json
107107
{
108108
"server": {
109-
"host": "0.0.0.0",
109+
"host": "127.0.0.1",
110110
"port": 1933
111111
}
112112
}
113113
```
114114

115+
> **Security note:** The default `host` is `127.0.0.1`. If you need to expose the server on the network, you **must** configure `root_api_key`.
116+
115117
## Unauthenticated Endpoints
116118

117119
The `/health` endpoint never requires authentication. This allows load balancers and monitoring tools to check server health.

docs/zh/guides/04-authentication.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,17 +101,19 @@ client = ov.SyncHTTPClient(
101101

102102
## 开发模式
103103

104-
不配置 `root_api_key` 时,认证禁用所有请求以 ROOT 身份访问 default account。
104+
不配置 `root_api_key` 时,认证禁用所有请求以 ROOT 身份访问 default account**此模式仅允许在服务器绑定 localhost 时使用**`127.0.0.1``localhost``::1`)。如果 `host` 设置为非回环地址(如 `0.0.0.0`)且未配置 `root_api_key`,服务器将拒绝启动
105105

106106
```json
107107
{
108108
"server": {
109-
"host": "0.0.0.0",
109+
"host": "127.0.0.1",
110110
"port": 1933
111111
}
112112
}
113113
```
114114

115+
> **安全提示:** 默认 `host``127.0.0.1`。如需将服务暴露到网络,**必须**配置 `root_api_key`
116+
115117
## 无需认证的端点
116118

117119
`/health` 端点始终不需要认证,用于负载均衡器和监控工具检查服务健康状态。

openviking/server/app.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from fastapi.responses import JSONResponse
1212

1313
from openviking.server.api_keys import APIKeyManager
14-
from openviking.server.config import ServerConfig, load_server_config
14+
from openviking.server.config import ServerConfig, load_server_config, validate_server_config
1515
from openviking.server.dependencies import set_service
1616
from openviking.server.models import ERROR_CODE_TO_HTTP_STATUS, ErrorInfo, Response
1717
from openviking.server.routers import (
@@ -50,6 +50,8 @@ def create_app(
5050
if config is None:
5151
config = load_server_config()
5252

53+
validate_server_config(config)
54+
5355
@asynccontextmanager
5456
async def lifespan(app: FastAPI):
5557
"""Application lifespan handler."""
@@ -72,7 +74,13 @@ async def lifespan(app: FastAPI):
7274
logger.info("APIKeyManager initialized")
7375
else:
7476
app.state.api_key_manager = None
75-
logger.info("Dev mode: no root_api_key configured, authentication disabled")
77+
logger.warning(
78+
"Dev mode: no root_api_key configured, authentication disabled. "
79+
"This is allowed because the server is bound to localhost (%s). "
80+
"Do NOT expose this server to the network without configuring "
81+
"server.root_api_key in ov.conf.",
82+
config.host,
83+
)
7684

7785
yield
7886

openviking/server/config.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,26 @@
22
# SPDX-License-Identifier: Apache-2.0
33
"""Server configuration for OpenViking HTTP Server."""
44

5+
import sys
56
from dataclasses import dataclass, field
67
from typing import List, Optional
78

9+
from openviking_cli.utils import get_logger
810
from openviking_cli.utils.config.config_loader import (
911
DEFAULT_OV_CONF,
1012
OPENVIKING_CONFIG_ENV,
1113
load_json_config,
1214
resolve_config_path,
1315
)
1416

17+
logger = get_logger(__name__)
18+
1519

1620
@dataclass
1721
class ServerConfig:
1822
"""Server configuration (from the ``server`` section of ov.conf)."""
1923

20-
host: str = "0.0.0.0"
24+
host: str = "127.0.0.1"
2125
port: int = 1933
2226
root_api_key: Optional[str] = None
2327
cors_origins: List[str] = field(default_factory=lambda: ["*"])
@@ -59,10 +63,47 @@ def load_server_config(config_path: Optional[str] = None) -> ServerConfig:
5963
server_data = data.get("server", {})
6064

6165
config = ServerConfig(
62-
host=server_data.get("host", "0.0.0.0"),
66+
host=server_data.get("host", "127.0.0.1"),
6367
port=server_data.get("port", 1933),
6468
root_api_key=server_data.get("root_api_key"),
6569
cors_origins=server_data.get("cors_origins", ["*"]),
6670
)
6771

6872
return config
73+
74+
75+
_LOCALHOST_HOSTS = {"127.0.0.1", "localhost", "::1"}
76+
77+
78+
def _is_localhost(host: str) -> bool:
79+
"""Return True if *host* resolves to a loopback address."""
80+
return host in _LOCALHOST_HOSTS
81+
82+
83+
def validate_server_config(config: ServerConfig) -> None:
84+
"""Validate server config for safe startup.
85+
86+
When ``root_api_key`` is not set, authentication is disabled (dev mode).
87+
This is only acceptable when the server binds to localhost. Binding to a
88+
non-loopback address without authentication exposes an unauthenticated ROOT
89+
endpoint to the network.
90+
91+
Raises:
92+
SystemExit: If the configuration is unsafe.
93+
"""
94+
if config.root_api_key:
95+
return
96+
97+
if not _is_localhost(config.host):
98+
logger.error(
99+
"SECURITY: server.root_api_key is not configured and server.host "
100+
"is '%s' (non-localhost). This would expose an unauthenticated "
101+
"ROOT endpoint to the network.",
102+
config.host,
103+
)
104+
logger.error(
105+
"To fix, either:\n"
106+
" 1. Set server.root_api_key in ov.conf, or\n"
107+
' 2. Bind to localhost (server.host = "127.0.0.1")'
108+
)
109+
sys.exit(1)

tests/server/test_auth.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44
"""Tests for multi-tenant authentication (openviking/server/auth.py)."""
55

66
import httpx
7+
import pytest
78
import pytest_asyncio
89

910
from openviking.server.app import create_app
10-
from openviking.server.config import ServerConfig
11+
from openviking.server.config import ServerConfig, _is_localhost, validate_server_config
1112
from openviking.server.dependencies import set_service
1213
from openviking.service.core import OpenVikingService
1314
from openviking_cli.session.user_id import UserIdentifier
@@ -199,3 +200,40 @@ async def test_cross_tenant_session_get_returns_not_found(auth_client: httpx.Asy
199200
)
200201
assert cross_get.status_code == 404
201202
assert cross_get.json()["error"]["code"] == "NOT_FOUND"
203+
204+
205+
# ---- _is_localhost tests ----
206+
207+
208+
@pytest.mark.parametrize("host", ["127.0.0.1", "localhost", "::1"])
209+
def test_is_localhost_true(host: str):
210+
assert _is_localhost(host) is True
211+
212+
213+
@pytest.mark.parametrize("host", ["0.0.0.0", "::", "192.168.1.1", "10.0.0.1"])
214+
def test_is_localhost_false(host: str):
215+
assert _is_localhost(host) is False
216+
217+
218+
# ---- validate_server_config tests ----
219+
220+
221+
def test_validate_no_key_localhost_passes():
222+
"""No root_api_key + localhost should pass validation."""
223+
for host in ("127.0.0.1", "localhost", "::1"):
224+
config = ServerConfig(host=host, root_api_key=None)
225+
validate_server_config(config) # should not raise
226+
227+
228+
def test_validate_no_key_non_localhost_raises():
229+
"""No root_api_key + non-localhost should raise SystemExit."""
230+
config = ServerConfig(host="0.0.0.0", root_api_key=None)
231+
with pytest.raises(SystemExit):
232+
validate_server_config(config)
233+
234+
235+
def test_validate_with_key_any_host_passes():
236+
"""With root_api_key set, any host should pass validation."""
237+
for host in ("0.0.0.0", "::", "192.168.1.1", "127.0.0.1"):
238+
config = ServerConfig(host=host, root_api_key="some-secret-key")
239+
validate_server_config(config) # should not raise

0 commit comments

Comments
 (0)