> ## Documentation Index
> Fetch the complete documentation index at: https://docs.blindcast.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Reverse Proxy

> Run BlindCast behind nginx, Caddy, or a cloud load balancer — with TLS termination, admin dashboard protection, and rate limiting.

BlindCast serves HTTP only. In production, put a reverse proxy in front of it to handle TLS termination, admin access control, and rate limiting.

## Why use a reverse proxy

* **TLS termination** — BlindCast doesn't handle HTTPS. Your proxy terminates TLS and forwards HTTP to BlindCast.
* **Admin protection** — The `/admin` dashboard serves static files without authentication. A proxy can gate it behind SSO, HTTP Basic Auth, or IP allowlists.
* **Rate limiting** — Protect key derivation (`/keys`) and API endpoints from abuse.
* **Setup endpoint security** — Block `POST /api/v1/setup` from the internet to prevent unauthorized admin key creation.

## nginx

### Quick start with Docker Compose

The server ships with a proxy overlay that adds nginx in front of BlindCast:

```bash theme={null}
cd packages/server

# Start with the proxy overlay
docker compose -f docker-compose.yml -f docker-compose.proxy.yml up -d
```

This does two things:

1. Adds an nginx service on port 80 that proxies to BlindCast
2. Sets `TRUST_PROXY=1` on BlindCast so Express reads real client IPs from the forwarded header (trusts exactly one proxy hop)

In production, use a firewall rule to block direct access to port 4100 so all traffic flows through nginx.

<Warning>
  The setup endpoint (`POST /api/v1/setup`) is blocked by default in the proxy config. Set `ADMIN_API_KEY` as an environment variable instead. See [Setup wizard security](#setup-wizard-security).
</Warning>

### Configuration reference

The proxy config is at `docker/nginx-proxy.conf`. Key sections:

**Forwarded headers** — Every proxied route sends `X-Forwarded-For`, `X-Forwarded-Proto`, `X-Real-IP`, and `Host` to BlindCast:

```nginx theme={null}
proxy_set_header Host $host;
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;
```

**Route breakdown:**

| Location        | Purpose                                                |
| --------------- | ------------------------------------------------------ |
| `/api/v1/setup` | Blocked by default (`deny all`)                        |
| `/admin`        | Admin dashboard (optional `auth_request`)              |
| `/api/`         | REST API (API key auth handled by Express)             |
| `/keys/`        | Key derivation (viewer-facing, optional rate limiting) |
| `/health`       | Health check (proxied, `access_log off`)               |

**CORS headers** — The proxy does *not* add CORS headers. Express handles CORS for all proxied routes. Adding CORS at both layers causes duplicate `Access-Control-Allow-Origin` headers, which browsers reject.

### Protecting /admin with auth\_request

Use nginx's `auth_request` to gate the admin dashboard behind an external auth provider (Auth0, Okta, Azure AD via oauth2-proxy, etc.):

1. Uncomment the `auth_request` lines in `nginx-proxy.conf`:

```nginx theme={null}
location /admin {
    auth_request /auth/verify;
    auth_request_set $auth_status $upstream_status;
    error_page 401 = @auth_redirect;

    proxy_pass http://blindcast;
    # ... proxy headers ...
}
```

2. Uncomment and configure the auth verification endpoint:

```nginx theme={null}
location = /auth/verify {
    internal;
    proxy_pass http://your-auth-backend:4180/oauth2/auth;
    proxy_pass_request_body off;
    proxy_set_header Content-Length "";
    proxy_set_header X-Original-URI $request_uri;
}

location @auth_redirect {
    return 302 https://your-idp.example.com/login?redirect=$request_uri;
}
```

3. Add your auth backend (e.g., [oauth2-proxy](https://oauth2-proxy.github.io/oauth2-proxy/)) as a Docker service alongside nginx.

### Protecting /admin with HTTP Basic Auth

For simple deployments, use nginx's built-in basic auth:

```nginx theme={null}
location /admin {
    auth_basic "BlindCast Admin";
    auth_basic_user_file /etc/nginx/.htpasswd;

    proxy_pass http://blindcast;
    # ... proxy headers ...
}
```

Generate the password file:

```bash theme={null}
# Install htpasswd (part of apache2-utils)
htpasswd -c .htpasswd admin
```

### Rate limiting

Uncomment the `limit_req_zone` directives at the top of `nginx-proxy.conf`:

```nginx theme={null}
limit_req_zone $binary_remote_addr zone=keys:10m rate=60r/m;
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/m;
```

Then uncomment `limit_req` in the relevant location blocks:

```nginx theme={null}
location /keys/ {
    limit_req zone=keys burst=20 nodelay;
    # ...
}
```

### TLS termination

The config includes a commented-out TLS server block. Uncomment it and provide your certificate paths:

```nginx theme={null}
server {
    listen 443 ssl;
    server_name your-domain.example.com;

    ssl_certificate /etc/nginx/certs/fullchain.pem;
    ssl_certificate_key /etc/nginx/certs/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    # Copy location blocks from the HTTP server
}
```

Mount certificates in the compose override:

```yaml theme={null}
services:
  nginx:
    ports:
      - "443:443"
    volumes:
      - ./certs:/etc/nginx/certs:ro
```

For Let's Encrypt, use [certbot](https://certbot.eff.org/) or a sidecar like [nginx-proxy-acme](https://github.com/nginx-proxy/acme-companion).

## Caddy

Caddy handles TLS automatically with Let's Encrypt. A minimal Caddyfile:

```
your-domain.example.com {
    reverse_proxy blindcast:4100
}
```

### Forward auth for /admin

Use Caddy's `forward_auth` to protect the admin dashboard:

```
your-domain.example.com {
    @admin path /admin*

    handle @admin {
        forward_auth your-auth-backend:4180 {
            uri /oauth2/auth
        }
        reverse_proxy blindcast:4100
    }

    handle {
        reverse_proxy blindcast:4100
    }
}
```

### Blocking the setup endpoint

```
your-domain.example.com {
    @setup path /api/v1/setup
    respond @setup "Blocked" 403

    reverse_proxy blindcast:4100
}
```

## AWS ALB / Cloud Load Balancers

For AWS ALB, GCP Cloud Load Balancing, or Cloudflare:

1. **Target group** — Point to BlindCast container on port 4100. Use `/health` for health checks.

2. **Listener rules** — Route all paths to the BlindCast target group.

3. **Admin protection** — Use the load balancer's built-in auth integration:
   * **AWS ALB**: Add an [authenticate-cognito](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/listener-authenticate-users.html) or authenticate-oidc action on the `/admin*` path rule.
   * **GCP**: Use [Identity-Aware Proxy (IAP)](https://cloud.google.com/iap/docs) to gate `/admin`.
   * **Cloudflare**: Use [Cloudflare Access](https://developers.cloudflare.com/cloudflare-one/applications/) to protect the `/admin` path.

4. **Set `TRUST_PROXY`** — Cloud load balancers add their own `X-Forwarded-For` headers. Set `TRUST_PROXY=true` on the BlindCast container.

## TRUST\_PROXY environment variable

When BlindCast runs behind a proxy, set `TRUST_PROXY` so Express reads the real client IP from forwarded headers.

| Value           | Behavior                                                                                           |
| --------------- | -------------------------------------------------------------------------------------------------- |
| *(not set)*     | Default. Ignores `X-Forwarded-*` headers. `req.ip` returns the proxy's IP.                         |
| `true`          | Trust all proxies. Use only when the hop count is unknown (e.g., cloud load balancers).            |
| `false`         | Same as not set.                                                                                   |
| `loopback`      | Trust loopback addresses (`127.0.0.1`, `::1`).                                                     |
| `1`, `2`, ...   | Trust exactly *N* proxy hops. **Recommended** for known topologies (e.g., `1` for a single nginx). |
| `172.18.0.0/16` | Trust specific IP ranges (CIDR notation).                                                          |

<Warning>
  Prefer a numeric hop count (`TRUST_PROXY=1`) over `true` when the proxy topology is known. With `true`, Express trusts all entries in `X-Forwarded-For`, so a client can prepend a spoofed IP. With `1`, Express only trusts the entry added by the immediate proxy. Never set any `TRUST_PROXY` value unless BlindCast is actually behind a proxy.
</Warning>

## Setup wizard security

The setup endpoint (`POST /api/v1/setup`) creates the first admin API key. It has no authentication — it only works when zero API keys exist in the database. This creates a race condition: if the server is network-accessible before you run setup, an attacker could claim the admin key first.

### Recommended: use ADMIN\_API\_KEY in production

Set the `ADMIN_API_KEY` environment variable to bootstrap with a known key. This skips the setup wizard entirely and eliminates the race condition:

```yaml theme={null}
services:
  blindcast:
    environment:
      ADMIN_API_KEY: bk_your_bootstrap_key_here
```

The bootstrap key works immediately and has `admin` scope. Store it in a secret manager (AWS Secrets Manager, HashiCorp Vault, Doppler).

### Alternative: temporarily unblock the setup endpoint

If you prefer the setup wizard:

1. Uncomment `proxy_pass` in the `/api/v1/setup` location block
2. Run `docker compose up` and complete setup at `/admin`
3. Re-block the endpoint by commenting out `proxy_pass` and reloading nginx:
   ```bash theme={null}
   docker compose exec nginx nginx -s reload
   ```

## Next steps

* [Docker Setup](/server/docker) — base Docker Compose configuration
* [API Keys](/server/api-keys) — key scopes and management
* [Admin Dashboard](/server/admin-dashboard) — browser-based management
* [Production Checklist](/production/checklist) — full production readiness guide
