Skip to main content
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:
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.
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.

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:
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:
LocationPurpose
/api/v1/setupBlocked by default (deny all)
/adminAdmin dashboard (optional auth_request)
/api/REST API (API key auth handled by Express)
/keys/Key derivation (viewer-facing, optional rate limiting)
/healthHealth 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:
location /admin {
    auth_request /auth/verify;
    auth_request_set $auth_status $upstream_status;
    error_page 401 = @auth_redirect;

    proxy_pass http://blindcast;
    # ... proxy headers ...
}
  1. Uncomment and configure the auth verification endpoint:
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;
}
  1. Add your auth backend (e.g., oauth2-proxy) as a Docker service alongside nginx.

Protecting /admin with HTTP Basic Auth

For simple deployments, use nginx’s built-in basic auth:
location /admin {
    auth_basic "BlindCast Admin";
    auth_basic_user_file /etc/nginx/.htpasswd;

    proxy_pass http://blindcast;
    # ... proxy headers ...
}
Generate the password file:
# 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:
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:
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:
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:
services:
  nginx:
    ports:
      - "443:443"
    volumes:
      - ./certs:/etc/nginx/certs:ro
For Let’s Encrypt, use certbot or a sidecar like nginx-proxy-acme.

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:
  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.
ValueBehavior
(not set)Default. Ignores X-Forwarded-* headers. req.ip returns the proxy’s IP.
trueTrust all proxies. Use only when the hop count is unknown (e.g., cloud load balancers).
falseSame as not set.
loopbackTrust 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/16Trust specific IP ranges (CIDR notation).
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.

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. Set the ADMIN_API_KEY environment variable to bootstrap with a known key. This skips the setup wizard entirely and eliminates the race condition:
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:
    docker compose exec nginx nginx -s reload
    

Next steps