8 August 2026

Keeping Cloudflare Zero Trust Lists in Sync with Dynamic Cloud Provider IP Ranges

A practical pattern for consuming AWS/Azure network feeds, calculating desired state and safely synchronising Cloudflare Zero Trust lists with PowerShell.


Static IP allowlists age badly in cloud environments.

AWS, Azure and other cloud providers publish machine-readable network ranges precisely because the underlying infrastructure changes over time. If a security policy depends on those ranges, copying them into a spreadsheet or security console creates an obvious lifecycle problem.

The pattern I prefer is to treat the provider feed as desired state and the Cloudflare list as deployed state.

Dynamic Cloud IP list synchronisation

The basic workflow

Provider JSON feed

Filter / normalise

Desired CIDR list

Read Cloudflare list

Compare

Append missing / remove stale

For AWS, for example, the published ip-ranges.json feed contains prefixes annotated with service and region information. That makes it possible to create logical groups such as regional EC2/AMAZON ranges rather than treating every AWS prefix as equivalent.

Separate acquisition from policy

A useful design is a small configuration object describing how each list should be produced:

$Config = @(
    [pscustomobject]@{
        Name = 'AWS Europe'
        Uri  = 'https://example.invalid/provider-ranges.json'
        RegionPattern = 'eu-*'
        Services = @('AMAZON','EC2')
    }
)

The important point is not the exact syntax. It is that the synchronisation engine does not contain a separate copy-and-paste block for every list.

The configuration says what to select; the engine performs the same acquisition, filtering, comparison and update process for each entry.

Compare desired and deployed state

A list synchroniser should not replace an entire object blindly if the API supports incremental change.

Conceptually:

$desired = $ProviderPrefixes | Sort-Object -Unique
$current = $CloudflareItems | Sort-Object -Unique

$diff = Compare-Object -ReferenceObject $desired -DifferenceObject $current

$toAdd    = $diff | Where-Object SideIndicator -eq '<='
$toRemove = $diff | Where-Object SideIndicator -eq '=>'

The resulting request contains only the delta.

That gives better logging and makes it much easier to notice an unexpectedly large change.

Put a brake on deletion

External feeds can fail in interesting ways.

A parsing bug, empty response or upstream schema change should not result in the automation deciding that every currently deployed prefix is obsolete.

Before removing entries I therefore like checks such as:

The purpose is not to prevent change. It is to distinguish a legitimate provider update from my automation misunderstanding its input.

Multiple environments should use one desired-state calculation

If the same logical lists exist in test, development and production, calculate the provider data once and then apply it consistently.

This avoids a subtle failure mode where each environment retrieves the feed at a different point and temporarily ends up with different list membership.

Generalising beyond AWS

The same engine can support several data sources if it understands a few concepts:

Once those pieces become configuration, the automation moves from AWS script to cloud-list synchronisation service.

Why this belongs in Zero Trust engineering

A policy is only as correct as the data it references.

If a Zero Trust rule relies on a list that is manually maintained, the manual process is part of the security control whether we acknowledge it or not.

Automating the list lifecycle gives us:

That is a small example of a wider idea: automate the inputs to security policy, not just the policy itself.


All example names and values are synthetic. No production account IDs, credentials or internal network ranges are included.