↓ Skip to main content
← All research

Securing the Dashboard with Nginx Proxy Manager

3 min read Patrik Grobshäuser Archive

Research summary

Putting the OpenClaw Control UI behind HTTPS with basic auth, WebSocket passthrough, and a proper Content Security Policy — all through Nginx Proxy Manager.

Installation - This article is part of a series.
Part 2: This Article

Dear Readers,

with the gateway running on localhost:18789, next step is making the Control UI accessible from outside — with HTTPS, auth, and WebSocket support.

Docker + Nginx Proxy Manager
#

NPM runs as a Docker container. The setup is minimal:

# /root/nginx-proxy-manager/docker-compose.yml
services:
  npm:
    image: jc21/nginx-proxy-manager:latest
    container_name: nginx-proxy-manager
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "81:81"
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
    extra_hosts:
      - "host.docker.internal:host-gateway"

Start it:

cd /root/nginx-proxy-manager
docker compose up -d

Admin panel is on port 81 (default credentials on first login).

The Proxy Host
#

Created a proxy host for hub.clawd.it pointing to the OpenClaw gateway.

Gotcha: Don’t use host.docker.internal as the forward hostname. Docker resolves it at container start, but nginx can’t resolve it at runtime when it tries to proxy requests. Use the Docker bridge IP directly:

Forward Hostname: 172.17.0.1
Forward Port: 18789

Then enable WebSocket support and request a Let’s Encrypt SSL certificate.

WebSocket + Basic Auth: The Conflict
#

Here’s the tricky part. HTTP basic auth to protect the dashboard makes sense, but browsers cannot send basic auth headers with WebSocket upgrade requests. Enable basic auth globally and WebSocket connections fail silently.

The fix: an nginx map directive that conditionally disables auth for WebSocket upgrades.

Custom HTTP Config
#

The map directive needs to live at the http level. Luckily NPM supports custom configs in /data/nginx/custom/:

# /data/nginx/custom/http.conf
map $http_upgrade $auth_type {
  default "Authorization required";
  ~(?i)websocket "off";
}

If it’s a WebSocket upgrade, $auth_type becomes "off". Everything else gets the auth prompt.

Custom Server/Proxy Config
#

# /data/nginx/custom/server_proxy.conf
auth_basic $auth_type;
auth_basic_user_file /data/access/1;

proxy_hide_header Content-Security-Policy;
add_header Content-Security-Policy "default-src 'self'; base-uri 'none'; object-src 'none'; frame-ancestors 'none'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; style-src-elem 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' ws: wss:" always;

This gives us conditional basic auth (skipped for WebSocket) plus a tight Content Security Policy.

The htpasswd Gotcha
#

NPM manages its own access files and overwrites /data/access/1 when you make config changes through the UI. Custom htpasswd entries get wiped.

The workaround is regenerating the htpasswd file after any NPM config change:

docker exec nginx-proxy-manager \
  htpasswd -nb admin "your-strong-password" > \
  /root/nginx-proxy-manager/data/access/1

Not elegant, but it works.

OpenClaw Gateway Config
#

A few settings in /root/.openclaw/openclaw.json make the proxy setup work:

{
  "gateway": {
    "controlUi": {
      "dangerouslyDisableDeviceAuth": true,
      "allowInsecureAuth": true
    },
    "trustedProxies": ["172.18.0.0/16"]
  }
}
  • dangerouslyDisableDeviceAuth — skips the device pairing flow since we’re relying on nginx basic auth instead
  • allowInsecureAuth — lets auth work through a reverse proxy
  • trustedProxies — trusts the Docker network so the gateway sees real client IPs

The Result
#

hub.clawd.it now serves the OpenClaw Control UI with HTTPS via Let’s Encrypt, HTTP basic auth that automatically steps aside for WebSocket upgrades, proper CSP headers, and real-time WebSocket connections working through the proxy.

You can authenticate with the gateway token directly in the URL:

https://hub.clawd.it/#token=your-gateway-token

The WebSocket + basic auth conflict was the trickiest part here. That silent failure where connections just refuse to work can cost you a while if you don’t know about the map approach.


Next up: Connecting Claude Max and Telegram

Related