Exposing Home Assistant without opening a port: Cloudflare Tunnel plus Access
On this page
There are three common ways to reach Home Assistant from outside your house, and two of them are bad.
Port forwarding puts your smart home directly on the public internet, where it will be found by automated scanners within hours. Nabu Casa Cloud is genuinely good and worth paying for if you want zero maintenance. The third option, a Cloudflare Tunnel with an Access policy in front of it, is what I run: no inbound ports, no dynamic DNS, no certificate renewal, and an authentication wall that sits in front of Home Assistant rather than inside it.
This is the setup running on my own subdomain. Here is how it goes together and, more usefully, the parts that are easy to get wrong.
Ad space, reserved
Why not just forward port 8123
Because the moment you do, the login page of your house is reachable by anyone who scans that IP range, which is everyone. Home Assistant has decent brute-force protection, but the correct number of unauthenticated strangers able to reach that page is zero, not few.
Port forwarding also means your home IP is in DNS, you need dynamic DNS when your ISP rotates it, and you are managing TLS certificates on a box in your living room. A tunnel removes all four problems at once.
How the tunnel actually works
The direction of the connection is the whole point. A daemon on your network makes an outbound connection to Cloudflare and holds it open. Requests to your hostname arrive at Cloudflare, travel back down that existing connection, and reach Home Assistant from inside your own network.
Nothing is listening for inbound connections on your router. There is no port to forward, and nothing to find on a scan. Your firewall stays closed.
Running cloudflared in Docker
Create the tunnel in the Cloudflare Zero Trust dashboard under Networks, then Tunnels, and copy the token it gives you. The container needs nothing else.
# docker-compose.yml
services:
cloudflared:
image: cloudflare/cloudflared:latest
container_name: cloudflared
restart: unless-stopped
command: tunnel --no-autoupdate run
environment:
- TUNNEL_TOKEN=${TUNNEL_TOKEN}
networks:
- homelab
networks:
homelab:
external: truePut the token in a .env file beside the compose file rather than inline, and keep that file out of any repository. The token is a credential that grants tunnel access to your network.
# .env
TUNNEL_TOKEN=eyJhIjoiXXXXXXXX...
# and in .gitignore
.envPointing the tunnel at Home Assistant
In the tunnel configuration, add a public hostname and route it to your Home Assistant service. If both are containers on the same Docker network, use the container name rather than an IP, because container IPs change on restart and hostnames do not.
Public hostname: smart.example.com
Service type: HTTP
URL: homeassistant:8123The part everyone hits: trusted proxies
Do this before you test, or you will spend an evening on it. Home Assistant sees every request as coming from the tunnel container rather than from the real client. Without telling it to trust that proxy, logins fail in confusing ways and every entry in your logbook shows the same internal address.
# configuration.yaml
http:
use_x_forwarded_for: true
trusted_proxies:
- 172.18.0.0/16 # your Docker network subnet
- 127.0.0.1Set trusted_proxies to your actual Docker subnet, which you can find with a quick inspect. Do not put 0.0.0.0/0 in there. That tells Home Assistant to believe the forwarded-for header from anyone, which hands an attacker the ability to claim any source address they like.
docker network inspect homelab --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}'Restart Home Assistant after changing this. A reload of the YAML is not enough for http settings.
Putting Access in front of it
The tunnel solves exposure. It does not solve authentication, and on its own it means your Home Assistant login page is now reachable worldwide, which is roughly where you started. Cloudflare Access is the piece that fixes that.
Create an Access application for the hostname, then add a policy. The simplest useful one allows a specific list of email addresses, with a one-time PIN sent to that address.
- Application type: Self-hosted
- Domain: your Home Assistant hostname
- Policy action: Allow
- Include rule: Emails, listing only the addresses that should get in
- Session duration: 24 hours is a reasonable balance
Now an unauthenticated visitor never reaches Home Assistant at all. They hit a Cloudflare login screen first. Two independent authentication layers, and the outer one is not code you have to patch.
The bit that will break your phone app
Access challenges browsers with an interactive login. The Home Assistant companion app is not a browser and cannot complete that flow, so the app stops connecting the moment you enable Access.
The fix is a service token: a client ID and secret pair that the app sends as headers, bypassing the interactive login for that one client. Create it under Access, then Service Auth, and add a second policy on the application.
Policy name: Companion app
Action: Service Auth
Include rule: Service Token -> your token nameThen in the companion app, add the two headers under connection settings.
CF-Access-Client-Id: <client-id>.access
CF-Access-Client-Secret: <client-secret>A service token is a long-lived credential that grants access without a human present. Give it to the app and nothing else, and rotate it if a phone is lost.
Checking it is actually closed
Two checks worth running before you trust it. First, confirm an unauthenticated request never reaches Home Assistant. You want a Cloudflare login page, not a Home Assistant one.
curl -sS -o /dev/null -w '%{http_code} %{redirect_url}n' https://smart.example.com/A redirect to a cloudflareaccess.com address is correct. A 200 that returns Home Assistant markup means the Access policy is not applied to that hostname.
Second, confirm nothing is listening from outside. From a network that is not yours, a scan of your home IP on 8123 should find nothing at all.
What this does not protect you from
Worth being honest about the limits. This design stops unauthenticated strangers reaching Home Assistant, which is the main risk. It does not protect you from a compromised device inside your own network, because that device is already behind the tunnel. It does not protect a service token that leaks. And it makes Cloudflare a dependency: if their edge is down, your remote access is down, though your local network keeps working normally.
For a smart home that is a fair trade. For anything where an outage costs money, run a second path in.
Why I run this rather than paying for Cloud
Nabu Casa Cloud is a good product and it funds Home Assistant development, which matters. If you want this to work without thinking about it, buy it.
I run the tunnel because the same pattern then serves everything else on the network. Once cloudflared is running and Access is configured, exposing another internal service is a hostname and a policy, not a new subscription. That generalisation is the actual value, and it is the reason this setup is worth the evening it costs.
Ad space, reserved


