Cloudflare Zero Trust · worked example

Reaching a service at 10.0.1.20:8000

Two complete routes to one app on your LAN — published to the internet behind Access with cloudflared, or kept entirely private with Cloudflare Mesh. Same endpoint, same untouched application, very different exposure.

The situation

An application listens on 10.0.1.20:8000 — a NAS, a VM, an appliance, anything. You do not want to install software on it, and you will not open a port on the router. A second Debian machine on the same subnet, 10.0.1.10, will act as the connector.

A — CLOUDFLARED Any browser on the internet Cloudflare edge Access policy cloudflared 10.0.1.10 DNS tunnel LAN B — CLOUDFLARE MESH Enrolled device only Cloudflare Gateway policy Mesh node 10.0.1.10 MASQUE mesh CIDR route The application 10.0.1.20:8000 unmodified
Both routes terminate at the same untouched application. The difference is everything to its left: route A publishes a hostname the whole internet can resolve and relies on Access to turn strangers away; route B publishes nothing, and only devices enrolled in your organisation have any path to the address at all.

Which one

A — cloudflaredB — Mesh
You reach it athttps://app.example.com10.0.1.20:8000
Works fromany browser, anywhereenrolled devices only
Client softwarenoneCloudflare One client required
ProtocolsHTTP, plus SSH/RDP via clientany TCP, UDP, ICMP
AuthorisationAccess application + policyGateway network policy
Public DNSyes — a proxied recordnone
Share with a guestadd their email to the policythey must enrol a device
Pick it whenit's a web app and convenience mattersit's unauthenticated, or not HTTP
Prerequisite for both

The connector machine must already reach the app across the LAN. Confirm from 10.0.1.10 before configuring anything in Cloudflare — a tunnel cannot fix a route or firewall that is broken locally:

curl -sI http://10.0.1.20:8000 | head -1
nc -vz 10.0.1.20 8000

Route A

Publish it with cloudflared

Public hostname, protected by Cloudflare Access. Reachable from any browser.

  1. Create the tunnel
    one.dash.cloudflare.com › Networks › Tunnels › Create a tunnel › Cloudflared

    Name it after the connector machinelan-connector, not after the app. One tunnel can publish many services on the subnet. Save, then leave the token screen open.

  2. Install the connector on 10.0.1.10
    sudo mkdir -p --mode=0755 /usr/share/keyrings
    curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg \
      | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
    
    echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared any main' \
      | sudo tee /etc/apt/sources.list.d/cloudflared.list
    
    sudo apt-get update && sudo apt-get install -y cloudflared
    sudo cloudflared service install <YOUR_TUNNEL_TOKEN>

    The suite is literally any for every Debian and Ubuntu release. Confirm it came up:

    systemctl status cloudflared
    journalctl -u cloudflared -n 30 --no-pager   # expect "Registered tunnel connection"

    The tunnel should read Healthy in the dashboard before you continue.

  3. Create the Access application — before the hostname exists
    Zero Trust › Access › Applications › Add an application › Self-hosted
    FieldValue
    NameLAN app
    Session duration24 hours
    Subdomainapp — never leave this blank
    Domainexample.com
    Pathempty
    Identity providersuntick Accept all, select only yours

    Then add a policy: action Allow, Include → Emails → your address. Finish the wizard and confirm the app is listed. A policy that is created but never attached protects nothing.

    Order is not cosmetic

    The next step makes the app reachable from the entire internet. If the Access application does not exist when DNS starts resolving, there is nothing between strangers and 10.0.1.20:8000. Build the wall first, then open the road.

  4. Point a public hostname at the LAN address
    Networks › Tunnels › your tunnel › Edit › Public Hostname › Add a public hostname
    FieldValue
    Subdomainapp — must match the Access application exactly
    Domainexample.com
    TypeHTTP
    URL10.0.1.20:8000

    This is the only line that differs from a same-machine setup. The URL is resolved by the connector, not by Cloudflare, so a private address is perfectly valid here — it simply has to be reachable from 10.0.1.10. Saving creates the proxied CNAME automatically.

    If it misbehaves

    • App serves HTTPS with a self-signed certificate → set Type to HTTPS and enable No TLS Verify.
    • Wrong site, or a redirect loop → set HTTP Host Header to app.example.com.
    • Slow to start → raise Connect timeout.
  5. Verify from outside
    curl -sI https://app.example.com | head -1   # want: HTTP/2 302
    ResultMeaning
    302Correct — Access is challenging. Redirect goes to cloudflareaccess.com.
    200No authentication happened. Either unprotected, or you are testing from a WARP-enrolled device.
    502Access passed you through but the connector cannot reach 10.0.1.20:8000.
    530Tunnel is down — check the service on 10.0.1.10.
    Do not trust a browser on a WARP device

    If your machine runs the Cloudflare One client enrolled in the same organisation, Access may authenticate it silently from device identity — no login appears. Incognito does not help, because the request still leaves through the same tunnel. Test on a phone with mobile data and WARP off; that is the only reading that means anything.


Route B

Reach it privately with Cloudflare Mesh

No hostname, no public DNS. The app keeps its own address and only enrolled devices can route to it.

What you end up with

The node receives a Mesh IP from 100.96.0.0/12, but the app does not. You advertise 10.0.1.0/24 as a route behind the node, so from an enrolled laptop you open http://10.0.1.20:8000 — the real address, unchanged — as though you were sitting on that LAN.

  1. Create the node
    Cloudflare dashboard › Networking › Mesh › Add a node

    Name it lan-node and select Create node. Copy the connector token from the wizard — it is single-use, and retrieving it later means returning to the node's detail page.

  2. Enable IP forwarding on 10.0.1.10

    Required before connecting, since this node routes for a subnet rather than only itself:

    printf 'net.ipv4.ip_forward = 1\nnet.ipv6.conf.all.forwarding = 1\nnet.ipv6.conf.all.accept_ra = 2\n' \
      | sudo tee /etc/sysctl.d/99-zzz-cloudflare-warp-connector.conf
    sudo sysctl --system
  3. Install the Cloudflare One client

    A different package and repository from cloudflared:

    curl -fsSL https://pkg.cloudflareclient.com/pubkey.gpg \
      | sudo gpg --yes --dearmor -o /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg
    
    echo "deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ noble main" \
      | sudo tee /etc/apt/sources.list.d/cloudflare-client.list
    
    sudo apt-get update && sudo apt-get install -y cloudflare-warp
    Do not use $(lsb_release -cs) here

    Unlike the cloudflared repo, this one has no any suite — and Cloudflare publishes only to LTS and Debian stable names. On an interim release such as Ubuntu 25.04 (plucky) or 24.10 (oracular), the generated line 404s on the Release file. Pin an available suite by hand: noble, jammy, focal, bookworm, trixie, bullseye.

  4. Join the mesh, headless
    sudo warp-cli --accept-tos connector new <CONNECTOR_TOKEN>
    sudo warp-cli --accept-tos connect
    warp-cli --accept-tos status

    No interactive login occurs — the token authenticates the machine. The node then appears in the Mesh list with its assigned Mesh IP.

  5. Advertise the subnet

    On the node's detail page, add a CIDR route for 10.0.1.0/24. This is the step that makes 10.0.1.20 reachable — without it, only the node itself answers.

    Advertise the narrowest range that covers what you need. 10.0.1.0/24 exposes the whole subnet to anyone your Gateway policy allows; a /32 for a single host is tighter if only this one app matters.

    Which addresses change, and which don't

    No translation is applied to an advertised CIDR. The node acts as a gateway: traffic for 10.0.1.0/24 is forwarded to it, and it delivers to the right host with addressing intact — which is exactly why step 2 enables IP forwarding.

    Receives a Mesh IP (100.96.0.0/12)Keeps its own address
    The node itself, 10.0.1.10Every host behind it on the advertised subnet
    Each enrolled client device10.0.1.20 — the application machine
    Hostname routes are the exception

    If you use a hostname route rather than a CIDR route, Gateway hands the client a token IP from 172.64.128.0/20 and rewrites the destination to the real private address before the packet reaches the node. Seeing a 172.64.x.x address on the client is expected there, not a misconfiguration — it is a transient routing mechanism rather than persistent translation. CIDR routes never do this.

  6. Configure the devices that will connect
    Zero Trust › Settings › WARP Client › Profile settings
    • Protocol must be MASQUE. On WireGuard you silently lose hostname routes, IPv6 CIDR routes and high availability — nothing errors, the features just don't work.
    • Split Tunnel in Include mode, listing 100.96.0.0/12 and 10.0.1.0/24. Include mode leaves the rest of the device's traffic alone.

    Install the Cloudflare One client on each laptop or phone, choose Zero Trust security, enter your team name, and authenticate.

    Overlapping networks

    A device physically on 10.0.1.0/24 must not route that range through the mesh — it would tunnel traffic back to the network it is already sitting on. Mesh access is for devices away from the LAN; at home they reach the app directly.

  7. Restrict who can reach it
    Zero Trust › Networks › Policies › Add a policy

    Enrolment alone is authentication, not authorisation — by default any enrolled device can reach an advertised route. Add a network policy scoped to destination IP 10.0.1.20 and port 8000, matched on the identities or device posture you intend to allow, and a lower-precedence Block for everything else.

  8. Verify from an enrolled device
    nc -vz 10.0.1.20 8000
    curl -sI http://10.0.1.20:8000 | head -1
    ping 10.0.1.20                    # ICMP works here; it would not through a tunnel
    Done when

    An enrolled laptop off the LAN reaches http://10.0.1.20:8000, and the same laptop with the client disconnected cannot. Confirm both directions — the second is the one that proves the app is private.


Running both

The two routes coexist on one machine. cloudflared and cloudflare-warp are separate daemons with separate repositories, and neither is aware of the other. Publishing a web UI over a hostname while reaching its database privately over the mesh is a reasonable arrangement.

What you must not do is assume one protects the other. An Access policy on app.example.com places no restriction whatsoever on 10.0.1.20:8000 over the mesh, and a Gateway network policy does nothing for the published hostname. They are independent control surfaces, and each needs its own rule.

Common failures

SymptomCause
502 via hostnameConnector cannot reach 10.0.1.20:8000. Test with curl from 10.0.1.10 — usually a host firewall on the app machine, or the app bound to loopback only.
App bound to 127.0.0.1Then 10.0.1.20:8000 is unreachable from anywhere, including the connector. Rebind it to the LAN address, or run the connector on the app's own machine.
404 on apt updateWARP repo suite doesn't exist for your release. Pin noble or bookworm.
Mesh node connects,
app unreachable
No CIDR route advertised, or IP forwarding not enabled. Both are required.
Nothing routes
on the client
Split Tunnel in Exclude mode, or the subnet missing from the Include list.
Hostname returns 200
with no login
Testing from a WARP-enrolled device. Retest off-network before concluding anything.