Most Zero Trust demonstrations are intentionally small.

They prove one feature in isolation: protect a web application with Access, route a private subnet through a connector, enforce a Gateway rule or check device posture.

I wanted a lab where those pieces interacted.

The result is a Terraform-managed environment spanning AWS, Azure and Google Cloud, with Cloudflare providing the identity-aware access and network control plane.

Multi-cloud Zero Trust lab architecture spanning Cloudflare, AWS, Azure and Google Cloud

What I wanted to learn

The purpose of the project was not to create a generic production landing zone.

It was to give me a repeatable environment for answering architecture questions such as:

  • how do private resources across several clouds sit behind one Zero Trust access model;
  • where does WARP Connector routing fit compared with traditional site-to-site VPN design;
  • how do Access policies, identity groups and device posture combine;
  • how can browser-rendered SSH, RDP and VNC reduce direct network exposure;
  • what should be Terraform-managed and what still requires bootstrap/manual configuration;
  • how do supporting Workers extend Access decisions and denial experiences;
  • how do I destroy and rebuild the environment without losing the operational model.

That made the lab useful as an engineering system rather than a collection of screenshots.

The high-level architecture

The environment has four major layers:

Identity and devices

Cloudflare Zero Trust
  - Access
  - Gateway
  - WARP / device profiles
  - device posture
  - private routing

Cloud resources
  - AWS
  - Azure
  - Google Cloud

Demo workloads and administration paths

Terraform manages both the cloud infrastructure and a large portion of the Cloudflare configuration so the relationships are visible in code.

Why use three cloud providers

Multi-cloud is often discussed as a strategy goal. In this lab it is simply a useful test condition.

If the same access pattern works for resources in AWS, Azure and Google Cloud, I can reason about the Cloudflare layer independently of one provider’s networking model.

Each cloud still has its own constructs, but the user-facing access pattern becomes more consistent:

user identity
+ device state
+ application policy
+ Cloudflare private routing

rather than:

which VPN am I on?
which cloud network is this resource in?
which firewall path reaches it?

That contrast is exactly what I wanted the lab to make tangible.

Keep the Terraform structure modular

The repository separates concerns into modules rather than putting every resource in one root file.

The structure includes dedicated areas for:

  • Cloudflare configuration;
  • cloud-provider resources;
  • key material;
  • WARP routing calculations.

The Cloudflare module contains resources for areas such as:

Access applications
Access policies
Gateway policies
WARP/device profiles
DNS records
rule groups
private network tunnels
short-lived certificates
SSH CA management

This is important because a demo environment can become difficult to learn from if all of its configuration is compressed into one large Terraform file.

The module boundaries make the architecture easier to inspect.

Private routing is where the lab becomes interesting

Protecting a public web application with Access is useful, but private routing changes the architecture more substantially.

WARP Connector tunnels provide connectivity from Cloudflare into the private networks containing the lab workloads.

The client does not need direct reachability to each cloud network. Traffic can be routed through the Cloudflare fabric according to the device/client configuration and private-network routes.

That lets the lab demonstrate access to resources that would traditionally be reached through a network VPN.

Generate routing data instead of hand-maintaining it

As the number of cloud networks grows, WARP routing and split-tunnel configuration can become another list-maintenance problem.

I prefer deriving those values from the infrastructure definition.

The repository includes a WARP routing module that calculates the subnets required by the environment and produces outputs that can be consumed by the Cloudflare configuration.

The principle is the same as in my generated WARP configuration work:

infrastructure data should be the input to client/network policy where possible.

That reduces drift between “the network Terraform created” and “the routes the Zero Trust client is expected to use”.

Access policy combines identity with application context

The environment contains several Access applications and policies so I can test different authorization conditions without rebuilding the surrounding infrastructure.

The useful thing about a lab like this is seeing policy combinations rather than isolated statements.

For example, an application can require:

valid identity
AND approved group
AND acceptable device posture

while another path can add an external evaluation requirement.

That makes it possible to test how a user experiences policy changes across multiple applications with the same underlying identity and device.

Device posture is part of the lab, not an afterthought

The project includes device posture configuration and WARP client checks.

That matters because Zero Trust architecture becomes much more realistic when a valid identity can still be denied because the endpoint does not meet policy.

I also added automation to keep operating-system version posture checks aligned with current macOS and iOS releases.

That addresses an operational issue I have written about separately: posture rules can become stale if the reference values are maintained manually.

The broader lesson is that posture policy has a lifecycle. Terraform can define the control, but some of the values used by the control may need independent automation.

Browser-rendered administration changes exposure

The lab includes use cases for browser-based access to administrative protocols such as SSH, RDP and VNC.

The point is not that every administrative workflow should move into a browser.

It is that a lab should let me compare:

traditional network reachability to management port

with:

identity-aware application access through Cloudflare

That gives a much better basis for discussing where ZTNA can remove inbound exposure and where native protocol access is still required.

Workers extend the policy experience

Two separate Worker projects integrate naturally with the lab.

The first is the Cloudflare Training Compliance Gateway, which lets an Access policy require an external training-completion decision.

The second is the Cloudflare Access Denied Information Page, which gives denied users and support teams richer diagnostic information.

Those integrations are useful because they demonstrate that Access policy is not an isolated product feature. It can participate in a broader application and control ecosystem.

Remote state deserves its own lifecycle

The main lab uses an S3-based Terraform backend with DynamoDB supporting state locking/coordination in the backend project.

I keep that infrastructure separate from the lab itself.

The reason is simple: the thing storing the Terraform state should not disappear when I destroy the disposable demonstration environment.

The lifecycle becomes:

Backend infrastructure
        ↓ persistent
Terraform state

Disposable/rebuildable lab

That separation makes repeated destruction and recreation much safer.

Some bootstrap configuration remains manual

Infrastructure as Code does not mean pretending every platform action can or should be automated immediately.

The repository documents a small number of manual/bootstrap requirements, including obtaining identifiers or creating prerequisite configuration that Terraform then consumes.

I prefer to make those boundaries explicit rather than hiding them behind brittle automation.

A good lab should tell you:

  • what Terraform owns;
  • what must exist first;
  • which identifiers need to be supplied;
  • which secrets come from the environment;
  • what post-deployment steps remain.

That makes rebuilds repeatable even when the workflow is not 100% API-driven.

Destruction is a feature

A lab that cannot be removed cleanly becomes infrastructure debt.

I designed the environment with cleanup in mind, including scripts for temporary state such as known-host entries and enrolled Cloudflare devices.

Terraform handles the infrastructure it owns, while supporting cleanup deals with external/ephemeral artifacts that are not naturally destroyed with the main resource graph.

This is especially important for a learning environment because repeated build/destroy cycles are part of the value.

What this project changed for me

The lab made several Zero Trust concepts easier to reason about because I could test them together:

  • identity is only one policy signal;
  • endpoint posture needs operational maintenance;
  • private routing and application access are different layers;
  • denial UX matters once policy becomes strict;
  • infrastructure state and policy state both need lifecycle management;
  • multi-cloud does not require users to experience three separate remote-access architectures;
  • the most useful automation derives policy inputs from infrastructure data rather than duplicating them manually.

A platform for further experiments

The repository is intentionally broader than one demonstration.

It gives me a base for testing changes to:

  • Access policy composition;
  • Gateway filtering;
  • WARP device profiles;
  • device posture;
  • private network routing;
  • external evaluation;
  • Worker integrations;
  • Terraform provider behaviour;
  • failure and recovery scenarios.

That is why I think of it as a Zero Trust engineering lab, not just a Terraform demo.

The full project is documented in Cloudflare Multi-Cloud Zero Trust Demo.

Related articles include: