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:
- Adds an nginx service on port 80 that proxies to BlindCast
- 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:
| 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.):
- 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 ...
}
- 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;
}
- 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:
-
Target group — Point to BlindCast container on port 4100. Use
/health for health checks.
-
Listener rules — Route all paths to the BlindCast target group.
-
Admin protection — Use the load balancer’s built-in auth integration:
-
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). |
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.
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:
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:
- Uncomment
proxy_pass in the /api/v1/setup location block
- Run
docker compose up and complete setup at /admin
- Re-block the endpoint by commenting out
proxy_pass and reloading nginx:
docker compose exec nginx nginx -s reload
Next steps