Key-IP Sentinel is a FastAPI-based reverse proxy that enforces first-use IP binding for model API keys before traffic reaches a downstream New API service.
## Features
- First-use bind with HMAC-SHA256 token hashing, Redis cache-aside, and PostgreSQL CIDR matching.
- Streaming reverse proxy built on `httpx.AsyncClient` and FastAPI `StreamingResponse`.
- Trusted proxy IP extraction that only accepts `X-Real-IP` from configured upstream networks.
- Redis-backed intercept alert counters with webhook delivery and PostgreSQL audit logs.
- Admin API protected by JWT and Redis-backed login lockout.
- Vue 3 + Element Plus admin console for dashboarding, binding operations, audit logs, and live runtime settings.
- Docker Compose deployment with Nginx, app, Redis, and PostgreSQL.
- PostgreSQL stores authoritative token bindings and intercept logs.
- Archive retention removes inactive bindings from the active table after `ARCHIVE_DAYS`. A later request from the same token will bind again on first use.
-`SENTINEL_FAILSAFE_MODE=closed` rejects requests when both Redis and PostgreSQL are unavailable. `open` allows traffic through.
In practice, you may run New API in either of these two ways.
### Pattern A: Production machine, New API in its own compose
This is the recommended production arrangement.
New API keeps its own compose project and typically joins:
-`default`
-`shared_network`
That means New API can continue to use its own internal compose network for its own dependencies, while also exposing its service name to Sentinel through `shared_network`.
Example New API compose fragment:
```yaml
services:
new-api:
image: your-new-api-image
networks:
- default
- shared_network
networks:
shared_network:
external: true
```
With this setup, Sentinel still uses:
```text
DOWNSTREAM_URL=http://new-api:3000
```
### Pattern B: Test machine, New API started as a standalone container
On a test machine, you may not use a second compose project at all. Instead, you can start a standalone New API container with `docker run`, as long as that container also joins `shared_network`.
Example:
```bash
docker run -d \
--name new-api \
--network shared_network \
your-new-api-image
```
Important:
- The container name or reachable hostname must match what Sentinel uses in `DOWNSTREAM_URL`.
- If the container is not named `new-api`, then adjust `.env` accordingly.
- The port in `DOWNSTREAM_URL` is still the New API container's internal listening port.
Example:
```text
DOWNSTREAM_URL=http://new-api:3000
```
or, if your standalone container is named differently:
- Local Python development uses `uv` via [`pyproject.toml`](/d:/project/sentinel/pyproject.toml).
- Container builds still use [`requirements.txt`](/d:/project/sentinel/requirements.txt) because the Dockerfile is intentionally minimal and matches the delivery requirements.
1. Open `http://<host>:8016/health` and confirm it returns `{"status":"ok"}`.
2. Open `http://<host>:8016/admin/ui/` and log in with `ADMIN_PASSWORD`.
3. Send a real model API request to Sentinel, not to New API directly.
4. Check the `Bindings` page and confirm the token appears with a recorded binding rule.
Example test request:
```bash
curl http://<host>:8016/v1/models \
-H "Authorization: Bearer <your_api_key>"
```
If your client still points directly to New API, Sentinel will not see the request and no binding will be created.
## Which Port Should Clients Use?
With the current example compose in this repository:
- Sentinel public port: `8016`
- New API internal container port: usually `3000`
That means:
- **For testing now**, clients should call `http://<host>:8016/...`
- **Sentinel forwards internally** to `http://new-api:3000`
Do **not** point clients at host port `3000` if that bypasses Sentinel.
## How To Go Live Without Changing Client Config
If you want existing clients to stay unchanged, Sentinel must take over the **original external entrypoint** that clients already use.
Typical cutover strategy:
1. Keep New API on the shared internal Docker network.
2. Stop exposing New API directly to users.
3. Expose Sentinel on the old public host/port instead.
4. Keep `DOWNSTREAM_URL` pointing to the internal New API service on `shared_network`.
For example, if users currently call `http://host:3000`, then in production you should eventually expose Sentinel on that old public port and make New API internal-only.
The current `8016:80` mapping in [`docker-compose.yml`](/d:/project/sentinel/docker-compose.yml) is a **local test mapping**, not the only valid production setup.