Skip to content

Commit

Permalink
Merge pull request #9 from j4ys0n/nginx-config
Browse files Browse the repository at this point in the history
add cidr groups
  • Loading branch information
j4ys0n authored Jul 12, 2024
2 parents 4bf1f43 + 210da49 commit 70bacc5
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 19 deletions.
137 changes: 136 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,139 @@ Manages Nginx for reverse proxy to multiple LLMs, with TLS & Bearer Auth tokens.
- Uses Let's Encrypt for TLS certificates
- Uses certbot for certificate issuance and renewal
- Uses Nginx as a public-domain reverse proxy to add TLS
- Uses JWT for bearer authentication
- Uses JWT for bearer authentication
- Auth & nginx enpoints are IP restricted.

***All requests to `/v1/*` are proxied to the LLM APIs except for `/v1/models`***

`/v1/models` is a special endpoint that returns the list of models available from all LLM APIs.

## How to use

Docker compose is going to be the easiest way to get up and running, but you could also manually run the docker image. Before you do anything else, if you don't have a cloudflare account, sign up now - it's free. You will need to create an API Token with the "Zone", "DNS", "Edit" permissions. This will be used when issuing your certs to verify that you own the domain you want to use for TLS. After you have your key, set up a new DNS record to point to your IP address. This can be proxied on the cloudflare end.

After this, you'll set up your files, start the container, hit a few routes and you'll be good to go!

#### **Don't forget to forward port 443 on your router!**

I'll use `localhost`, `192.168.1.100` or `your.domain.com` as an examples, but fill these in with your domain or IP address.

### Files

Here's what you'll need in your docker-compose file:
```yaml
version: '3.6'

services:
llmp:
image: ghcr.io/j4ys0n/llm-proxy:1.2.0
container_name: llmp
hostname: llmp
restart: unless-stopped
ports:
- 8080:8080
- 443:443
volumes:
- .env:/app/.env # environment variables
- ./data:/app/data # any data that the app needs to persist
- ./cloudflare_credentials:/opt/cloudflare/credentials # cloudflare api token
- ./nginx:/etc/nginx/conf.d # nginx configs
- ./certs:/etc/letsencrypt # tsl certificates
```
Here's what your `.env` file should look like:
```bash
PORT=8080 # node.js listen port. right now nginx is hard coded, so don't change this.
TARGET_URLS=http://localhost:1234,http://192.168.1.100:1234 # list of api endpoints (don't add /v1)
JWT_SECRET=randomly_generated_secret # secret for JWT token generation, change this!
AUTH_USERNAME=admin
AUTH_PASSWORD=secure_password # super basic auth credentials for the admin interface
```

Here's what your cloudflare_credendials file should look like
```bash
dns_cloudflare_api_token = your_token_here
```

### Routes

You'll need to use the local, unsecured endpoints to get set up initially. The `/auth/token` endpoint is the only endpoint that does't need an Authorization header and token.

Generate tokens.

`POST http://192.168.1.100:8080/auth/token`
```json
{
"username": "admin",
"password": "secure_password"
}
```
response:
```json
{
"token": "generated_token_here"
}
```

#### All of the routes below need a bearer token in the Authorization header.
`Authorization: Bearer generated_token_here`

Get TLS certificates.

`POST http://192.168.1.100:8080/nginx/certificates/obtain`
```json
{
"domains": ["your.domain.com"]
}
```
response:
```json
{
"success": true,
"message": "Certificates obtained successfully."
}
```

Write default config with your domain. (this should be sufficient for you, fill in your domain and cider groups)

Note: you can add multiple CIDR groups if you have multiple internal IP ranges you want admin functions to be accessible to. This is all of the routes that start with `/auth` or `/nginx`.

Hint: `192.168.1.0/24` will allow all IPs from `192.168.1.1` - `192.168.1.254`. `192.168.1.111/32` will only allow `192.168.1.111`.

`POST http://192.168.1.100:8080/nginx/config/write-default`
```json
{
"domain": "your.domain.com",
"cidrGroups": ["192.168.1.0/24"]
}
```
response:
```json
{
"success": true,
"message": "Default config written successfully"
}
```

Reload nginx to apply changes.
`GET http://192.168.1.100:8080/nginx/reload`
response:
```json
{
"success": true,
"message": "Nginx configuration reloaded successfully."
}
```

***If you made it here, you should be good to go!***

Other available endpoints (these will be documented better in the future)

`GET /nginx/config/get` - get current nginx config as a string.

`POST /nginx/config/update` - update the nginx config with a custom domain.
Body: `{ "config": string }`

`GET /nginx/config/get-default` - get default nginx config template.

`GET /nginx/certificates/renew` - renew certificates for your domains.
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: '3.6'

services:
llmp:
image: ghcr.io/j4ys0n/llm-proxy:1.1.0
image: ghcr.io/j4ys0n/llm-proxy:1.2.0
container_name: llmp
hostname: llmp
restart: unless-stopped
Expand Down
2 changes: 1 addition & 1 deletion example.env
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
PORT=3000
PORT=8080
TARGET_URLS=http://localhost:1234/v1
JWT_SECRET=your-jwt-secret-key-here
AUTH_USERNAME=admin
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "llm-proxy",
"version": "1.1.5",
"version": "1.2.0",
"description": "Manages Nginx for reverse proxy to multiple LLMs, with TLS & Bearer Auth tokens",
"main": "dist/index.js",
"scripts": {
Expand All @@ -18,7 +18,8 @@
"openai",
"certificate",
"bearer auth",
"tls"
"tls",
"ai"
],
"author": "Jayson Jacobs",
"license": "Apache-2.0",
Expand Down
20 changes: 12 additions & 8 deletions src/controllers/nginx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ export class NginxController {
}

public registerRoutes(): void {
this.app.post('/nginx/reload', ...this.requestHandlers, this.reloadNginx.bind(this))
this.app.get('/nginx/reload', ...this.requestHandlers, this.reloadNginx.bind(this))
this.app.post('/nginx/config/update', ...this.requestHandlers, this.updateConfig.bind(this))
this.app.get('/nginx/config/get', ...this.requestHandlers, this.getConfig.bind(this))
this.app.get('/nginx/config/get-default', ...this.requestHandlers, this.getDefaultConfig.bind(this))
this.app.get('/nginx/config/write-default', ...this.requestHandlers, this.writeDefaultConfig.bind(this))
this.app.post('/nginx/config/write-default', ...this.requestHandlers, this.writeDefaultConfig.bind(this))
this.app.post('/nginx/certificates/obtain', ...this.requestHandlers, this.obtainCertificates.bind(this))
this.app.get('/nginx/certificates/renew', ...this.requestHandlers, this.renewCertificates.bind(this))
log('info', 'NginxController initialized')
Expand Down Expand Up @@ -60,13 +60,17 @@ export class NginxController {
}

private async writeDefaultConfig(req: Request, res: Response): Promise<void> {
if (req.body != null && req.body.domain != null) {
const domain = req.body.domain
const { success, message } = await this.nginxManager.writeDefaultTemplate(domain)
if (success) {
res.json({ success, message: 'Default config written successfully' })
if (req.body != null && req.body.domain != null && req.body.cidrGroups != null) {
const { domain, cidrGroups } = req.body
if (Array.isArray(cidrGroups) && typeof domain === 'string') {
const { success, message } = await this.nginxManager.writeDefaultTemplate(domain, cidrGroups)
if (success) {
res.json({ success, message: 'Default config written successfully' })
} else {
res.status(500).json({ success, message })
}
} else {
res.status(500).json({ success, message })
res.status(400).json({ success: false, message: 'Invalid request body' })
}
} else {
res.status(400).json({ success: false, message: 'Invalid request body' })
Expand Down
5 changes: 1 addition & 4 deletions src/static/nginx-server-template.conf
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,7 @@ server {
proxy_buffering off;
client_max_body_size 0;
proxy_read_timeout 36000s;
allow 10.1.0.0/16;
allow 10.6.0.0/24;
allow 10.9.9.0/24;
allow 10.99.10.0/24;
{{allowedIPs}}
deny all;
}

Expand Down
7 changes: 5 additions & 2 deletions src/utils/nginx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,12 @@ export class NginxManager {
return this.putFile(this.configPath, newConfig)
}

async writeDefaultTemplate(domain: string): Promise<NginxResponse> {
async writeDefaultTemplate(domain: string, cidrGroups: string[]): Promise<NginxResponse> {
const templateContent = await readFile(CONFIG_TEMPLATE_PATH, 'utf-8')
const content = templateContent.replace(/{{domainName}}/g, domain)
const allowedIPs = cidrGroups.map((g) => ` allow ${g};\n`).reduce((acc, curr) => acc + curr, '')
const content = templateContent
.replace(/{{domainName}}/g, domain)
.replace(/{{allowedIPs}}/g, allowedIPs)
return this.putFile(this.configPath, content)
}

Expand Down

0 comments on commit 70bacc5

Please sign in to comment.