Automated Security: IP-Based Bypass for Cloudflare Zero Trust access to self hosted applications

What this achieves

This setup provides access to locally hosted services (Home Assistant, Immich, Frigate, Paperless, etc.) without sacrificing security. By using an iOS Shortcut that triggers when you leave Wi-Fi, your mobile device automatically adds its current IP address to a “Safe List” in Cloudflare.

This allows you to maintain a strict “MFA-Required” policy for the open internet, while granting a “Bypass” (no login screen) to your trusted mobile devices. It essentially treats your mobile data connection as a “Known Network,” similar to how your home internet functions, making the external access experience much smoother for family members (WIFE FRIENDLY!!)

After almost 12 months of reading posts, AI bots and LOTS of trial and error… below is how I ended up doing it for me and thought others may be interested. This is not just for HA, it is for other applications. Of course I say support Nabu Casa…


1. Create your CF Safe IP List

Navigate to Zero TrustReusable componentsLists.

  1. Create manual list:
    • List name: safe-ip-list
    • List type: IP addresses
    • Add entry: Enter one static IP you wish to keep (e.g., your home internet).
  2. Gather IDs for the shortcut & script later on:
    • Click the three dots on your new list → Edit.
    • Check the URL bar. It should look like: https://one.dash.cloudflare.com/ACCOUNT_ID/reusable-components/lists/LIST_ID
    • Save the ACCOUNT_ID and the LIST_ID.

2. Generate API Token

  1. Go to the main Cloudflare Dashboard (top left).

  2. Select 3 dots next to your name → Account API Tokens.

  3. Create TokenCreate Custom TokenGet Started.

  4. Permissions:

    • AccountAccount Filter ListsEdit
  5. Save the Bearer TOKEN.

  6. Note: If you run into permission issues, you may also need:

    • AccountZero TrustEdit
    • AccountAccess: Apps and PoliciesEdit

3. Create the Access Policy

Navigate to Zero TrustAccessPoliciesReusable policies.

  1. Add a policy:
    • Name: IP-Exclude
    • Action: Bypass
    • Session Duration: 24 hours
  2. Add the rule - Navigate to Add rulesAdd include
    • Rules: Selector: IP list
    • Value: safe-ip-list

4. Apply Policy to Applications

Navigate to Zero TrustAccess controlsApplications.

  1. Edit your application (e.g., Home Assistant, Immich) – Note; If you don’t currently have an application Add an applicationSelf-hosted
  2. Under Policies, select IP-Exclude.
  3. Confirm.

Note: Ensure your Tunnel public hostname routes match the Application subdomain under Networks → Connectors → Tunnel → Configure.
If you don’t already have your applications created and host names linked to your local service, you need to do that as well.
Zero TrusNetworksConnectors3 dots next to the tunnelConfigurePublished application routes → Then that subdomain/domain needs to match the application, and you just put the local address of your service below. Ie. HA = http → 192.168.1.10:8123

You need to repeat this process for each application (e.g., Home Assistant, Immich, etc)

It should be noted that I have two policies per application, the above ‘bypass’ as well as an ‘allow’ policy which has email addresses in the include and my country in the require. Please read up or watch videos for further info on how


5. Automate IP Updates (iOS Shortcuts)

  1. Import Shortcut: Cloudflare IP Update Shortcut
  2. Edit Shortcut: Enter your ACCOUNT_ID, LIST_ID, and TOKEN (there are 2 of each, 6 in total to enter).
  3. Create Automation:
    • Open Shortcuts AppAutomation+.
    • Search for Wi-Fi → Select Is Disconnected.
    • Select Run Immediately.
    • Set the action to run the shortcut you just saved.

Note; This process will only add new IP’s, not duplicates. PS. I am currently only using iOS primarily for mobile devices, so I can’t provide the Android equivalent.


6a. Housekeeping (Python Script)

To keep your IP list clean and secure, use this Python script to remove old, inactive mobile IPs.

Requirements

  • Python 3.x
  • Requests library: pip install requests

Save as cloudflare-iplist-cleaner.py:

#!/usr/bin/env python3
import requests
import json
from datetime import datetime
print(f"=== Run started {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} ===", flush=True)
# --- Configuration ---
ACCOUNT_ID = "CF_ACCOUNT_ID"
LIST_ID = "YOUR_LIST_ID"
TOKEN = "YOUR_ACCESS_TOKEN"
TARGET_DESCRIPTIONS = ["Mobile phone IPv6 iOS shortcut", "Mobile phone IPv4 iOS shortcut"]
KEEP_COUNT = 4

URL = f"https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/gateway/lists/{LIST_ID}"
HEADERS = {
    "Authorization": f"Bearer {TOKEN}",
    "Content-Type": "application/json"
}

def clean_cloudflare_list():
    response = requests.get(URL, headers=HEADERS)
    if not response.ok:
        print(f"Failed to fetch list: {response.text}")
        return

    data = response.json()
    items = data.get("result", {}).get("items", []) or []
    
    to_remove_ips = [] # Changed to store just strings

    for desc in TARGET_DESCRIPTIONS:
        filtered_items = [i for i in items if i.get("description") == desc]
        
        # Sort newest to oldest
        filtered_items.sort(key=lambda x: x['created_at'], reverse=True)
        
        if len(filtered_items) > KEEP_COUNT:
            excess_items = filtered_items[KEEP_COUNT:]
            for item in excess_items:
                # FIX: Add only the string value, not a dictionary
                to_remove_ips.append(item["value"])
                print(f"Marked for deletion: {item['value']} ({item['description']} - {item['created_at']})")

    if to_remove_ips:
        # The API expects: {"remove": ["1.1.1.1", "2.2.2.2"]}
        payload = {"remove": to_remove_ips}
        patch_response = requests.patch(URL, headers=HEADERS, json=payload)
        
        if patch_response.ok:
            print(f"Successfully removed {len(to_remove_ips)} old entries.")
        else:
            print(f"Error during removal: {patch_response.text}")
    else:
        print("No cleanup necessary.")

if __name__ == "__main__":
    clean_cloudflare_list()

Change the TARGET_DESCRIPTIONS above (Ie. Mobile phone IPv6 iOS shortcut) to suit what you called them in the shortcut description.

Give permission to execute the script

chmod +x cloudflare-iplist-cleaner.py

6b. Housekeeping (Python Script - automation/crontab)

You can run it as often as you like, here is what my crontab looks like:

0 3 * * * /usr/bin/python3 /home/spaldo/cloudflare-iplist-cleaner.py >> /home/spaldo/cf_cleaner.log 2>&1

Disclaimer; I am not a security expert and this may not suit you if you are after super security…

Thanks to @Lewis and @final_blueberry in this forum who also worked on similar solutions.

2 Likes

What secures your python scripts that hold the keys to the kingdom?

Your wife and family’s password to access HomeAssistant?

Your castle may have high walls and a deep moat, but if the drawbridge is left down…

Doctor Spalding!

You took inspo and blasted this past 11!

Nice work!

My meagre contribution is hopefully not outdated…

But I am not sure I knew about the CF API and updating entries programmatically.

Your method would make access even more secure.

Fantastic!

I am not sure I understand the question. The script is hosted locally on one of my VM’s. This may not be for everyone…

Again, I don’t understand the user/pass question, that is entered or stored via the app, nothing to do with this. This is only the method of transport from your device to the hosted service, the logins, etc for Immich, HA, etc are either handled by the browser or respective application.

How do you give your family run only access to your scripts and not read access to examine them.

The family doesn’t need access to the scripts at all. It is only run from my VM by me (crontab). It is just cleaning up old IP addresses from the CF list.

Ok. Makes sense.

this is awesome, thanks for posting it! im just curious, how do you handle joining other wifi networks? for example, you get to a family members home wifi - wouldn’t you lose your ability to hit your apps? seems like the iOS shortcut could compensate for this by looking for events like when you join a particular network, but I’m not too familiar with iOS shortcuts and if it could handle this complexity.

I haven’t really added that because I don’t join many other wifi networks but it would be pretty easy, two methods come to mind and it just depends if you want to keep their IP long term or just temporarily.

  1. Temporarily; this would be the easiest way… just create a new automation in the shortcuts app, instead of triggering the shortcut when you leave a wifi network, you would trigger when you join. Ideally I would say select the wifi name so you’re not doing it for public wifi but you could do all networks as well if you don’t care…

  2. Long term; two methods:
    A) manually add the ip via the CF dashboard in your list
    B) Duplicate the shortcut, then change the descriptions to something else that the cleanup script won’t catch. Then create a new automation in shortcuts (similar to option 1) to fire when you join a wifi, select the duplicate script. Except this won’t wipe the IP in the cleanup script because it has a different description …

Recently we went to a hotel and I just added the IP manually in the CF dashboard. However you also could just run the script (original or duplicate) manually in shortcuts and not automate as well.

Thanks for this, just set it up now. Was having issues with authorization at first, but then realised “Bearer” needs to come before the API key value for the ‘Authorization’ header!

1 Like

Yep, it has to be exactly like that. You can play around with the full curl command in terminal to check if it gives an error first if you wanted as well