diff --git a/README.md b/README.md index c812a8b..903489e 100644 --- a/README.md +++ b/README.md @@ -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 \ No newline at end of file +- 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. \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index d1e8692..4820700 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/example.env b/example.env index e733537..e7b8386 100644 --- a/example.env +++ b/example.env @@ -1,4 +1,4 @@ -PORT=3000 +PORT=8080 TARGET_URLS=http://localhost:1234/v1 JWT_SECRET=your-jwt-secret-key-here AUTH_USERNAME=admin diff --git a/package.json b/package.json index 013ab96..6c8c4fb 100644 --- a/package.json +++ b/package.json @@ -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": { @@ -18,7 +18,8 @@ "openai", "certificate", "bearer auth", - "tls" + "tls", + "ai" ], "author": "Jayson Jacobs", "license": "Apache-2.0", diff --git a/src/controllers/nginx.ts b/src/controllers/nginx.ts index b5e1238..28d2325 100644 --- a/src/controllers/nginx.ts +++ b/src/controllers/nginx.ts @@ -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') @@ -60,13 +60,17 @@ export class NginxController { } private async writeDefaultConfig(req: Request, res: Response): Promise { - 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' }) diff --git a/src/static/nginx-server-template.conf b/src/static/nginx-server-template.conf index 79f1773..3f095ac 100644 --- a/src/static/nginx-server-template.conf +++ b/src/static/nginx-server-template.conf @@ -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; } diff --git a/src/utils/nginx.ts b/src/utils/nginx.ts index a3c8d79..221d7c1 100644 --- a/src/utils/nginx.ts +++ b/src/utils/nginx.ts @@ -62,9 +62,12 @@ export class NginxManager { return this.putFile(this.configPath, newConfig) } - async writeDefaultTemplate(domain: string): Promise { + async writeDefaultTemplate(domain: string, cidrGroups: string[]): Promise { 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) }