## Implementation Plan: Restrict OpenAI Gateway to Internal Network ### 1. Nginx-Level Restriction **File:** `nginx/nginx.conf.template` Insert a dedicated location block for `/openai/` **before** the catch-all `location /` (around line 196). This ensures the rule is evaluated first. Add: ```nginx location /openai/ { allow 127.0.0.1; deny all; proxy_pass http://app; } ``` Place it immediately after the `/xmlrpc` block (line 184) and before the catch-all (line 196). No other configuration changes needed. ### 2. Application-Level Defense-in-Depth (Middleware) **File:** `devplacepy/main.py` Add a `@app.middleware("http")` function that checks `request.client.host` for any path starting with `/openai`. If not `127.0.0.1` or `::1`, return a 403 response. This catches requests that bypass nginx (e.g., direct access to the app port) and is testable. Place it near the existing middleware definitions (e.g., after the rate-limiter middleware around line 490). Example: ```python @app.middleware("http") async def restrict_openai_to_localhost(request: Request, call_next): if request.url.path.startswith("/openai"): host = request.client.host if request.client else None if host not in ("127.0.0.1", "::1"): return JSONResponse(status_code=403, content={"detail": "Forbidden"}) return await call_next(request) ``` Add import for `JSONResponse` if not present: `from fastapi.responses import JSONResponse`. ### 3. Unit Tests for the Middleware **File:** New test file `tests/unit/test_openai_ip_restriction.py` Test three cases: - Request from `127.0.0.1` → 200 (or pass-through) - Request from `::1` → 200 - Request from `10.0.0.1` → 403 Use `Starlette`’s `TestClient` against the app or mock the middleware directly. Include these tests in the unit test suite so they run with `make test-unit`. ### 4. Verify All Internal Consumers Still Work No code changes required—internal consumers already use `localhost` (`http://localhost:10500/openai/v1/...`). Confirm via grep that no consumer references a public IP for the gateway. --- ## Definition of Done - [ ] `nginx/nginx.conf.template` contains a `location /openai/` block with `allow 127.0.0.1; deny all; proxy_pass http://app;` before the catch-all `location /`. - [ ] `devplacepy/main.py` includes an `@app.middleware("http")` that checks `request.client.host` for `/openai` paths and returns 403 for non-localhost IPs. - [ ] New unit test file `tests/unit/test_openai_ip_restriction.py` covers localhost pass‑through and external IP rejection. - [ ] All existing unit tests still pass: ```bash pip install -e '.[dev]' -q && make test-unit ``` - [ ] Linting passes: ```bash pip install -q ruff && ruff check . ``` - [ ] (Manual) A real request from a non‑localhost IP to `/openai/v1/chat/completions` returns `403 Forbidden`. Requests from internal services (localhost) continue to work.