notesarchiveworkabout

A Cloudflare Tunnel closes your ports, not your attack surface

The tunnel removes inbound ports, but every hostname in ingress is still publicly resolvable and reachable. Access policies, not the tunnel, are what make a service private.

The pitch for cloudflared is that you stop forwarding ports. That part is true: the daemon dials out to Cloudflare, the connection is established from inside, and your router needs no inbound rule at all. nmap against the WAN address comes back empty.

What is easy to miss is that this changes how traffic arrives, not who may send it. Every hostname listed under ingress resolves publicly and answers publicly. If the service behind it has a weak login page, the tunnel has done nothing for you. It has just given the login page a clean TLS certificate and a CDN in front of it.

What the config actually declares

A tunnel config is a routing table, not a policy:

tunnel: 6ff1c2a4-1b3e-4a90-9d21-8c5f7e0b4a11
credentials-file: /etc/cloudflared/6ff1c2a4.json

ingress:
  - hostname: media.example.com
    service: http://127.0.0.1:8096
  - hostname: files.example.com
    service: http://127.0.0.1:8080
    originRequest:
      noTLSVerify: false
      connectTimeout: 10s
  # Anything not matched above gets a 404 instead of leaking to a default origin.
  - service: http_status:404

Two things worth noting. The catch-all http_status:404 at the end is mandatory, because cloudflared refuses to start without a final rule with no hostname. Making it a 404 rather than a real service means a request for a hostname you forgot to remove hits nothing.

To see what a running tunnel is publishing, read the rules rather than guessing:

# Which hostnames does this tunnel answer for?
cloudflared tunnel ingress rule https://media.example.com

# Validate the whole file before restarting anything.
cloudflared tunnel ingress validate

# What the daemon is doing right now.
journalctl -u cloudflared -f --no-pager

The part that makes it private

Access sits in front of the hostname and runs before the request reaches the tunnel. Without a policy, files.example.com is a public file server with good uptime.

The failure mode I see most often is a policy scoped to the wrong path. A policy on files.example.com/admin protects exactly that prefix; /api/v1/download is untouched. Scope the policy at the hostname and add exceptions inward, not the other way around.

Second failure mode: service tokens issued for automation and never rotated. A token is a bearer credential. Anything holding it is you, indefinitely, until you revoke it.

What to check on an existing tunnel

  • Every hostname in ingress has a matching Access application. No exceptions “for now”.
  • The final ingress rule is http_status:404, not a real service.
  • Origins bind to 127.0.0.1, not 0.0.0.0. The tunnel reaches localhost fine, and a misconfigured firewall then cannot expose the same port on the LAN.
  • Service tokens have an expiry set, and you know which script holds each one.

The tunnel is a good default. It is a transport decision, though, and transport decisions do not answer authorisation questions.