Home Assistant, reachable from anywhere, exposed to no one

Home Assistant runs at home, on a small box next to the router. Using it from a phone on the train, or a laptop at work, means getting from the internet to that box. The usual way opens a door: the router forwards ports 80 and 443 to the home machine, a dynamic DNS name such as DuckDNS publishes the home's IP address, and a reverse proxy such as Caddy or nginx serves that name with a Let's Encrypt certificate. It works, and every scanner on the internet can find it. nfltr turns the direction around: one process at home connects out to a relay and gives Home Assistant an unlisted, end-to-end encrypted HTTPS link, and nothing at home accepts a connection from outside.

We built both setups around the real Home Assistant image (2026.10.4) and tested them end to end in containers: a home network behind NAT, a router, and a visitor on the internet standing in for the phone. Both setups served the UI, the REST API and the WebSocket live updates the app runs on, and both refused a visitor without a token. The open ports at home went from 2 to 0 and the configuration from 16 lines to 9. Below are the setups, the checks, the costs and the gap that is still open.

What you get

  • No open port at home. Ports of the home's public address that accept a connection: 80 and 443 before, none after. Inbound rules on the router: 4 → 0.
  • No public name for your house. A DuckDNS name answers anyone who asks with your home's IP address. The nfltr link is a random host name on the relay, and your IP address is not in it.
  • The same Home Assistant. UI, REST API and live updates over the WebSocket worked through both setups, and both refused a visitor without a token. Home Assistant's own login stays the gate.
  • Encrypted end to end. The visitor's TLS session ends on the home machine, not on the relay: the relay forwards bytes it cannot read.
  • Less to keep running. Configuration 16 → 9 lines; moving parts 4 → 2. No certificate to renew, no DNS updater, no router settings.
  • Restarts handled. The home process killed and started: Home Assistant answered through the same link again 0.8 s after the kill.
  • A link while you use it. Each visit keeps the link alive for another day, across restarts of the home process. A link nobody opens for a day ends, and the next start gives a new one. Details under Limits.

Before and after

Before, the home owner configures the router, a DNS service, a reverse proxy and Home Assistant itself. The router forwards ports 80 (Let's Encrypt's HTTP challenge) and 443 to the home machine. A cron job tells DuckDNS the home's current IP address every few minutes. Caddy serves the DuckDNS name, renews its certificate every couple of months and passes requests to Home Assistant, which has to be told to trust it.

Before: a port forward to Home Assistant The phone resolves the DuckDNS name to the home's IP address and connects to ports 80 and 443 of the home router, which forwards them to Caddy on the home machine; Caddy passes requests to Home Assistant. A cron job updates DuckDNS, and Caddy renews its certificate with Let's Encrypt. away from home home router home machine phone, laptop browser, app DuckDNS name → home IP ports 80, 443 forwarded Caddy cert to renew Home Assistant cron: DuckDNS update publishes the home IP anyone who resolves the name can knock on 80 and 443
Before: the home's address is public, and two ports on it lead to the home machine.

After, the home machine runs nfltr up with one file. It connects out to the relay and asks for a link to Home Assistant's port. The phone opens the link; the relay passes the encrypted connection to the home process, which ends TLS and hands the request to Home Assistant on the same machine. The router is left as it came.

# home.yaml: nfltr up -c home.yaml
agent: home
services:
  ha: { http: 8123 }
After: Home Assistant through an outbound connection The phone opens an HTTPS link on the relay. The home machine's nfltr up process holds an outbound connection to the relay, ends the phone's TLS and passes requests to Home Assistant. The home router has no inbound rule. away from home home router home machine phone, laptop opens the link relay forwards ciphertext nfltr up connects out, ends TLS Home Assistant its own login the only connection across the router goes out
After: the home connects out and the router forwards nothing; the phone's encrypted session ends at home.

Both setups need the same addition to Home Assistant's configuration.yaml. Home Assistant refuses a proxied request (one that carries X-Forwarded-For) unless the proxy is trusted, and Caddy and nfltr up both reach it from loopback:

http:
  use_x_forwarded_for: true
  trusted_proxies:
    - 127.0.0.1
    - "::1"

Two common setups were not tested. Home Assistant Cloud, a subscription from Nabu Casa, the company behind Home Assistant, also needs no port forward and adds voice assistant integrations. Its remote access works much like nfltr's end-to-end encrypted link: Nabu Casa's proxy routes each connection by the name in its TLS handshake (SNI) and forwards the encrypted bytes to the home instance, which holds its own Let's Encrypt certificate (Remote UI deep dive); the proxy is the open-source SniTun, "End-to-End encryption with SNI proxy on top of a TCP multiplexer" (both read November 1, 2026). Cloudflare Tunnel also connects out from home, but it needs a domain on Cloudflare, and Cloudflare ends TLS, so it can read the traffic.

What we checked

The test runs every machine as a container: the home machine (Home Assistant, and Caddy or nfltr up), the home router doing NAT, a visitor on the internet, and, after, a private relay in place of the hosted one. The visitor plays the phone: it loads the UI, calls the REST API with the owner's token, changes a state through the API and waits for it on the WebSocket, and tries both APIs without a token. Then it tries four ports on the home's public address.

CheckBeforeAfter
UI loads; REST API answers with the tokenyesyes
A state change arrives on the WebSocketyesyes
REST (401) and WebSocket refuse a visitor without a tokenyesyes
Ports of the home's public address that accept a connection (of 80, 443, 8123, 8443)80, 443none
Inbound rules on the home router40
A visitor with a guessed link name is refusedn/a (the name is public)yes
The visitor's TLS ends at homeyes (Caddy)yes (the relay cannot read it)
Home process killed and started: answering againn/a0.8 s after the kill, same link

Evidence: reachability, refusals and open ports

Setup
Home Assistant 2026.10.4 (the official image, pinned by digest, onboarded through its API), Caddy 2.10 and a private nfltr relay, all in containers on one Mac (Apple Silicon) with Docker, two runs on October 31, 2026; numbers from the second. The DuckDNS name is a network alias and both setups use self-signed certificates the visitor pins, since Let's Encrypt cannot issue for a test network; on nfltr.xyz the home process gets a Let's Encrypt certificate for its link.
Numbers
Every check passed in both setups; open ports at the home's public address 2 → 0; router inbound rules 4 → 0; the guessed link refused; the visitor's certificate the home's, not the relay's; Home Assistant answered again 0.8 s after the home process was killed, through the same link.
Reproduce
(a) With the released binaries: the commands at the end, then open the printed link from a phone on mobile data and run a port scan of your home's address. (b) Our check, in the closed nfltr repository: the example's end-to-end test, judged by exit codes.
Limits
One host and its containers: no real mobile network, no NAT other than the test router's. The relay ran in the same Docker network.

What it costs

  • Latency. On one host, a REST call through the relay took 0.16 ms at p50 against 0.14 ms through the port forward, and a state change reached the WebSocket in 0.34 ms against 0.29 ms. A real phone adds its network trip to the relay, where the port forward adds its trip to your home.
  • Traffic. Opening the app from a warm browser cache moved 62,491 bytes (the WebSocket session: login, states, configuration, services); a cold load at least 191,138 bytes (the page and the 2 scripts it names; the app fetches more on demand).
  • Money. nfltr is free until further notice. On a relay you run yourself, a month of this use (connected all month, 20 opens a day, 4 cold loads: 0.0356 GiB) adds 0.016 USD at GCP list prices, or 0.0003 EUR at Hetzner's; the relay's own server is 6.51 USD or 1.06 EUR a month at list prices. For comparison, Home Assistant Cloud lists $6.50 a month or $65 a year plus tax, DuckDNS is free, and a .com domain for a Cloudflare Tunnel is about $10.46 a year at Cloudflare's at-cost registrar (prices read on October 31, 2026).
  • One process at home, and the account key from nfltr config login on the home machine.
p50 / p95 in msBeforeAfter
REST call, kept alive (300)0.14 / 0.170.16 / 0.33
WebSocket command round trip (300)0.02 / 0.030.05 / 0.15
State change, REST write to WebSocket event (20)0.29 / 0.750.34 / 1.37

Evidence: latency, bytes and money

Setup
The same test, second run on October 31, 2026, visitor and home on one host. Bytes are what the visitor read on the wire, TLS included. Money: nfltr's cost ledger at public list prices for a relay you run (GCP us-central1, Hetzner EU), with the measured bytes and the stated month of use; the alternatives' list prices read on October 31, 2026 from nabucasa.com, duckdns.org and domainoffer.net.
Numbers
REST p50 0.14 → 0.16 ms, p95 0.17 → 0.33 ms; WebSocket round trip p50 0.02 → 0.05 ms; state change to WebSocket p50 0.29 → 0.34 ms; 62,491 bytes per warm open, at least 191,138 per cold load; a month 0.0356 GiB, 0.016 USD (GCP) or 0.0003 EUR (Hetzner) on top of a relay server at 6.51 USD or 1.06 EUR.
Reproduce
Our check, in the closed nfltr repository: the example's end-to-end test and its report script, which writes these tables from the run's results.
Limits
One host: what each path adds, not a phone's network. The month of use is an assumption, not a measurement; the cold load counts only the scripts the page names.

Limits

  • Idle links expire. A link ends after a day without a visit (the relay's share lifetime on nfltr.xyz). Each visit starts the day again, so a bookmark in daily use keeps working across restarts; after a day unused, nfltr up reports the link expired and its next start prints a new one.
  • Unlisted is not secret. Whoever holds the link reaches Home Assistant's login, as anyone reaches a DuckDNS name's; Home Assistant's own login, with its optional multi-factor authentication and IP ban, stays the gate. A share password does not stack on top: the Home Assistant app sends its own Authorization header on API calls, which replaces the browser's.
  • Measured on one host. The numbers show what each path adds, not what a phone on mobile data sees.

Is it for you?

Fits if you run Home Assistant (or any web app) at home, want to reach it from a phone or laptop without forwarding ports or publishing your home's IP address, and can run one small process next to it.

Not ready for a link that waits unused for weeks: one nobody opens for a day ends, and the phone needs the new one. Voice assistant integrations need Home Assistant Cloud. The nfltr CLI and relay server are closed source, and running your own relay is a beta.

Try next Install nfltr on the home machine and sign in:

curl -fsSL https://nfltr.xyz/install.sh | sh
nfltr config login

Add the http: lines above to Home Assistant's configuration.yaml and restart it, write home.yaml as shown, and start it:

nfltr up -c home.yaml

Open the link it prints from your phone.

← All posts