Secure Remote and Local HA Access Via Cloudflare Tunnel + mTLS and Nginx Proxy Manager
This guide walks through setting up a Cloudflare Tunnel on Home Assistant OS using the Cloudflared app, then securing it with mutual TLS (mTLS) using Cloudflare-issued client certificates and a WAF custom rule — no Enterprise plan required. It also briefly covers configuring Nginx Proxy Manager for local SSL/TLS.
Before you start: if you also want local (on-network) access to Home Assistant without routing through Cloudflare, use separate hostnames for external and internal access — for example
ha.yourdomain.comexternally andlan.ha.yourdomain.cominternally. Split-brain DNS (same hostname resolving to different IPs inside vs outside the network) does not work reliably with Cloudflare-fronted hostnames, because of DNS-over-HTTPS (DoH) behavior on iOS, macOS Safari, and on some network appliances. See Part 8 for the split-URL architecture and Appendix A for the full cautionary tale.
Prerequisites
- A domain name with DNS managed by Cloudflare
- A Cloudflare account (Free plan or above)
- Home Assistant OS running with Supervisor/app-on support
- Access to the Home Assistant UI and the ability to edit
configuration.yaml - OpenSSL installed on your local machine (for
.p12export) - (Optional, for the split-URL architecture in Part 8) Nginx Proxy Manager app-on or another local reverse proxy
Part 1: Install and Configure the Cloudflared App
1.1 Add the Repository
- In Home Assistant, go to Settings > apps > App Store.
- Click the three-dot menu (top right) and select Repositories.
- Add the following repository URL:
https://github.com/homeassistant-apps/repository - Click Add, then close the dialog. The “Cloudflare Tunnel Client” app should now appear in the store.
1.2 Install the App
- Find Cloudflare Tunnel Client in the app store and click Install.
- After installation, do not start it yet — configure it first.
1.3 Choose a Tunnel Mode
The app supports two modes:
- Local tunnel (managed by the app): The app handles tunnel creation and DNS. Simpler, but limited to exposing Home Assistant on one hostname.
- Remote tunnel (managed in Cloudflare dashboard): You create the tunnel in the Cloudflare Zero Trust dashboard and pass the token to the app. This gives you full control over multiple public hostnames and ingress rules. Use this mode for mTLS setups.
1.4 Create a Remote Tunnel in Cloudflare
- Log in to the Cloudflare Zero Trust dashboard.
- Go to Networks > Connectors.
- Click Create a tunnel and choose Cloudflared as the connector type.
- Give it a name (e.g.,
homeassistant). - On the connector install page, copy the tunnel token — you’ll need it for the app config.
- Under Public Hostnames, add a route:
- Subdomain: e.g.,
ha(will becomeha.yourdomain.com) - Domain: select your Cloudflare-managed domain
- Service:
http://homeassistant:8123(the internal Docker hostname)- If that doesn’t work, try
http://172.30.32.1:8123
- If that doesn’t work, try
- Subdomain: e.g.,
- Save the tunnel.
1.5 Configure the app
- Go to the Cloudflared app Configuration tab in Home Assistant.
- Set the tunnel token:
tunnel_token: "eyJhIjoixxxxxxx..." - Make sure the tunnel name matches what was used in the Cloudflare dashboard (not sure if this is necessary)
- Leave other options at defaults unless you have specific needs.
- Save the configuration.
1.6 Update Home Assistant configuration.yaml
Home Assistant must trust the Cloudflared proxy. Add the following to your configuration.yaml:
http:
use_x_forwarded_for: true
trusted_proxies:
- 172.30.33.0/24
After saving, restart Home Assistant (not just reload — a full restart is needed for http config changes).
1.7 Start the app
- Go back to the Cloudflared app and click Start.
- Check the Log tab to confirm the tunnel connects successfully. You should see messages about the tunnel being registered.
- Test basic access by navigating to
https://ha.yourdomain.comin a browser. You should see your Home Assistant login page.
Part 2: Verify Cloudflare SSL/TLS Settings
Before moving on to mTLS, confirm that standard HTTPS is working for your tunnel hostname. All three of the following settings are required.
2.1 Set SSL/TLS Encryption Mode to Full
- In the Cloudflare dashboard, select your domain.
- Go to SSL/TLS > Overview.
- Set the encryption mode to Full.
Do not use “Off” or “Flexible” — these will cause connection failures or redirect loops. “Full” is correct because Cloudflare terminates TLS at the edge and forwards traffic to Home Assistant over the tunnel as plain HTTP. Do not use “Full (Strict)” unless you have a valid origin certificate installed on your HA instance.
2.2 Enable Always Use HTTPS
- Go to SSL/TLS > Edge Certificates.
- Enable Always Use HTTPS.
This ensures all HTTP requests to your hostname are automatically redirected to HTTPS.
2.3 Ensure Your Hostname Has a Valid Edge Certificate
Cloudflare’s free Universal SSL certificate covers yourdomain.com and *.yourdomain.com, but it does not cover deeper subdomains like ha.home.yourdomain.com. If your tunnel hostname is two or more levels deep, you need an additional certificate.
Check for a warning on the DNS record that says “This hostname is not covered by a certificate.” If you see it:
- Go to SSL/TLS > Edge Certificates.
- Click Order Advanced Certificate.
- Add both the wildcard and the base for your subdomain level:
*.home.yourdomain.comhome.yourdomain.com
- Complete the order. Cloudflare will provision the certificate (this is available on the free plan).
Once provisioned, verify that https://ha.yourdomain.com loads your Home Assistant login page before proceeding.
Part 3: Create Client Certificates Using Cloudflare’s CA
Instead of generating your own CA, Cloudflare acts as the certificate authority and issues client certificates directly from the dashboard. This works on all plans (Free and above).
3.1 Create a Client Certificate
- In the Cloudflare dashboard, select your domain.
- Go to SSL/TLS > Client Certificates.
- Click Create Certificate.
- Cloudflare generates a certificate signed by a Cloudflare-managed, account-level root CA.
- Choose the key type and validity period:
- RSA or ECDSA (RSA 2048 is fine for broad device compatibility)
- Validity: 1 year, 2 years, or 10 years
- Cloudflare displays the certificate and private key.
Important: Copy both the certificate and private key immediately. The private key is shown only once and cannot be retrieved later.
- Save them to files on your local machine:
client-cert.pem— the certificateclient-key.pem— the private key
3.2 Repeat for Additional Devices
Create a separate certificate for each device that needs access. This makes revocation straightforward — you can revoke a single device’s certificate without affecting the others.
3.3 Export as .p12 (PKCS#12)
Browsers and mobile devices import .p12 files for client certificate authentication. On your local machine, run:
openssl pkcs12 -export \
-out client.p12 \
-in client-cert.pem \
-inkey client-key.pem \
-name "HA Client Certificate"
You will be prompted to set an export password. Remember this — you’ll need it when importing on your devices.
3.4 File Summary
| File | Purpose | Keep Secret? |
|---|---|---|
client-cert.pem |
Client certificate (from Cloudflare) | No |
client-key.pem |
Client private key (from Cloudflare) | Yes — save it, Cloudflare won’t show it again |
client.p12 |
Bundled cert + key for device import | Yes — distribute to trusted devices only |
Part 4: Enable mTLS and Enforce with a WAF Rule
4.1 Associate the Hostname with Client Certificates
- In the Cloudflare dashboard, go to your domain’s SSL/TLS > Client Certificates.
- In the Hosts section of the Client Certificates card, click Edit.
- Add
ha.yourdomain.com(the hostname your tunnel uses). - Save.
This tells Cloudflare to request a client certificate from any browser or device connecting to that hostname.
4.2 Create a WAF Custom Rule to Block Requests Without a Valid Certificate
- Go to Security > WAF > Custom Rules.
- Click Create rule.
- Configure the rule:
- Rule name:
Require mTLS for Home Assistant - Expression (edit expression):
(http.host eq "ha.yourdomain.com") and (not cf.tls_client_auth.cert_verified) and (not starts_with(http.request.uri.path, "/api/webhook/")) - Action:
Block
- Rule name:
- Deploy the rule.
Any request to ha.yourdomain.com that does not present a valid client certificate signed by your account’s Cloudflare-managed CA will now receive a 403 Forbidden response — except requests to the /api/webhook/ path, which are allowed through without a client cert.
Why the webhook exclusion is required. The Home Assistant Companion App posts to /api/webhook/<token> for sensor updates, location updates, and actionable notification callbacks. These POSTs are made from a background URLSession on iOS (and the equivalent on Android) that does not share the foreground app’s access to the installed client identity, so the cert is never presented on the TLS handshake. Without the /api/webhook/ exclusion, every one of those POSTs is blocked by the mTLS rule, and the Companion App fails to save or verify the external URL with the error:
Error Saving URL — Unacceptable status code 403
The /api/webhook/ path is safe to exempt because HA webhook URLs contain a long random secret token that authenticates the caller. Treat those URLs as bearer secrets — do not paste them in screenshots, logs, shared backups, or issue trackers.
Tip: Cloudflare’s “Create mTLS Rule” template button on the Client Certificates page generates the basic form of this rule. You will still need to edit the expression afterward to add the
/api/webhook/exclusion.
4.3 Test the Configuration
- Without the certificate installed: Navigate to
https://ha.yourdomain.comin a browser. You should be blocked with a 403 error. - With the certificate installed: After importing the
.p12(see Part 5), the browser should prompt you to select the client certificate. After selecting it, you should see your Home Assistant login page. - Webhook path spot-check: With wifi off on your phone (forcing the Companion App onto cellular), re-verify the external URL in Settings → Companion App → Connection. It should save without the 403 error. In Cloudflare Security > Events, filter by
Path contains /api/webhook/— you should see POSTs with Mitigation: Not mitigated.
Part 5: Install the .p12 Certificate on Your Devices
macOS
- Double-click the
.p12file — it opens in Keychain Access. - Enter the export password you set earlier.
- The certificate is added to your login keychain. For Safari to present the cert reliably, you may need to drag it into the System keychain (Safari sometimes does not pick up client identities from the login keychain in newer macOS versions).
- When you visit
ha.yourdomain.comin Safari or Chrome, the browser will prompt you to select the client certificate.
iOS / iPadOS
- AirDrop or email the
.p12file to your device. - Open the file — you’ll be directed to Settings > General > VPN & Device Management.
- Tap Install, enter your device passcode, then enter the
.p12export password. - The profile is now installed. Safari will automatically present the certificate when visiting
ha.yourdomain.com, and the HA Companion App’s foregroundWKWebViewrequests will pick it up as well.
Windows
- Double-click the
.p12file to open the Certificate Import Wizard. - Choose Current User as the store location.
- Enter the export password.
- Let Windows automatically select the certificate store (or choose Personal).
- Complete the wizard. Chrome and Edge will use this certificate automatically.
Android
- Transfer the
.p12file to the device. - Go to Settings > Security > Encryption & credentials > Install a certificate.
- Select VPN and app user certificate.
- Browse to the
.p12file, enter the password, and give it a name. - When visiting
ha.yourdomain.comin Chrome, you’ll be prompted to select the certificate.
Firefox (all platforms)
Firefox uses its own certificate store, separate from the OS:
- Go to Settings > Privacy & Security > Certificates > View Certificates.
- Under Your Certificates, click Import.
- Select the
.p12file and enter the password.
Part 6: Home Assistant Companion App Setup
The Home Assistant Companion App supports mTLS natively. Once the client certificate is installed at the OS level, the app will present it automatically during the TLS handshake — with one important caveat about background requests (see Part 4.2).
iOS / iPadOS
- Install the
.p12profile on your device as described in Part 5 (iOS / iPadOS section). - Open the Companion App and go to Settings > Connection.
- Set the External URL to
https://ha.yourdomain.com. - If you also use a local URL (recommended — see Part 8), set the Internal URL to
https://lan.ha.yourdomain.comor whatever you chose. - The app will use the installed client certificate when connecting over the external URL.
Android
- Install the
.p12certificate on your device as described in Part 5 (Android section). - Open the Companion App and go to Settings > Connection.
- Set the External URL to
https://ha.yourdomain.com. - When the app connects, Android will prompt you to select the client certificate. Choose the one you installed.
Part 7: Maintenance and Revocation
Revoking a Certificate
If a device is lost or compromised:
- Go to SSL/TLS > Client Certificates in the Cloudflare dashboard.
- Find the certificate for the compromised device.
- Click Revoke.
- The certificate is immediately invalidated. The WAF rule will block requests using that certificate going forward.
This is a significant advantage of using Cloudflare-issued certificates — you can revoke individual certificates without affecting other devices.
Creating Replacement Certificates
Follow the same process in Part 3 to create a new certificate for a replacement device. Each certificate is independent.
Certificate Limits
Cloudflare has a limit on the number of client certificates per account. If you hit the limit, revoke and delete any certificates that are no longer in use before creating new ones.
Monitoring
- Check tunnel health in the Cloudflare Zero Trust dashboard under Networks > Tunnels.
- Review blocked requests under Security > Events to see mTLS enforcement in action. Filter by
Action = BlockandHost = ha.yourdomain.comto see only the enforcement hits. - The WAF analytics will show how many requests are being blocked by the mTLS rule.
Part 8: Recommended Architecture — Separate External and Internal URLs
For anyone on a home network that serves both on-LAN and remote clients, use two separate hostnames: one that goes through the Cloudflare Tunnel (with mTLS), and one that stays on the LAN and resolves directly to Home Assistant via a local reverse proxy.
Example:
| URL | Path | Purpose |
|---|---|---|
https://ha.yourdomain.com |
Cloudflare edge → Cloudflare Tunnel → HA | External access from outside the home network. mTLS-enforced. |
https://lan.ha.yourdomain.com |
LAN client → local reverse proxy → HA | Internal access from devices on the home network. No Cloudflare, no mTLS. |
Why two URLs
- Reliability. Split-brain DNS (same hostname, different answers depending on the client’s location) fails in subtle ways when the hostname is fronted by Cloudflare. See Appendix A for the full story — short version: modern clients use DoH, which bypasses your local DNS, so the “internal” answer never reaches them.
- Performance. Local clients stay on the LAN. No trip out to the Cloudflare edge and back through a tunnel.
- Debuggability. When something breaks you know exactly which path is at fault — the hostname tells you whether it’s going through Cloudflare or not.
- Security boundary is clearer. mTLS is enforced on
ha.yourdomain.com.lan.ha.yourdomain.comhas no mTLS because it’s only reachable from inside the LAN — that’s the access control.
Setting up the local URL
- Install the Nginx Proxy Manager app in Home Assistant (or use any local reverse proxy you prefer — Caddy, Traefik, etc.).
- In NPM, create a Proxy Host:
- Domain name:
lan.ha.yourdomain.com - Scheme:
http - Forward hostname / IP:
homeassistant - Forward port:
8123 - Websockets support: enabled (required for HA’s frontend)
- Block common exploits: optional
- Domain name:
- On the SSL tab of the proxy host, request a Let’s Encrypt certificate for
lan.ha.yourdomain.com. NPM can complete the DNS-01 challenge against Cloudflare directly — use the Cloudflare API token option. - In Cloudflare DNS, create a DNS-only (grey-cloud) A record:
- Name:
lan.ha - Content: the LAN IP of your Home Assistant host (e.g.,
10.10.21.221) - Proxy status: DNS only — not proxied through Cloudflare
- Name:
- Optionally create a matching AAAA record if you run IPv6 on the LAN. Use a static ULA or link-local IPv6 assigned to the HA host.
- From a LAN client, confirm
https://lan.ha.yourdomain.comloads HA directly.
Because the record is DNS-only, external clients resolving lan.ha.yourdomain.com will also get the LAN IP — but they can’t route to it, so it’s effectively internal-only. This is acceptable; it’s not a secret, and the LAN-only reachability is enforced by your router, not by DNS visibility.
Update the Companion App
- External URL:
https://ha.yourdomain.com - Internal URL:
https://lan.ha.yourdomain.com
The app will automatically use the internal URL when connected to your home wifi (it compares the SSID) and the external URL otherwise.
Update the http: block in Home Assistant
Since requests will now come from both the Cloudflared app and NPM, both need to be in trusted_proxies:
http:
use_x_forwarded_for: true
trusted_proxies:
- 172.30.33.0/24 # Cloudflared app
Restart HA after changes to the http: block.
Troubleshooting
| Issue | Solution |
|---|---|
| Browser doesn’t prompt for certificate | Confirm the hostname is listed under SSL/TLS > Client Certificates > Hosts. Clear browser cache and restart the browser. |
| 403 even with cert installed | Verify the .p12 was built from the correct cert/key pair. Check that the certificate hasn’t been revoked in the Cloudflare dashboard. Try a different browser to rule out cert store issues. On macOS, try moving the client identity from the login keychain to the System keychain for Safari. |
| Companion App: “Error Saving URL — Unacceptable status code 403” | The app’s background webhook POST is being blocked by the mTLS WAF rule. Add and (not starts_with(http.request.uri.path, "/api/webhook/")) to the rule expression. See Section 4.2. |
| Companion App saves the URL but notifications don’t arrive, sensors don’t update, location isn’t posting | Same root cause as the 403 above — the webhook POSTs from the app’s background URLSession don’t present the client cert. Apply the /api/webhook/ exclusion in Section 4.2. |
| Tunnel not connecting | Check the Cloudflared app logs in HA. Verify the tunnel token is correct. Ensure HA can reach the internet. |
| HA shows “400 Bad Request” or IP ban after adding NPM / Cloudflared | trusted_proxies is missing the proxy’s subnet. Add both the Cloudflared and NPM app subnets to http.trusted_proxies in configuration.yaml and fully restart HA. |
| Companion App can’t connect | Verify the .p12 is installed at the OS level. Try accessing the URL in the device’s browser first to confirm the cert works there. Then verify the WAF rule exempts /api/webhook/. |
| “Maximum number of certificates reached” | Delete revoked or unused certificates from SSL/TLS > Client Certificates before creating new ones. |
cf.tls_client_auth.cert_verified is always false |
Ensure the hostname is added to the Hosts list on the Client Certificates page. Without this, Cloudflare won’t request or validate client certs. |
| On-LAN clients get the Cloudflare “you have been blocked” page when hitting the external hostname | You are hitting the split-brain DNS failure mode. The client’s DNS is going through DoH and resolving to Cloudflare’s public IPs instead of your LAN. The reliable fix is to move to the split-URL architecture in Part 8. See Appendix A for the details. |
| macOS Safari intermittently shows the Cloudflare block page for a local hostname | macOS system DNS in recent versions uses encrypted DNS paths that ignore /etc/hosts in some situations and ignore your LAN’s local DNS server entirely. There is no user-facing toggle to disable this for a specific domain. Use the separate internal URL in Part 8 instead of trying to make split-brain DNS work. |
| Chrome ignores local DNS for a Cloudflare-fronted hostname | Chrome has its own DNS-over-HTTPS. Disable it at chrome://settings/security → Use secure DNS = Off. This makes Chrome use the OS resolver, which honors your LAN DNS. Note: this only mitigates Chrome; it does not fix Safari, iOS apps, or any native app on macOS. |
| HA Companion App on iOS always takes the Cloudflare path for the external URL, even on-LAN | iOS has no per-app or per-domain DoH override. When the OS resolves a Cloudflare-hosted name, it may use encrypted DNS that bypasses your LAN’s DNS server. Accept it and use the split-URL architecture in Part 8 — the app will pick the internal URL automatically when on your home wifi. |
Security Notes
- Save your private keys. Cloudflare only shows the private key once at creation time. If you lose it, you’ll need to revoke that certificate and create a new one.
- Use one certificate per device. This makes revocation clean — you only revoke the compromised device’s cert.
- The WAF rule is your enforcement layer. Without it, Cloudflare will request a client cert but won’t block requests that don’t provide one. The WAF rule with
not cf.tls_client_auth.cert_verifiedis what actually enforces access. - Webhook path is exempted from mTLS. The
/api/webhook/<token>path is excluded from the WAF rule so the Companion App’s background requests can post sensor and location updates. HA webhook URLs contain a long random secret that authenticates the caller. Treat those URLs as bearer secrets — do not include them in screenshots, logs, or shared backups. - Consider shorter validity periods (1 year) for devices that are more likely to be lost or compromised.
- HA login is still required. mTLS controls who can reach your HA instance at the network level. Users still need valid HA credentials to log in. This is defense in depth.
- The internal URL is protected by your LAN, not by Cloudflare. If an attacker is already on your home network, they can reach
lan.ha.yourdomain.comwithout a client certificate. Harden your wifi (WPA3 or strong WPA2, guest network isolation, etc.) accordingly.
Appendix A: Why Split-Brain DNS Fails with Cloudflare — A Cautionary Tale
This appendix documents what was tried, what broke, what was mitigated, and what turned out to be unfixable. It exists so you can skip rediscovering all of this yourself.
The goal
Run a single hostname — ha.yourdomain.com — that:
- Resolves to the LAN IP of the Home Assistant host when the client is on the home network (fast, direct, no tunnel, no mTLS).
- Resolves to the Cloudflare edge when the client is off-network (mTLS-protected via the tunnel).
This is the textbook “split-brain DNS” or “split-horizon DNS” pattern, and it works fine for hostnames that are not fronted by Cloudflare. With a Cloudflare-fronted hostname, it fails.
What was set up
- A local DNS record on the UniFi Dream Machine Pro (UDMP) pointing
ha.yourdomain.comto the HA host’s LAN IPv4. - The public Cloudflare DNS record for
ha.yourdomain.compointing to the Cloudflare Tunnel (orange-cloud proxied). - A working Cloudflare Tunnel + mTLS rule (as described in Parts 1–4 of this guide).
- A valid client certificate installed on the Mac and on an iPhone.
Symptoms
- On the Mac, Safari, Chrome, and the HA Companion App would sometimes show the Cloudflare “Sorry, you have been blocked” page when opening
ha.yourdomain.comfrom inside the home network. - On iPhone it was worse — the HA Companion App couldn’t initially connect to the external URL at all from on-LAN cellular-off.
- The same hostname worked fine from other clients some of the time, and sometimes stopped working after a browser cache clear.
pingandcurlfrom a Mac terminal resolved to the LAN IP correctly. Browsers and native apps did not.
Root cause #1 — IPv6 AAAA records
Cloudflare automatically returns AAAA (IPv6) records for proxied hostnames that point at Cloudflare’s anycast IPv6 addresses (2606:4700::...). Modern browsers implement RFC 8305 (“happy eyeballs”) and prefer IPv6 when both IPv4 and IPv6 are available. The local DNS server (UDMP) was returning only the A record for the LAN IPv4 address. The AAAA record came back from public DNS, pointed at Cloudflare, and the browsers preferred it over the local A record.
Mitigation: Added a local AAAA record on the UDMP pointing the hostname to a local IPv6 address, and assigned a static IPv6 address to the HA host (HA OS does not support setting static IPv6 via DHCPv6 on the UDMP, so the static address was set on the HA host itself via ha network update).
This helped but did not fully fix it — traffic was still reaching Cloudflare.
Root cause #2 — DNS-over-HTTPS (DoH) bypassing local DNS
Modern clients do not necessarily use the DNS server advertised by DHCP. Instead, they send their DNS queries to a public resolver over encrypted HTTPS or TLS:
- Chrome ships with its own built-in DoH enabled by default (
chrome://settings/security→ “Use secure DNS”). It resolves through its configured provider, bypassing the UDMP entirely. - macOS Safari / system resolver. Recent macOS versions can route DNS through encrypted transports opaquely. There is no user-facing setting to turn this off for a specific domain. Safari’s use of the system resolver means the result of the encrypted lookup is what Safari sees — which is the public Cloudflare record, not the local one.
- iOS Home Assistant Companion App uses the system resolver. iOS has the same encrypted-DNS behavior and the same lack of a per-domain override. There is no app-level setting to turn DoH off for the HA app.
- The UDMP itself had UniFi CyberSecure’s Encrypted DNS feature set to Predefined: Cloudflare. This meant the router’s own upstream DNS lookups for public names were going over DoH to Cloudflare — so even the network-wide DNS path was encrypted and bypassing any hope of interception, and the router received the public answer for its own cache.
When DoH is in play, the “local DNS override” you configured on your router is simply not consulted.
What was mitigated, partially
- Chrome can be mitigated: go to
chrome://settings/securityand set Use secure DNS to Off. Chrome will then fall back to the OS resolver, which (on macOS, if the OS is not also doing its own DoH for that domain) honors your LAN DNS. This worked for Chrome specifically. - UDMP can have its DoH setting disabled under Settings → Security → CyberSecure → Encrypted DNS → None. This stops the router from doing DoH but does not stop the individual client devices from doing their own.
/etc/hostson macOS can override DNS for specific hostnames and does work for some clients, but Safari’s behavior was inconsistent — it would honor/etc/hostssometimes and ignore it others. A fresh Safari cache clear would often re-break it. The leading hypothesis is that Safari is routing certain lookups through an OS-level encrypted DNS path that does not consult/etc/hosts.
What could not be mitigated
- Safari on macOS. No way to force it off the encrypted DNS path for a specific domain. No user-facing toggle.
/etc/hostsis unreliable. - HA Companion App on iOS. Same story — no per-app or per-domain override for encrypted DNS. The OS resolver decides, and the OS resolver will often return the Cloudflare public answer.
- Any other native macOS or iOS app that uses the system resolver. Same limitation.
The resolution — abandon split-brain DNS
After all the above, the only reliable architecture was to stop trying to make one hostname behave two ways. The setup was changed to:
ha.yourdomain.com— proxied by Cloudflare, mTLS-enforced, the only way in from outside. LAN clients never use this name.lan.ha.yourdomain.com— DNS-only (grey-cloud) A/AAAA record pointing at the HA host’s LAN IP, served by a local Nginx Proxy Manager instance with its own Let’s Encrypt certificate. LAN clients use this name exclusively.
The Companion App supports both an External URL and an Internal URL, and it picks based on the connected wifi SSID — so it automatically uses the internal URL at home and the external URL away from home. The mTLS WAF rule protects only the external URL. The internal URL is protected by the boundary of the home network itself.
This is documented in Part 8 as the recommended architecture. It sidesteps all of the DoH / IPv6 / split-brain issues entirely.
Lessons
- Split-brain DNS only works when no client in the topology is using DoH, which is no longer a realistic assumption.
- Split-brain DNS on a Cloudflare-fronted hostname is especially brittle because Cloudflare always returns AAAA records, and those records always reach any client that can do encrypted DNS out-of-band.
- When you hit a DNS-routing problem that only affects some apps or some browsers, check for DoH on every layer: the browser, the OS, the router, and any third-party secure-DNS extension.
- If you need both external and internal access to a self-hosted service and the external path uses Cloudflare, use two hostnames. It is shorter, faster, more reliable, and easier to debug than any amount of split-brain cleverness.
