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
ingresshas 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, not0.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.