From 39c67d714ef091df1533181bdc3df82dc9ac3e07 Mon Sep 17 00:00:00 2001 From: DustHunter Date: Sat, 16 May 2026 09:29:27 +0800 Subject: [PATCH] fix: add API key authorization to /v2 endpoints (#42594) Signed-off-by: DustHunter Signed-off-by: wang.yuqi Co-authored-by: Qwen-Coder Co-authored-by: wang.yuqi --- docs/usage/security.md | 6 ++- .../test_optional_middleware.py | 49 +++++++++++++++++-- vllm/entrypoints/openai/server_utils.py | 7 ++- 3 files changed, 55 insertions(+), 7 deletions(-) diff --git a/docs/usage/security.md b/docs/usage/security.md index 49c4d1f0f3..92e5b5ecc9 100644 --- a/docs/usage/security.md +++ b/docs/usage/security.md @@ -128,7 +128,7 @@ firewall configuration instructions. ### Overview -The `--api-key` flag (or `VLLM_API_KEY` environment variable) provides authentication for vLLM's HTTP server, but **only for OpenAI-compatible API endpoints under the `/v1` path prefix**. Many other sensitive endpoints are exposed on the same HTTP server without any authentication enforcement. +The `--api-key` flag (or `VLLM_API_KEY` environment variable) provides authentication for vLLM's HTTP server, but **only for OpenAI-compatible API endpoints under the `/v1` path prefix**, and other similar `/v2`, `/inference` path prefix**. Many other sensitive endpoints are exposed on the same HTTP server without any authentication enforcement. **Important:** Do not rely exclusively on `--api-key` for securing access to vLLM. Additional security measures are required for production deployments. @@ -154,6 +154,9 @@ When `--api-key` is configured, the following `/v1` endpoints require Bearer tok - `/v1/rerank` - Reranking API - `/v1/load_lora_adapter` - Load a LoRA adapter (can alter model behavior; only available when `--enable-lora` is set and `VLLM_ALLOW_RUNTIME_LORA_UPDATING=True`) - `/v1/unload_lora_adapter` - Unload a LoRA adapter (can alter model behavior; only available when `--enable-lora` is set and `VLLM_ALLOW_RUNTIME_LORA_UPDATING=True`) +- `/inference/v1/generate` - Generate completions +- `/v2/embed` - Cohere Embed API +- `/v2/rerank` - Cohere Rerank API ### Unprotected Endpoints (No API Key Required) @@ -162,7 +165,6 @@ The following endpoints **do not require authentication** even when `--api-key` **Inference endpoints:** - `/invocations` - SageMaker-compatible endpoint (routes to the same inference functions as `/v1` endpoints) -- `/inference/v1/generate` - Generate completions - `/generative_scoring` - Generative scoring API - `/pooling` - Pooling API - `/classify` - Classification API diff --git a/tests/entrypoints/serve/instrumentator/test_optional_middleware.py b/tests/entrypoints/serve/instrumentator/test_optional_middleware.py index fef10cdc0c..865bd445c8 100644 --- a/tests/entrypoints/serve/instrumentator/test_optional_middleware.py +++ b/tests/entrypoints/serve/instrumentator/test_optional_middleware.py @@ -86,13 +86,56 @@ async def test_passed_api_token(server: RemoteOpenAIServer): indirect=True, ) @pytest.mark.asyncio -async def test_not_v1_api_token(server: RemoteOpenAIServer): - # Authorization check is skipped for any paths that - # don't start with /v1 (e.g. /v1/chat/completions). +async def test_not_v1_or_v2_path_skips_auth(server: RemoteOpenAIServer): + # Authorization check is skipped for paths that + # don't start with /v1 or /v2 (e.g. /health, /metrics). response = requests.get(server.url_for("health")) assert response.status_code == HTTPStatus.OK +# --------------------------------------------------------------------------- +# /v2 path authentication tests +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + "server", + [["--api-key", "test"]], + indirect=True, +) +@pytest.mark.asyncio +async def test_v2_endpoint_rejects_missing_api_token(server: RemoteOpenAIServer): + # /v2/embed should require authentication when --api-key is set. + body = { + "model": MODEL_NAME, + "texts": ["hello"], + "embedding_types": ["float"], + } + response = requests.post(server.url_for("/v2/embed"), json=body) + assert response.status_code == HTTPStatus.UNAUTHORIZED + + +@pytest.mark.parametrize( + "server", + [["--api-key", "test"]], + indirect=True, +) +@pytest.mark.asyncio +async def test_v2_endpoint_accepts_valid_api_token(server: RemoteOpenAIServer): + # /v2/embed should accept requests with a valid API key. + body = { + "model": MODEL_NAME, + "texts": ["hello"], + "embedding_types": ["float"], + } + response = requests.post( + server.url_for("/v2/embed"), + json=body, + headers={"Authorization": "Bearer test"}, + ) + assert response.status_code == HTTPStatus.OK + + @pytest.mark.parametrize( "server", ["--enable-request-id-headers"], diff --git a/vllm/entrypoints/openai/server_utils.py b/vllm/entrypoints/openai/server_utils.py index f8fe3366b0..b60119af55 100644 --- a/vllm/entrypoints/openai/server_utils.py +++ b/vllm/entrypoints/openai/server_utils.py @@ -35,6 +35,9 @@ from vllm.v1.engine.exceptions import EngineDeadError, EngineGenerateError logger = init_logger("vllm.entrypoints.openai.server_utils") +GUARDED_PREFIX = ("/v1", "/v2", "/inference") + + class AuthenticationMiddleware: """ Pure ASGI middleware that authenticates each request by checking @@ -44,7 +47,7 @@ class AuthenticationMiddleware: ----- There are two cases in which authentication is skipped: 1. The HTTP method is OPTIONS. - 2. The request path doesn't start with /v1 (e.g. /health). + 2. The request path doesn't start with GUARDED_PREFIX (e.g. /health). """ def __init__(self, app: ASGIApp, tokens: list[str]) -> None: @@ -80,7 +83,7 @@ class AuthenticationMiddleware: url_path = URL(scope=scope).path.removeprefix(root_path) headers = Headers(scope=scope) # Type narrow to satisfy mypy. - if url_path.startswith("/v1") and not self.verify_token(headers): + if url_path.startswith(GUARDED_PREFIX) and not self.verify_token(headers): response = JSONResponse(content={"error": "Unauthorized"}, status_code=401) return response(scope, receive, send) return self.app(scope, receive, send)