Make a Local Service Accessible to Cloud Agents with Cloudflare Tunnel

Table of Contents

Expose a service running on your local machine to a remote server without opening any ports. For instance, let your Codex Cloud agent (the remote server) access qBittorrent Web UI on your Mac (the local machine), to download a movie for you.

The local machine makes an outbound-only connection to Cloudflare. The remote server hits your subdomain on Cloudflare's edge. Traffic flows:

Your remote server -> https://your-tunnel-name.example.com -> Cloudflare edge servers -> Cloudflare Tunnel (cloudflared) -> qBittorrent Web UI on your local machine

You can probably do the same thing with Tailscale, but unfortunately, the Tailscale app doesn't work well with Mullvad VPN on macOS (and I don't want to use Tailscale's Mullvad VPN add-on).

ref:
https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/
https://tailscale.com/docs/features/exit-nodes/mullvad-exit-nodes

Setup

1. Create Cloudflare Tunnel

Do this from any device where you're logged into Cloudflare. No login needed on the local machine or the remote server.

  1. Go to Cloudflare dashboard -> Networking -> Tunnels
  2. Create Tunnel
    • Tunnel name: your-tunnel-name
    • Click Create Tunnel
  3. Open the tunnel -> Overview -> Add a replica -> macOS
    • Copy the tunnel token from the installation command (the value after service install)
  4. Open the tunnel -> Routes -> Add route -> Published application
    • Subdomain: your-tunnel-name
    • Domain: select your domain from the dropdown (e.g., example.com)
    • Path: [leave empty]
    • Service URL: http://localhost:8080
  5. Click Add route
    • After you create the published application route, Cloudflare will automatically create the DNS record for your subdomain

ref:
https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/get-started/tunnel-useful-terms/
https://developers.cloudflare.com/cloudflare-one/networks/routes/add-routes/

2. Access Controls for Cloudflare Tunnel

Protect the local service with an access policy and let your cloud agent authenticate with a service token instead of a browser login.

  1. Go to Cloudflare dashboard -> Zero Trust -> Access controls -> Service credentials
  2. Service Tokens -> Create Service Token
    • Service token name: your-token-name
    • Service token duration: Non-expiring
    • Click Generate token
    • Save the CF-Access-Client-Id and CF-Access-Client-Secret (shown only once)
  3. Policies -> Add a policy
    • Policy details:
      • Policy Name: your-policy-name
      • Action: Service Auth
      • Policy session duration: 24 hours
    • Policy rules -> Include:
      • Selector: Service Token
      • Value: select the service token you just created (e.g., your-token-name)
    • Click Save policy
  4. Applications -> Create new application -> Self-hosted and private -> Continue with Self-hosted and private
    • Destinations -> Public hostnames:
      • Subdomain: your-tunnel-name (must match the subdomain in step 1.4)
      • Domain: select your domain from the dropdown (e.g., example.com)
      • Path: [leave empty]
    • Access policies -> Add current policies:
      • Select the policy you created in step 2.3
    • Details:
      • Name: your-tunnel-name
      • Session Duration: 24 hours
    • Click Create

ref:
https://developers.cloudflare.com/cloudflare-one/access-controls/service-credentials/service-tokens/
https://developers.cloudflare.com/cloudflare-one/access-controls/policies/
https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/

3. Run cloudflared on Local Machine (macOS)

Make cloudflared run on boot, connecting outbound to Cloudflare. No browser auth needed.

brew install cloudflared

# install as a LaunchAgent using the tunnel token from step 1
sudo cloudflared service install YOUR_TUNNEL_TOKEN

ref:
https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/

To verify it's running:

sudo launchctl list | grep cloudflared

4. Access the Local Service on Remote Server

Test that the tunnel and access policy work. We're accessing qBittorrent Web UI here:

curl \
  -H "CF-Access-Client-Id: $YOUR_CF_ACCESS_CLIENT_ID" \
  -H "CF-Access-Client-Secret: $YOUR_CF_ACCESS_CLIENT_SECRET" \
  -d "username=YOUR_USERNAME&password=YOUR_PASSWORD" \
  https://your-tunnel-name.example.com/api/v2/auth/login

The CF-Access-XXX headers must be included on every request. Without them, Cloudflare returns a 302 redirect to a login page.

ref:
https://github.com/qbittorrent/qBittorrent/wiki/#webui

5. Disable Browser Integrity Check on Cloudflare (Optional)

If Cloudflare blocks requests from your remote server and returns HTTP 403 with Cloudflare error 1010, you need to disable Browser Integrity Check:

  1. Go to Cloudflare dashboard -> example.com -> Rules -> Overview
  2. Configuration Rules -> Create rule
    • Rule name: Disable Browser Integrity Check for your-tunnel-name
    • If incoming requests match: Custom filter expression
    • When incoming requests match:
      • Field: Hostname
      • Operator: equals
      • Value: your-tunnel-name.example.com (must match the hostname in step 1.4)
    • Then the settings are -> Browser Integrity Check:
      • Click + Add
      • Leave the toggle off
  3. Click Deploy, then retry the request from your remote server

This disables Browser Integrity Check only for that hostname instead of the entire domain.

ref:
https://developers.cloudflare.com/waf/tools/browser-integrity-check/
https://developers.cloudflare.com/rules/configuration-rules/

Why Cloudflare Tunnel Over Tailscale?

  • A good fit if your infrastructure is already on Cloudflare
  • Works with Mullvad VPN if you use split tunneling to exclude cloudflared
  • Covered by Cloudflare Zero Trust's free tier