Securing Your Moltbot: Moving the Control UI Behind Tailscale
TL;DR: If you’re running Moltbot on a VPS, your admin control panel is probably exposed to the internet. Here’s how to lock it down using Tailscale Funnel while keeping your Telegram bot working.
The Problem I Discovered
I was setting up network hardening on my Moltbot VPS when I realized something uncomfortable: I could access the Moltbot control UI from my laptop – a laptop that wasn’t even on my Tailscale network.
Wait, what?
I’d gone through the trouble of setting up Tailscale, configuring UFW to block everything except the Tailscale interface, and feeling pretty good about my security posture. But there was a catch: port 50473 was left open for “webhooks.”
The problem? The Moltbot gateway serves both webhooks AND the control UI on the same port. So while I thought I was just exposing a webhook endpoint, I was actually exposing the entire admin interface to anyone who could find my server’s IP.
Understanding the Attack Surface
Here’s what was exposed:
- Control UI – Full admin access to the bot
- Gateway API – WebSocket connections for managing sessions
- Configuration – Ability to view and modify bot settings
Anyone who discovered my server’s IP could access the full admin interface.
The Solution: Localhost + Tailscale Funnel
The fix involves four key changes:
- Bind Docker ports to localhost only – The container can’t be reached from external interfaces
- Configure trustedProxies – So Moltbot recognizes Tailscale connections
- Use Tailscale Funnel – Provides HTTPS access with automatic TLS certificates
- Remove the public firewall rule – No more port 50473 exposed to the internet
Step 1: Update docker-compose.yml
The critical change is in the ports section:
ports:
# Before (vulnerable):
- "${PORT}:18789"
# After (secure):
- "127.0.0.1:${PORT}:18789"
That 127.0.0.1: prefix means Docker only binds to localhost, not all interfaces. This single change blocks all direct public access.
Step 2: Configure trustedProxies
Here’s a gotcha that took a while to figure out: when you put Moltbot behind Tailscale, it sees connections coming from the Docker network (172.x.x.x) with forwarded headers from Tailscale (100.x.x.x). By default, Moltbot doesn’t trust these proxy headers, which causes authentication failures.
The fix is to add trustedProxies to your gateway config:
{
"gateway": {
"trustedProxies": ["127.0.0.1", "172.16.0.0/12", "100.64.0.0/10"],
"auth": {
"mode": "token",
"token": "YOUR_GATEWAY_TOKEN"
},
"controlUi": {
"allowInsecureAuth": true
}
}
}
The ranges cover:
127.0.0.1– Localhost172.16.0.0/12– Docker networks100.64.0.0/10– Tailscale CGNAT range
Important: Keep allowInsecureAuth: true. The security now comes from the localhost binding and Tailscale, not from Moltbot’s auth layer. Setting it to false causes “pairing required” errors when behind a proxy.
Step 3: Update the Config in Docker Volume
Another gotcha: Moltbot’s config persists in a Docker volume. If you update docker-compose.yml, the startup script writes to the config file, but Moltbot merges it with the existing config in the volume. To ensure your changes take effect:
# Stop the container
cd /docker/clawdbot-ii5q && docker compose down
# Edit the config directly in the volume
cat > /var/lib/docker/volumes/clawdbot-ii5q_clawdbot_config/_data/clawdbot.json << 'EOF'
{
"gateway": {
"mode": "local",
"trustedProxies": ["127.0.0.1", "172.16.0.0/12", "100.64.0.0/10"],
"auth": {
"mode": "token",
"token": "YOUR_GATEWAY_TOKEN"
},
"controlUi": {
"allowInsecureAuth": true
}
},
"channels": {
"telegram": {
"enabled": true,
"botToken": "YOUR_TELEGRAM_BOT_TOKEN",
"dmPolicy": "open",
"allowFrom": ["*"]
}
}
}
EOF
# Start the container
docker compose up -d
Step 4: Set Up Tailscale Funnel
The Moltbot control UI requires a secure context – either HTTPS or localhost. A plain HTTP connection won’t work; you’ll get a WebSocket error:
disconnected (1008): control ui requires HTTPS or localhost (secure context)
Tailscale Funnel provides HTTPS with automatic TLS certificates and makes the service accessible from anywhere (not just your Tailscale network):
tailscale funnel --bg --https=443 http://127.0.0.1:50473
This gives you a URL like:
https://your-hostname.tailnet-name.ts.net/
Why Funnel instead of Serve? Tailscale Serve only works from devices on your Tailscale network. Funnel makes the endpoint publicly accessible – but the security comes from your gateway token, and the public can no longer access the raw port on your server.
Step 5: Close the Public Port
ufw delete allow 50473/tcp
The firewall now only allows traffic on the Tailscale interface:
Status: active
To Action From
-- ------ ----
Anywhere on tailscale0 ALLOW Anywhere
What About Telegram?
You might wonder: if the port is closed, how do webhooks work?
They don’t – and that’s fine. Moltbot uses polling by default, not webhooks. The bot makes outbound connections to Telegram’s API to check for new messages. No inbound port needed.
If you previously set a webhook (like I mistakenly did), delete it:
curl "https://api.telegram.org/botYOUR_TOKEN/deleteWebhook"
Then restart the container. Moltbot will automatically switch to polling mode.
The Result
Before:
Internet --> 76.13.23.47:50473 --> Control UI (EXPOSED)
After:
Internet --> 76.13.23.47:50473 --> BLOCKED (localhost binding)
Internet --> https://hostname.ts.net/?token=xxx --> Tailscale Funnel --> localhost:50473 --> Control UI
To access the control UI, you now need:
- The Tailscale Funnel HTTPS URL
- Your gateway token in the URL
Access via tokenized URL:
https://your-hostname.tailnet-name.ts.net/?token=YOUR_GATEWAY_TOKEN
The public IP no longer exposes anything on port 50473.
Testing the Fix
Quick verification:
# Should timeout/refuse (good - public port blocked)
curl --connect-timeout 5 http://YOUR_PUBLIC_IP:50473
# Should return HTML (Funnel working)
curl https://your-hostname.tailnet-name.ts.net/
# Check Docker is bound to localhost only
docker ps --format "{{.Ports}}"
# Should show: 127.0.0.1:50473->18789/tcp
Gotchas I Encountered
1. “pairing required” Error
If you see disconnected (1008): pairing required, check:
trustedProxiesincludes Docker and Tailscale rangesallowInsecureAuthistrue
2. “Proxy headers detected from untrusted address”
This log warning means trustedProxies isn’t configured correctly. Add the ranges listed above.
3. Config Not Updating
Moltbot’s config persists in a Docker volume. Stop the container, edit the volume directly, then restart.
4. Tailscale Serve vs Funnel
- Serve = Tailscale network only (requires Tailscale on your device)
- Funnel = Public internet via Tailscale infrastructure
Use Funnel if you want to access from devices without Tailscale.
Lessons Learned
- Audit what’s actually exposed – I assumed “webhook port” meant minimal exposure. It didn’t.
- Localhost binding is powerful – A simple
127.0.0.1:prefix in Docker completely changes the security model. - Trust your proxies – When putting services behind reverse proxies, configure
trustedProxiesor authentication will break. - Config persistence matters – Docker volumes retain config across restarts. Edit the volume directly for persistent changes.
- Tailscale Funnel is underrated – It elegantly provides HTTPS access to localhost services without exposing ports.
If you’re running Moltbot or any self-hosted service on a VPS, take a few minutes to audit what’s actually reachable from the internet. You might be surprised.

Leave a Reply