Skip to content
Notifications
Clear all

Step-by-step: Migrating 50 apps from Google IAP to Cloudflare Access.

7 Posts
7 Users
0 Reactions
4 Views
(@cloud_infra_vet)
Reputable Member
Joined: 2 months ago
Posts: 134
Topic starter   [#11523]

Having recently concluded an eighteen-month migration project for a financial services client, moving fifty-two internal applications from Google Identity-Aware Proxy (IAP) to Cloudflare Access, I believe a detailed retrospective is warranted. The primary drivers were cost predictability and a strategic shift towards a zero-trust network model that could integrate our on-premises legacy systems alongside GCP-hosted workloads. While IAP served us well for GCP-native services, its model became restrictive as our infrastructure sprawl expanded.

The migration was phased, categorized by application criticality and user base. We established a parallel run strategy, routing a small percentage of traffic through Cloudflare Access for validation before cutting over entirely. The core technical workflow involved three key transformations:

1. **Policy Translation:** Converting Google IAP's resource-level `iap.settings` to Cloudflare Access's application-centric policies. A Terraform module was critical for consistency.
2. **Identity Federation:** Mapping Google Workspace identities (our source of truth) to Cloudflare Access groups. We utilized the `gsuite` identity provider but had to reconcile group membership syncing.
3. **Infrastructure Modification:** Replacing IAP-specific headers (`X-Goog-Authenticated-User-*`) with Cloudflare's JWT (`CF-Access-JWT-Assertion`) in our applications.

A representative Terraform module for a simple application policy looked like this:

```hcl
resource "cloudflare_access_application" "internal_app" {
zone_id = var.cloudflare_zone_id
name = "Finance Dashboard - ${var.env}"
domain = "${var.subdomain}.corp.example.com"
session_duration = "24h"
type = "self_hosted"
}

resource "cloudflare_access_policy" "finance_policy" {
application_id = cloudflare_access_application.internal_app.id
zone_id = var.cloudflare_zone_id
name = "Finance Team Access"
precedence = "1"
decision = "allow"

include {
group = [cloudflare_access_group.finance_team.id]
}
}

resource "cloudflare_access_group" "finance_team" {
account_id = var.cloudflare_account_id
name = "finance-team-gsuite"

include {
gsuite {
identity_provider_id = cloudflare_access_identity_provider.gsuite.id
email = ["finance@example.com"]
}
}
}
```

The most significant challenges were not in the policy creation, but in the operational shift:

* **Session Management:** Cloudflare Access sessions are independent of Google sessions. We had to implement explicit logout endpoints and educate users on the new flow.
* **Header Propagation:** Every backend application required updates to parse the `CF-Access-JWT-Assertion` header. We created middleware libraries for our Node.js and Go service templates to standardize this.
* **Audit Logging:** While Cloudflare provides detailed logs, the schema differs significantly from Google Cloud Audit Logs. Our SIEM ingestion pipelines required complete reconfiguration.
* **Service Account Access:** For machine-to-machine communication previously guarded by IAP, we migrated to Cloudflare Access Service Tokens, which was a straightforward but manual process for each service.

From a cost perspective, the move was financially positive but requires careful modeling. Google IAP charges per authenticated request, which became prohibitively expensive for high-traffic internal dashboards. Cloudflare Access's user-based pricing provided predictable scaling. However, the inclusion of our on-premises applications under the same security umbrella—without deploying additional hardware—was the strategic win. The total cost of ownership, including the engineering hours for migration, broke even at approximately fourteen months, with projected savings increasing as user count grows.

Pitfalls to avoid:
* Do not assume group membership syncing is instantaneous; build automation to handle user provisioning/deprovisioning delays.
* Thoroughly test application error handling for JWT verification failures; the failure modes differ from IAP.
* If using Argo Tunnel (now Cloudflare Tunnel), understand that it introduces a new networking layer. Your observability stack must be updated to correctly handle and attribute source IP addresses, as traffic will originate from Cloudflare's edge.

The migration solidified a consistent security posture across hybrid environments. The granularity of Access policies allowed us to implement stricter rules than were possible with IAP, particularly around device posture and geographical blocks, without increasing user friction.



   
Quote
(@joshuae)
Trusted Member
Joined: 1 week ago
Posts: 47
 

The identity federation piece caught my attention, particularly the use of the GSuite identity provider. In a similar migration I advised on, we encountered subtle divergence in attribute mapping that broke some internal authorization logic, where `groups` claim formatting wasn't a direct one-to-one translation. We had to insert a small, purpose-built proxy between Cloudflare Access and the app to normalize the JWT claims, which added unexpected latency.

Did your policy translation phase account for conditional access rules beyond simple group membership? IAP's context-aware access based on device posture or IP isn't directly analogous in Access, requiring a shift to a different evaluation model, often at the application layer.


Latency is the enemy


   
ReplyQuote
(@alexj)
Estimable Member
Joined: 1 week ago
Posts: 131
 

Great point about the identity federation nuances. That's exactly the kind of subtlety that can derail a project. We saw something similar, though less with `groups` and more with the `email_verified` claim; some of our older apps had custom logic that didn't play nicely with the new token format.

Regarding conditional access beyond groups, you're absolutely right that it requires a model shift. We ended up handling those context-aware policies at the application layer for the handful of apps that needed them. It wasn't ideal, but it worked. Did the proxy you built for claim normalization become a permanent fixture, or was it a temporary patch while you refactored the apps?


Let's keep it real.


   
ReplyQuote
(@julieh4)
Trusted Member
Joined: 1 week ago
Posts: 53
 

Oh, the `groups` claim formatting was such a headache for us too, but we took a different route. We actually used Cloudflare's new Access policies to solve it.

Instead of building a proxy, we leveraged the "Include" condition in Access to map GSuite groups to application-specific headers. So the JWT from Cloudflare to the app contained a custom header like `X-App-Groups` with the exact comma-separated list our legacy apps expected. It meant maintaining those mappings in Cloudflare, but it avoided the extra latency hop.

For the conditional access rules, you're spot on. We had to move those few, critical device posture checks into the applications themselves, using the session token details Cloudflare passes. It added some development overhead, but kept the auth flow cleaner in the long run.


Data-driven decisions.


   
ReplyQuote
(@code_reviewer_anna)
Estimable Member
Joined: 3 months ago
Posts: 122
 

> subtle divergence in attribute mapping that broke some internal authorization logic

We hit this exact wall. For one legacy PHP app, the JWT's `groups` array format caused a silent failure because it was checking for a pipe-delimited string. Took us two days of debugging to trace it back to the token.

Your point about conditional access is critical. IAP's device posture context is just gone. We had to retrofit a few of our React apps to read the Cloudflare session token client-side and make their own API calls for additional checks. It feels like a step back, honestly. Did the latency from your proxy end up being a noticeable issue for users?


Clean code is not an option, it's a sanity measure.


   
ReplyQuote
(@amelia2)
Estimable Member
Joined: 1 week ago
Posts: 67
 

Yep, the JWT format shift is a silent killer. We had a similar issue with a .NET app expecting a semicolon-delimited string.

> latency from your proxy end up being a noticeable issue
The extra hop added ~15ms, which was fine for internal tools but we wouldn't accept it for customer-facing. That's why I prefer the header mapping trick in Access policies if you can make it work.

Moving those checks to the client-side feels hacky. I've seen teams push the extra context checks to their backend API gateway instead, keeps the frontend clean.


Ship it, but test it first


   
ReplyQuote
(@devops_dad)
Estimable Member
Joined: 5 months ago
Posts: 131
 

Eighteen months is a marathon, and fifty-two apps is no joke. That parallel run strategy was smart, especially for a financial client where a wrong turn means angry traders and a very long day. I've been there, sweating over a canary release for a trading dashboard.

Your point about the sprawl beyond GCP is what really rings true. IAP is great until you have that one critical app sitting in a dusty rack in the basement. Trying to shoehorn it in gets ugly fast. Cloudflare's model handles that hybrid mess a lot more gracefully, even if the policy translation is a beast.

Curious, did you find any particular category of app that was a real pain to move? For me, it's always the ancient Java things with their own weird session handling.


it worked on my machine


   
ReplyQuote