hello everyone! Fairly new to HA, but not to IT/computer/networking/security. I’m writing this because I know there have been many pain points around the iOS companion app, Cloudflare, and how the current security workarounds still expose HA instances to risk levels that some may not be comfortable with, including me.
I’m not going to open that can of worms here, but I am going to share how I hacked together architected a solution that works for me. If it helps at least one person, I’d be happy.
Kudos to the community, especially folks in this post, who got me thinking of ways to do it, and helping me enough for me to give back.
Anyhow, let’s dive in.
Requirements
I wanted:
- An additional layer of security other than just the HA login page (even though I’ve implemented 2FA)
- A non-WARP solution (I use another DNS provider and I don’t want to move away from that)
- A non-VPN solution
- A relatively seamless way to only grant HA access to my family’s Apple devices, including the Companion app
- Not to have to install any other software or third party tools on my iPhone to do all this
- Has to be cost effective (aka, “free”).
Caveats
- I have only done this on Macbooks and iPhones, my household doesn’t use Apple watches so ymmv.
- The techniques in here may come across as intermediate/advanced, depending on how comfortable you are with coding, APIs, and the like.
Tried and failed, or didn’t like it.
- mTLS auth via Cloudflare, as the iOS companion app doesn’t support it.
- Per the previously linked post, allowing access to my HA to an entire AS number. I know the risk is low but I’d rather not at all, if I could.
- Cloudflare Zero Trust Access - the companion app can’t do it’s thing if Access is enabled. I’d imagine if you’re reading this that you already know about that and are banging your head against the table.
But first, the setup
Fairly “standard”, HA on an old laptop, tunnelled via cloudflared (local tunnel) to a subdomain of a domain I own.
So what’s the strategy?
I used Cloudflare’s WAF (and only the WAF. no Zero Trust Access) to restrict access. I use three rules, and the magic lies in Lists - see $link1
in comments. I create a List of IPs (let’s call this ip_list
). My rules, in order, are:
- Allow access to HA from IPs in
ip_list
- Allow access to a specific HA webhook from
AS8075
(“But why?” - Read the section on Purging stale IPs.) - Block everyone else
But…your device changes IP, you can’t hardcode that!
Bingo! This is where it gets hacky. This is what I did at a high level. How exactly you go about doing this, I leave as an exercise for you, the reader.
On Cloudflare
- Create a List on Cloudflare. You’re allowed one free on a free account (accurate at time of writing)
- Add an IP into that list, which creates a list. Use something you can trust, like your home IP, or
1.1.1.1
to initialise this list. Call the list whatever you want, but I’ll refer to it here asip_list
- Generate an API token on Cloudflare (see
$link2
in comments). This allows you (or a program, or script, whatever), to do whatever you give it Permission(s) to. In our case, we want to provide it with theAccount -> Account Filter Lists -> Edit
permission. Just that single Permission will do. I’ll call the resultant tokenbearer_token
from now on. Treat this like yourunderwearpassword, don’t share it with anyone. - Get your Account ID (see
$link3
in comments). Will refer to this asaccount_id
from henceforth. - Get the id of the list you just created. the easiest way is to call the Get Lists (see
$link4
in comments) endpoint usingcurl
or something. The list id (there should only be one, because we’re only allowed one free one) is in the response. We’ll call thislist_id
from now on.
On your device
Get your device to use the token generated to call the Create List Items (see $link5
in comments) endpoint. What this does is, whenever it’s called, it can add a new IP to ip_list
. You will need to:
- Execute a
POST
request - With two headers:
- Authorization: Bearer
bearer_token
- Content-Type: application/json
- Authorization: Bearer
- To the endpoint
https://api.cloudflare.com/client/v4/accounts/account_id/rules/lists/list_id/items
- And the body/payload needs to look like this:
[{"ip":" <your_current_ip_address>","comment":"whatever_comment_you_want"}]
Trigger and frequency strategy
How you call it, and how often you call it, is entirely up to you. I’ve done mine in Shortcuts using the Get Contents from URL section (see $link6
in comments). I’ve set mine to start when I leave my home network, update my IP every 15 minutes, and stop when I get back to my home network.
I get it to execute every 15 minutes by turning on and off Focus, as per in the top comment on this post (see $link7
in comments). It doesn’t have to be 15 minutes, it can be any duration you want/feel comfortable with. You can also trigger it based on anything you want (e.g., update starting at 7am every Wednesday). Lots of options here.
I do this on my Apple devices and my HA server. I use Shortcuts Automation on iPhone, cron
on the Macbook, and a combination of shell_command
and Automations in HA.
But…
- What happens if the IP already exists in the ilst? Cloudflare only overwrites the comment, and the
modified_on
field (which you can see via the API) - What if you run out of IPs for the list? Each list allows 10,000 IPs…so you’re good. Or consider purging the list every so often (read on for more).
- Opening up to so many IPs is a huge security risk! Yes, I agree and personally am not comfortable with that, which is why I write about purging next…
Purging stale IPs from the list
I’ve set up a Python script that, when run, can use the bearer_token
and its access to:
- Pull down the list items (see
$link8
in comments) - Iterate through all of them, figure out which ones are stale (up to you to define this, I’ve got this set at 2 hours)
- Send off a request to this endpoint (see
$link9
in comments) to delete them. I’ll save you some trouble (because this isn’t very well documented (see$link10
in comments), but the payload you need to send is{"items":[{"id": entry_item_1_id}, {"id": entry_item_2_id}]}
. You can only specifiy 1id
to delete, or tack on more (than the 2 in the example) if you wish.
I put my code on Github (it’s a private repo, sorry), and I use a Github action to run this on a schedule (see $link11
in comments). For me, that’s every hour, so it automatically triggers without me or my machine being online. Github accounts are allowed 2,000 free action minutes per month, and each run of this takes 1 action minute.
I’ve also set up a webhook in HA to notify me (via push on mobile) if anything fails, which is the reason for that odd WAF rule mentioned above. I’ve only allowed Github (owned by Microsoft)'s AS to be able to use that webhook. This makes the risk low enough for me!
If you’re doing this, please make sure that you code in logic to stop if it’s going to delete ALL the entries. I’m not entirely sure what’ll happen if I delete all items in a list (will it get rid of the list? will it generate a new list id and break everything?), and I kinda don’t wanna find out.
Cool, so what’s the end look like?
I end up with a sort of “live IP tracking” solution, which allows ONLY my device’s IP (whichever device I use) to access my HA setup. Any stale IPs (not updated for more than 2 hours) is purged, and this is checked every hour. An attacker has a 2 hour window to use my exact IP, to find the subdomain of my HA on my owned domain, to bypass auth and 2FA and trigger something to execute on my HA system. That may slightly annoy me, but it’s not end of the world. I’m pretty comfortable with that.
At max, there’s about 3 IPs that will appear in the Cloudflare List (one for my Macbook, one for my iPhone, and one for my HA server), and towards the end of the night when all the devices are on my home network, there’s 1 entry. I’ve also used the “Comment” field in the List to identify which device updated at what time (e.g., “iPhone updated 2025-01-01 at 5:27 pm”).
The only difference that I can notice as an end user is that my Focus modes keep turning on and off on my iPhone. I take that as a reminder that the automation is being executed
This is my robust and secure solution to a problem - plus it gave me a great excuse to tinker.
Conclusion
I hope this helps at least inspire someone to architect something.
I’m also open to hear if you can suggest any improvements!
AMA, i guess? Happy automating!