← back to all posts
Published 2026-05-24

I Gave My AI Agent a Cloudflare Zero Trust Tunnel So I Can Run My Command Center From Anywhere

I run a little AI command center on my laptop. It's the control surface for the agents I'm building at Grand Canyon Computers — kick off a task, watch it work, approve a step, read the output. For weeks it lived at http://localhost:8787 and that was fine, right up until I was standing in line for coffee and wanted to nudge a job along from my phone.

The lazy answer is "open a port on my router and forward it." Please don't do that. The thing I'm about to describe is the opposite of that, and it took me about twenty minutes.

The setup: cloudflared, not port forwarding

A Cloudflare Tunnel works by running a small daemon (cloudflared) on the same machine as your app. The daemon dials out to Cloudflare's edge and holds a persistent connection open. Your app never gets an inbound port. There's no firewall hole, no public IP exposed, nothing for a scanner to find. Cloudflare routes requests for your hostname down that outbound connection to your localhost service.

Install it and authenticate:

brew install cloudflared
cloudflared tunnel login
cloudflared tunnel create command-center

That create command writes a credentials file and gives you a tunnel UUID. Then you map a hostname to your local port with a config file:

# ~/.cloudflared/config.yml
tunnel: command-center
credentials-file: /Users/me/.cloudflared/<UUID>.json

ingress:
  - hostname: cc.grandcanyon.computer
    service: http://localhost:8787
  - service: http_status:404

Point DNS at the tunnel and run it:

cloudflared tunnel route dns command-center cc.grandcanyon.computer
cloudflared tunnel run command-center

That's it. https://cc.grandcanyon.computer now reaches my laptop. Cloudflare terminates TLS at the edge, so I get a real certificate for free and never touch mkcert or self-signed nonsense.

The part people skip — and the part that matters

Here's where I want to slow down, because this is the actual teaching moment and it's the thing I see people get wrong.

A tunnel makes your app reachable. It does not make it protected. The second I routed DNS, my command center was sitting on the public internet behind a friendly hostname, wide open to anyone who guessed the URL. And this is not a blog or a marketing page. This endpoint can start, stop, and steer an AI agent that has credentials and can take actions. That is a session-controlling endpoint. Leaving it unauthenticated is the digital equivalent of leaving your car running with the keys in it in a parking lot, then walking away.

Think about the blast radius. An open command center isn't "someone might read my data." It's "someone might tell my agent to do something." Prompt-injection-as-a-service, except they don't even need to be clever — the front door is unlocked.

So before I told anyone the URL, I put Cloudflare Access in front of it. Access is the Zero Trust gate. It sits at the edge, before the request ever reaches the tunnel, and demands proof of identity:

  1. In the Zero Trust dashboard, create a Self-hosted application for cc.grandcanyon.computer.
  2. Add a policy: Allow, with a rule like Emails == my email. One person. Me.
  3. Pick an identity provider — I use a one-time PIN to my email, but Google/GitHub SSO works too.

Now the flow is: request hits Cloudflare → Access checks for a valid session token → if none, you get an identity challenge → only after you authenticate does the request get handed to the tunnel and reach localhost. An unauthenticated visitor never touches my machine at all. The gate is in front of the door, not behind it.

The rule I now refuse to break

If an endpoint can control a session — start it, stop it, feed it instructions, approve its actions — it must be authenticated at the edge before the request reaches the app.

Not "I'll add auth later." Not "it's an obscure URL, nobody will find it." Security through obscurity has a half-life measured in hours once a hostname shows up in certificate transparency logs (and Cloudflare-issued certs do show up there — your "secret" subdomain is publicly logged the moment the cert is issued).

The reason I'm dogmatic about doing the auth at the edge rather than inside the app: defense in depth, and reducing the code I have to trust. If auth lives only in my Node server, then a bug in my Node server is a bug in my auth. By gating at Access, an attacker has to get past Cloudflare's identity layer before my application code ever runs. My app can still do its own session checks — and it should — but the front line isn't my hand-rolled middleware at 11pm.

One more thing for the agent case specifically: I also scope what the agent can do independently of who can reach it. The tunnel and Access control access; least-privilege credentials control damage. Both. An attacker who somehow gets through Access still shouldn't find an agent holding god-mode API keys.

What I'd tell you to copy

  • Use a tunnel (cloudflared) instead of port forwarding for anything on your own machine. Zero inbound ports is a feature.
  • Treat the tunnel and the auth as two separate jobs. The tunnel gets you there; Access decides who.
  • Gate session-controlling endpoints at the edge, every time, before you share the URL — not after.
  • Assume your hostname is public knowledge the moment the TLS cert is issued.

I now run my whole agent command center from my phone, from a coffee shop, behind a real identity check, with no open ports anywhere. It feels like cheating. It's just the boring-correct way to do it.

If you're building something similar and you want a second set of eyes on whether your agent's control plane is actually locked down — that's literally the kind of thing I poke at in my Agent Production-Readiness Audit. Happy to talk.

Five emails a week on AI reliability. Free, no spam, unsubscribe anytime.

Subscribe →