From e86dcc17bc83570859083655b391eac8b25a46e8 Mon Sep 17 00:00:00 2001 From: Fin Griffin Date: Mon, 27 Apr 2026 15:13:27 +0100 Subject: [PATCH 1/5] feat(stacks): add auth layer to FastAPI for fine-tuning service --- stacks/finetuning-service/app/auth.py | 39 +++++++++++++++++++ stacks/finetuning-service/app/main.py | 9 ++++- .../finetuning-service/requirements.api.txt | 1 + 3 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 stacks/finetuning-service/app/auth.py diff --git a/stacks/finetuning-service/app/auth.py b/stacks/finetuning-service/app/auth.py new file mode 100644 index 0000000..4ddf4c9 --- /dev/null +++ b/stacks/finetuning-service/app/auth.py @@ -0,0 +1,39 @@ +"""Authentication dependency for the fine-tuning service.""" + +import os + +import httpx +from fastapi import Depends, HTTPException +from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer + +_bearer = HTTPBearer() + +LITELLM_URL = os.environ["LITELLM_URL"] +LITELLM_MASTER_KEY = os.environ["LITELLM_MASTER_KEY"] + + +async def verify_litellm_key( + credentials: HTTPAuthorizationCredentials = Depends(_bearer), +) -> None: + """Verify a Bearer token is a valid LiteLLM API key. + + Calls LiteLLM's /key/info endpoint using the service master key. + Returns normally if the key is valid; raises 401 otherwise. + + Args: + credentials: The Bearer token extracted from the Authorization + header by FastAPI's HTTPBearer scheme. + + Raises: + HTTPException: 401 if the token is not a valid LiteLLM key. + """ + async with httpx.AsyncClient() as client: + response = await client.get( + f"{LITELLM_URL}/key/info", + params={"key": credentials.credentials}, + headers={"Authorization": f"Bearer {LITELLM_MASTER_KEY}"}, + ) + if response.status_code != 200: + raise HTTPException( + status_code=401, detail="Invalid or inactive API key." + ) diff --git a/stacks/finetuning-service/app/main.py b/stacks/finetuning-service/app/main.py index 902072c..ad05ef5 100644 --- a/stacks/finetuning-service/app/main.py +++ b/stacks/finetuning-service/app/main.py @@ -3,8 +3,9 @@ from contextlib import asynccontextmanager from typing import AsyncGenerator -from fastapi import FastAPI +from fastapi import Depends, FastAPI +from .auth import verify_litellm_key from .database import init_db, recover_running_jobs from .routes import router @@ -31,7 +32,11 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: title="Splinter Fine-Tuning Service", lifespan=lifespan, ) -app.include_router(router, prefix="/v1/fine_tuning") +app.include_router( + router, + prefix="/v1/fine_tuning", + dependencies=[Depends(verify_litellm_key)], +) @app.get("/health") diff --git a/stacks/finetuning-service/requirements.api.txt b/stacks/finetuning-service/requirements.api.txt index 36b7287..d0f2079 100644 --- a/stacks/finetuning-service/requirements.api.txt +++ b/stacks/finetuning-service/requirements.api.txt @@ -1,4 +1,5 @@ uvicorn fastapi +httpx pyyaml typing_extensions From e3bb938099abf8487cdf3dfcea6fff02e888b3b4 Mon Sep 17 00:00:00 2001 From: Fin Griffin Date: Tue, 28 Apr 2026 10:53:53 +0100 Subject: [PATCH 2/5] feat(stacks): add docker network between llm and fine-tuning service --- stacks/finetuning-service/docker-compose.yml | 13 ++++++++++--- stacks/llm-service/docker-compose.yml | 6 +++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/stacks/finetuning-service/docker-compose.yml b/stacks/finetuning-service/docker-compose.yml index 01e23e6..03b0646 100644 --- a/stacks/finetuning-service/docker-compose.yml +++ b/stacks/finetuning-service/docker-compose.yml @@ -22,15 +22,16 @@ services: restart: unless-stopped environment: FINETUNING_PORT: ${FINETUNING_PORT} - LITELLM_URL: "http://host.docker.internal:${LITELLM_PORT}" + LITELLM_URL: "http://litellm-proxy:${LITELLM_PORT}" LITELLM_MASTER_KEY: ${LITELLM_MASTER_KEY} volumes: - ./config.yaml:/app/config.yaml:ro - finetuning_jobs:/data ports: - "127.0.0.1:${FINETUNING_PORT}:${FINETUNING_PORT}" - extra_hosts: - - "host.docker.internal:host-gateway" + networks: + - default + - finetuning healthcheck: test: ["CMD", "curl", "-f", "http://localhost:${FINETUNING_PORT}/health"] interval: 30s @@ -73,3 +74,9 @@ services: volumes: finetuning_jobs: + +networks: + default: + finetuning: + external: true + name: finetuning_default diff --git a/stacks/llm-service/docker-compose.yml b/stacks/llm-service/docker-compose.yml index 5adb749..ebb6a7e 100755 --- a/stacks/llm-service/docker-compose.yml +++ b/stacks/llm-service/docker-compose.yml @@ -299,6 +299,7 @@ services: networks: - default - monitoring + - finetuning volumes: postgres_data: @@ -309,4 +310,7 @@ networks: default: monitoring: external: true - name: monitoring_default \ No newline at end of file + name: monitoring_default + finetuning: + external: true + name: finetuning_default \ No newline at end of file From 68d7351b6f438e4b4ea61d8cb3f78cd63bc8256c Mon Sep 17 00:00:00 2001 From: Fin Griffin Date: Tue, 28 Apr 2026 11:03:36 +0100 Subject: [PATCH 3/5] feat(stacks): add user id whitelist for fine-tuning service --- stacks/finetuning-service/app/auth.py | 21 +++++++++++++++++++-- stacks/finetuning-service/config.yaml | 4 ++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/stacks/finetuning-service/app/auth.py b/stacks/finetuning-service/app/auth.py index 4ddf4c9..4b3dcc5 100644 --- a/stacks/finetuning-service/app/auth.py +++ b/stacks/finetuning-service/app/auth.py @@ -3,6 +3,7 @@ import os import httpx +import yaml from fastapi import Depends, HTTPException from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer @@ -12,20 +13,28 @@ LITELLM_MASTER_KEY = os.environ["LITELLM_MASTER_KEY"] +def _load_allowed_users() -> list[str]: + with open("/app/config.yaml") as f: + config = yaml.safe_load(f) + return config.get("allowed_users", []) + + async def verify_litellm_key( credentials: HTTPAuthorizationCredentials = Depends(_bearer), ) -> None: """Verify a Bearer token is a valid LiteLLM API key. Calls LiteLLM's /key/info endpoint using the service master key. - Returns normally if the key is valid; raises 401 otherwise. + Returns normally if the key is valid and the associated user is + on the allowlist (if one is configured); raises 401 otherwise. Args: credentials: The Bearer token extracted from the Authorization header by FastAPI's HTTPBearer scheme. Raises: - HTTPException: 401 if the token is not a valid LiteLLM key. + HTTPException: 401 if the token is invalid or the user is not + on the allowlist. """ async with httpx.AsyncClient() as client: response = await client.get( @@ -37,3 +46,11 @@ async def verify_litellm_key( raise HTTPException( status_code=401, detail="Invalid or inactive API key." ) + + allowed_users = _load_allowed_users() + if allowed_users: + user_id = response.json().get("info", {}).get("user_id") + if user_id not in allowed_users: + raise HTTPException( + status_code=401, detail="User not authorised for this service." + ) diff --git a/stacks/finetuning-service/config.yaml b/stacks/finetuning-service/config.yaml index 0ba4f58..c43c421 100644 --- a/stacks/finetuning-service/config.yaml +++ b/stacks/finetuning-service/config.yaml @@ -7,6 +7,10 @@ # # ============================================================================= +# If non-empty, only these LiteLLM user IDs may access the service. +# Leave empty ([]) to allow any valid LiteLLM key. +allowed_users: ["ft-test-allowed"] + allowed_models: - meta-llama/Llama-3.1-8B-Instruct From d010f8c08f99af62ddc7461a049bbc2ff50599f9 Mon Sep 17 00:00:00 2001 From: Fin Griffin Date: Tue, 28 Apr 2026 11:13:57 +0100 Subject: [PATCH 4/5] fix: migrate to gitignored whitelist for user auth --- .gitignore | 4 +++- stacks/finetuning-service/app/auth.py | 14 ++++++++++---- stacks/finetuning-service/config.yaml | 4 ---- stacks/finetuning-service/docker-compose.yml | 3 ++- stacks/finetuning-service/whitelist.txt.example | 6 ++++++ 5 files changed, 21 insertions(+), 10 deletions(-) create mode 100644 stacks/finetuning-service/whitelist.txt.example diff --git a/.gitignore b/.gitignore index 7bdb2c4..040754b 100755 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,6 @@ postgres_data/ .DS_Store Thumbs.db -.idea/ \ No newline at end of file +.idea/ + +whitelist.txt \ No newline at end of file diff --git a/stacks/finetuning-service/app/auth.py b/stacks/finetuning-service/app/auth.py index 4b3dcc5..190c9f0 100644 --- a/stacks/finetuning-service/app/auth.py +++ b/stacks/finetuning-service/app/auth.py @@ -3,7 +3,6 @@ import os import httpx -import yaml from fastapi import Depends, HTTPException from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer @@ -14,9 +13,16 @@ def _load_allowed_users() -> list[str]: - with open("/app/config.yaml") as f: - config = yaml.safe_load(f) - return config.get("allowed_users", []) + """Load allowed user IDs from whitelist.txt, one ID per line. + + Returns an empty list if the file does not exist, which permits + any valid LiteLLM key. + """ + try: + with open("/app/whitelist.txt") as f: + return [line.strip() for line in f if line.strip()] + except FileNotFoundError: + return [] async def verify_litellm_key( diff --git a/stacks/finetuning-service/config.yaml b/stacks/finetuning-service/config.yaml index c43c421..0ba4f58 100644 --- a/stacks/finetuning-service/config.yaml +++ b/stacks/finetuning-service/config.yaml @@ -7,10 +7,6 @@ # # ============================================================================= -# If non-empty, only these LiteLLM user IDs may access the service. -# Leave empty ([]) to allow any valid LiteLLM key. -allowed_users: ["ft-test-allowed"] - allowed_models: - meta-llama/Llama-3.1-8B-Instruct diff --git a/stacks/finetuning-service/docker-compose.yml b/stacks/finetuning-service/docker-compose.yml index 03b0646..7b67cbd 100644 --- a/stacks/finetuning-service/docker-compose.yml +++ b/stacks/finetuning-service/docker-compose.yml @@ -22,10 +22,11 @@ services: restart: unless-stopped environment: FINETUNING_PORT: ${FINETUNING_PORT} - LITELLM_URL: "http://litellm-proxy:${LITELLM_PORT}" + LITELLM_URL: "http://litellm-proxy:${LITELLM_PORT}}" LITELLM_MASTER_KEY: ${LITELLM_MASTER_KEY} volumes: - ./config.yaml:/app/config.yaml:ro + - ./whitelist.txt:/app/whitelist.txt:ro - finetuning_jobs:/data ports: - "127.0.0.1:${FINETUNING_PORT}:${FINETUNING_PORT}" diff --git a/stacks/finetuning-service/whitelist.txt.example b/stacks/finetuning-service/whitelist.txt.example new file mode 100644 index 0000000..70a47ca --- /dev/null +++ b/stacks/finetuning-service/whitelist.txt.example @@ -0,0 +1,6 @@ +# Allowed LiteLLM user IDs (one per line). +# Copy this file to whitelist.txt and add real user IDs. +# If whitelist.txt is absent, any valid LiteLLM key is accepted. + +user_abc123 +user_xyz456 From 46f5ef77e7ff8a7232802d377ba0e018af99dd57 Mon Sep 17 00:00:00 2001 From: Fin Griffin Date: Tue, 28 Apr 2026 11:40:19 +0100 Subject: [PATCH 5/5] fix: fix lite llm port typo --- stacks/finetuning-service/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stacks/finetuning-service/docker-compose.yml b/stacks/finetuning-service/docker-compose.yml index 7b67cbd..0a567cc 100644 --- a/stacks/finetuning-service/docker-compose.yml +++ b/stacks/finetuning-service/docker-compose.yml @@ -22,7 +22,7 @@ services: restart: unless-stopped environment: FINETUNING_PORT: ${FINETUNING_PORT} - LITELLM_URL: "http://litellm-proxy:${LITELLM_PORT}}" + LITELLM_URL: "http://litellm-proxy:${LITELLM_PORT}" LITELLM_MASTER_KEY: ${LITELLM_MASTER_KEY} volumes: - ./config.yaml:/app/config.yaml:ro