Notifications
Cloudflare Tunnels

Cloudflare Tunnels (Frigate & MQTT)

This guide shows a reliable way to expose:

  • Frigate at https://nvr.example.com
  • MQTT over WebSockets at wss://mqtt.example.com

…without requiring a VPN.

It also covers the most important hardening step: Cloudflare Access with a Service Token, which Viewu can send as headers.

If you only need private remote access for your own devices, Tailscale is usually simpler. Cloudflare Tunnel is best when you want “works anywhere” access without a VPN.


Quick start (Pattern A: tunnel directly to Frigate + Mosquitto)

This is the most common configuration and the one this page optimizes for.

What you will end up with

  • nvr.example.com → Cloudflare Tunnel → http://127.0.0.1:5000 (Frigate)
  • mqtt.example.com → Cloudflare Tunnel → http://127.0.0.1:9001 (Mosquitto WebSockets)
  • Both hostnames protected by Cloudflare Access (Service Token)

Minimal checklist (do these in order)

  1. Confirm Frigate works locally:
    • curl http://127.0.0.1:5000/api/version
  2. Enable Mosquitto WebSockets (port 9001):
  3. Install cloudflared on the same machine when possible
  4. Create a tunnel and configure ingress routes for nvr.example.com and mqtt.example.com
  5. Confirm DNS records exist for both hostnames
  6. Create Cloudflare Access Applications for both hostnames
  7. Create a Service Token and allow it in policies
  8. Verify with curl (Frigate + MQTT handshake)
  9. Configure Viewu (NVR + MQTT + headers)
  10. Re-test and only then start debugging the app

Common trap: people configure Cloudflare correctly but skip the local verification step. If your local origin services are not correct, Cloudflare will only hide the problem behind “502” and timeouts.


Step 0: Two concepts to keep straight (plain English)

Cloudflare Tunnel

  • Runs an agent (cloudflared) on your network
  • Creates outbound connections to Cloudflare
  • Lets Cloudflare forward requests to your internal services

Cloudflare Access

  • Adds an authentication/authorization layer in front of a hostname
  • For automation (apps), you typically use a Service Token
  • Viewu sends that Service Token via headers so requests can be allowed without an interactive login

Step 1: Configure Mosquitto for WebSockets

Cloudflare Tunnel supports WebSockets, but Mosquitto must listen on a WebSockets port.

On the Mosquitto host, add a listener (example port 9001).

Create or edit a config file (common location: /etc/mosquitto/conf.d/websockets.conf):

listener 9001 0.0.0.0
protocol websockets
allow_anonymous false
password_file /etc/mosquitto/passwd

Restart Mosquitto:

sudo systemctl restart mosquitto
sudo systemctl status mosquitto --no-pager

Tip: Run cloudflared on the same machine as Mosquitto when possible. It reduces networking surprises and lets you use 127.0.0.1 as the tunnel origin.


Step 2: Create the Cloudflare Tunnel

In Cloudflare Dashboard:

  1. Go to Zero Trust → Access → Tunnels (or Network → Tunnels depending on UI).
  2. Click Create a tunnel.
  3. Name it (example: viewu-home).
  4. Follow the install instructions for your platform.

On Linux you typically end up with a service like:

sudo systemctl status cloudflared --no-pager

Step 3: Add tunnel routes (Ingress)

You can configure routes in the dashboard or via a config.yml. A config file is easier to audit and repeat.

Example config.yml

tunnel: <YOUR_TUNNEL_ID>
credentials-file: /etc/cloudflared/<YOUR_TUNNEL_ID>.json
 
ingress:
  - hostname: nvr.example.com
    service: http://127.0.0.1:5000
 
  - hostname: mqtt.example.com
    service: http://127.0.0.1:9001
 
  - service: http_status:404

Critical detail: the origin service should be HTTP (not HTTPS, not TCP). Cloudflare terminates TLS at the edge and forwards to your origin over plain HTTP.

Common trap: If you configure a TCP tunnel for MQTT, WebSockets will not work and clients often fail with opaque connection errors.

Restart cloudflared after changing config:

sudo systemctl restart cloudflared

Step 4: Add/confirm DNS records

Cloudflare usually offers a “Route traffic” step that creates the DNS entries for you.

Confirm you have DNS records for:

  • nvr.example.com
  • mqtt.example.com

They typically show as CNAMEs pointing at the tunnel address.


Step 5: Lock it down with Cloudflare Access (recommended)

If you leave these hostnames public without Access, anyone who guesses the hostname can attempt to reach them.

A good baseline:

  1. Create an Access Application for nvr.example.com
  2. Create an Access Application for mqtt.example.com
  3. Create a Service Token
  4. Add a policy rule like “Service Token is allowed”

What Viewu needs

When Access is enabled, Viewu must send these headers:

  • CF-Access-Client-Id: <service-token-client-id>
  • CF-Access-Client-Secret: <service-token-client-secret>

Set them in Viewu under:

  • Settings → NVR (for nvr.example.com)
  • Settings → MQTT (for mqtt.example.com) if MQTT is protected too

Step 6: Verify with curl (save yourself hours)

Do not skip this. If curl fails, the app will fail too.

6.1 Verify Frigate via Cloudflare

Without Access:

curl -i https://nvr.example.com/api/version

With Access enabled:

curl -i \
  -H "CF-Access-Client-Id: <id>" \
  -H "CF-Access-Client-Secret: <secret>" \
  https://nvr.example.com/api/version

Success looks like:

  • HTTP/2 200 (or HTTP/1.1 200)
  • Version JSON in the response body

6.2 Verify MQTT WebSockets handshake

If your broker expects the WebSockets endpoint at /mqtt, test:

curl -i --http1.1 \
  -H "Connection: Upgrade" \
  -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" \
  -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
  -H "Sec-WebSocket-Protocol: mqtt" \
  -H "CF-Access-Client-Id: <id>" \
  -H "CF-Access-Client-Secret: <secret>" \
  https://mqtt.example.com/mqtt

Success looks like:

  • HTTP/1.1 101 Switching Protocols

Tip: Some setups use / instead of /mqtt. If /mqtt fails, try https://mqtt.example.com/ and confirm what your client expects.


Step 7: Configure Viewu

NVR settings

  • URL: https://nvr.example.com
  • If Access is enabled: set the Client ID/Secret headers
Viewu Settings → NVR (Cloudflare Access headers)

MQTT settings

  • Host: mqtt.example.com
  • Use WSS (WebSockets over TLS)
  • If Access is enabled: set the same headers
Viewu Settings → MQTT (Cloudflare Tunnel + Access)

Troubleshooting (most common mistakes)

“WebSocket won’t connect”

Most common causes:

  • You used a TCP tunnel instead of an HTTP tunnel.
  • You pointed the tunnel origin at https:// instead of http://.
  • Mosquitto is not actually listening with protocol websockets.

Quick checks:

  1. Confirm Mosquitto is listening on 9001:
    • ss -lntp | grep 9001 (Linux)
  2. Confirm the Cloudflare ingress origin is http://127.0.0.1:9001
  3. Confirm the curl handshake returns 101 Switching Protocols

“Frigate works but snapshots don’t show in notifications”

Likely causes:

  • The snapshot URL isn’t reachable from the phone at notification time (VPN not connected, DNS not reachable off-LAN).
  • Access is enabled but the snapshot request is missing headers.
  • A self-signed certificate exists somewhere in the chain.

Quick checks:

  • Test the snapshot URL in Safari on cellular (not Wi‑Fi).
  • If Access is enabled, confirm Viewu has the Service Token headers set for the NVR host.

“It worked once, then broke”

Likely causes:

  • Cloudflare Access token was rotated or pasted incorrectly
  • You updated tunnel config and did not restart cloudflared
  • You changed Mosquitto config and did not restart Mosquitto

Quick fix pattern:

  • Re-enter headers in Viewu, save, and re-test
  • Restart services:
    • sudo systemctl restart cloudflared
    • sudo systemctl restart mosquitto
Last updated on December 17, 2025