213 lines
4.9 KiB
Markdown
213 lines
4.9 KiB
Markdown
# API Proxy — HTTPS Reverse Proxy
|
|
|
|
Production-ready nginx reverse proxy in Docker for forwarding requests from an internal Kubernetes cluster to external APIs (ElevenLabs, OpenAI, etc.)
|
|
|
|
## Quick Start
|
|
|
|
### Prerequisites
|
|
- Ubuntu 24.04 VM
|
|
- Port 8080 open **only** to k8s cluster CIDR (firewall rule)
|
|
|
|
### Installation
|
|
|
|
```bash
|
|
# On a fresh VM (as root):
|
|
sudo bash scripts/install.sh
|
|
```
|
|
|
|
### Configuration
|
|
|
|
```bash
|
|
# Edit environment variables:
|
|
vi /opt/proxy/proxy-vm/.env
|
|
|
|
# Set your k8s cluster CIDR:
|
|
ALLOWED_CIDR=10.0.0.0/8,172.16.0.0/12
|
|
|
|
# Restart to apply:
|
|
cd /opt/proxy/proxy-vm
|
|
docker compose restart
|
|
```
|
|
|
|
### Verify
|
|
|
|
```bash
|
|
# Health check (no auth required):
|
|
curl http://localhost:8080/health
|
|
|
|
# Test with auth:
|
|
curl -H "X-Proxy-Token: YOUR_SECRET" http://localhost:8080/elevenlabs/v1/voices
|
|
```
|
|
|
|
---
|
|
|
|
## How to Add a New Upstream
|
|
|
|
Example: adding `api.anthropic.com` → `/anthropic/*`
|
|
|
|
### 1. Add upstream block in `nginx/nginx.conf`:
|
|
|
|
```nginx
|
|
upstream anthropic_backend {
|
|
server api.anthropic.com:443;
|
|
keepalive 32;
|
|
}
|
|
```
|
|
|
|
### 2. Add location block in the `server` section:
|
|
|
|
```nginx
|
|
location /anthropic/ {
|
|
if ($allowed_ip = 0) {
|
|
return 403 '{"error":"ip_not_allowed"}';
|
|
}
|
|
if ($auth_ok = 0) {
|
|
return 403 '{"error":"invalid_token"}';
|
|
}
|
|
|
|
rewrite ^/anthropic/(.*) /$1 break;
|
|
|
|
proxy_pass https://anthropic_backend;
|
|
proxy_ssl_server_name on;
|
|
proxy_ssl_name api.anthropic.com;
|
|
|
|
proxy_set_header Host api.anthropic.com;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_set_header Connection "";
|
|
proxy_set_header X-Proxy-Token "";
|
|
|
|
proxy_http_version 1.1;
|
|
|
|
proxy_buffering off;
|
|
proxy_request_buffering off;
|
|
proxy_read_timeout 120s;
|
|
proxy_send_timeout 120s;
|
|
}
|
|
```
|
|
|
|
### 3. Rebuild and restart:
|
|
|
|
```bash
|
|
cd /opt/proxy/proxy-vm
|
|
docker compose up -d --build
|
|
```
|
|
|
|
---
|
|
|
|
## K8s Side: How to Call the Proxy
|
|
|
|
### Environment Variables (in your Deployment/ConfigMap)
|
|
|
|
```yaml
|
|
env:
|
|
- name: PROXY_BASE_URL
|
|
value: "http://10.0.1.50:8080" # proxy VM internal IP
|
|
- name: PROXY_SECRET
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: api-proxy
|
|
key: token
|
|
```
|
|
|
|
### TypeScript Example
|
|
|
|
```typescript
|
|
// In tts-worker, replace ELEVENLABS_BASE_URL:
|
|
// Before: https://api.elevenlabs.io
|
|
// After: http://<proxy-vm-ip>:8080/elevenlabs
|
|
|
|
const response = await fetch(
|
|
`${process.env.PROXY_BASE_URL}/elevenlabs/v1/text-to-speech/${voiceId}`,
|
|
{
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'xi-api-key': process.env.ELEVENLABS_API_KEY, // passed through to upstream
|
|
'X-Proxy-Token': process.env.PROXY_SECRET, // validated by proxy
|
|
},
|
|
body: JSON.stringify(payload),
|
|
}
|
|
);
|
|
// Response is binary MP3, identical to direct ElevenLabs call
|
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
```
|
|
|
|
---
|
|
|
|
## Security Checklist
|
|
|
|
- [ ] `PROXY_SECRET` generated via `openssl rand -hex 32`
|
|
- [ ] `ALLOWED_CIDR` restricted to cluster CIDR only
|
|
- [ ] Port 8080 closed to the public (only k8s CIDR in firewall/NSG)
|
|
- [ ] VM has no public IP or is behind NAT
|
|
- [ ] Logs rotate (Docker logging driver configured: 50m x 5 files)
|
|
|
|
---
|
|
|
|
## Adding TLS (Self-Signed)
|
|
|
|
If you need HTTPS between k8s and the proxy:
|
|
|
|
```bash
|
|
# Generate self-signed cert:
|
|
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
|
|
-keyout /opt/proxy/proxy-vm/nginx/ssl/proxy.key \
|
|
-out /opt/proxy/proxy-vm/nginx/ssl/proxy.crt \
|
|
-subj "/CN=api-proxy"
|
|
```
|
|
|
|
Then in `nginx.conf`, change the server block:
|
|
|
|
```nginx
|
|
server {
|
|
listen 8443 ssl;
|
|
ssl_certificate /etc/nginx/ssl/proxy.crt;
|
|
ssl_certificate_key /etc/nginx/ssl/proxy.key;
|
|
# ... rest of config
|
|
}
|
|
```
|
|
|
|
Mount the certs in `docker-compose.yml`:
|
|
|
|
```yaml
|
|
volumes:
|
|
- ./nginx/ssl:/etc/nginx/ssl:ro
|
|
```
|
|
|
|
---
|
|
|
|
## Monitoring
|
|
|
|
### Tail logs in JSON format
|
|
|
|
```bash
|
|
docker compose logs -f proxy | jq .
|
|
```
|
|
|
|
### Count requests per upstream per minute
|
|
|
|
```bash
|
|
docker compose logs --since=1m proxy --no-log-prefix \
|
|
| jq -r '.uri' \
|
|
| cut -d'/' -f2 \
|
|
| sort | uniq -c | sort -rn
|
|
```
|
|
|
|
### Check container health
|
|
|
|
```bash
|
|
docker inspect --format='{{.State.Health.Status}}' api-proxy
|
|
```
|
|
|
|
---
|
|
|
|
## How It Works
|
|
|
|
1. A k8s pod sends an HTTP request to `http://<proxy-vm>:8080/elevenlabs/v1/text-to-speech/...` with the `X-Proxy-Token` header and the original API key (`xi-api-key`).
|
|
2. Nginx checks the source IP against the configured `ALLOWED_CIDR` geo block and validates the `X-Proxy-Token` against `PROXY_SECRET` — rejecting with 403 if either fails.
|
|
3. The `/elevenlabs/` prefix is stripped via rewrite, and the request is forwarded over HTTPS to `api.elevenlabs.io` with SNI enabled and keepalive connections.
|
|
4. The `X-Proxy-Token` header is removed before forwarding; all other headers (including `xi-api-key`) pass through unchanged.
|
|
5. The response streams back unbuffered to the pod — critical for binary audio data from ElevenLabs TTS.
|