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:
- The container runtime image uses [`requirements.txt`](/d:/project/sentinel/requirements.txt) and intentionally installs only Python dependencies.
- Application source code is mounted by Compose at runtime, so the offline host does not need to rebuild the image just to load the current backend code.
## Offline Deployment Model
If your production machine has no internet access, the current repository should be used in this way:
1. Build the `key-ip-sentinel:latest` image on a machine with internet access.
2. Export that image as a tar archive.
3. Import the archive on the offline machine.
4. Place the repository files on the offline machine.
5. Start the stack with `docker compose up -d`, not `docker compose up --build -d`.
This works because:
-`Dockerfile` installs only Python dependencies into the image.
-`docker-compose.yml` mounts `./app` into the running `sentinel-app` container.
- The offline machine only needs the prebuilt image plus the repository files.
Important limitation:
- If you change Python dependencies in `requirements.txt`, you must rebuild and re-export the image on a connected machine.
- If you only change backend application code under `app/`, you do not need to rebuild the image; restarting the container is enough.
-`frontend/dist` must already exist before deployment, because Nginx serves the built frontend directly from the repository.
- The base images used by this stack, such as `nginx:alpine`, `redis:7-alpine`, and `postgres:16`, must also be available on the offline host in advance.
### Prepare images on a connected machine
Build and export the Sentinel runtime image:
```bash
docker build -t key-ip-sentinel:latest .
docker save -o key-ip-sentinel-latest.tar key-ip-sentinel:latest
```
Also export the public images used by Compose if the offline machine cannot pull them:
```bash
docker pull nginx:alpine
docker pull redis:7-alpine
docker pull postgres:16
docker save -o sentinel-support-images.tar nginx:alpine redis:7-alpine postgres:16
```
If the admin frontend is not already built, build it on the connected machine too:
```bash
cd frontend
npm install
npm run build
cd ..
```
Then copy these items to the offline machine:
- the full repository working tree
-`key-ip-sentinel-latest.tar`
-`sentinel-support-images.tar` if needed
### Import images on the offline machine
```bash
docker load -i key-ip-sentinel-latest.tar
docker load -i sentinel-support-images.tar
```
### Start on the offline machine
After `.env`, `frontend/dist`, and `shared_network` are ready:
- Ensure `key-ip-sentinel:latest`, `nginx:alpine`, `redis:7-alpine`, and `postgres:16` are already present on the host if the host cannot access the internet.
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.