A scope note before anything else. This post is about Microsoft Entra Global Secure Access (GSA) - the Security Service Edge product that spans Microsoft Entra Internet Access and Microsoft Entra Private Access, controlled through traffic forwarding profiles. Everything below is grounded in the networkAccess Microsoft Graph namespace that actually exposes those profiles. One important caveat up front, repeated where it matters: that namespace and its PowerShell cmdlets currently live in the beta Graph endpoint. Beta APIs are, in Microsoft's own words, "subject to change" and unsupported for production. The read surface is stable enough to audit with, but pin your module version and re-check the property names when you upgrade.
With that out of the way: GSA has a very specific way of failing quietly. Nothing errors. The client shows connected. A quick Conditional Access test passes. And yet the exact traffic you deployed GSA to inspect - general internet and SaaS egress, or your private line-of-business apps - is sailing straight out of the network untouched, because the profile that was supposed to acquire it was never switched on. Below: how that happens, how to confirm one profile's real state by hand, and a read-only PowerShell audit that surfaces every disabled or empty profile across the tenant.
The problem: the portal says GSA is deployed, but two of the three profiles never turned on
Here is the scenario, and if you have stood up Global Secure Access in the last year you will recognise the shape of it. A project lands to "roll out GSA". Someone installs the client to a pilot ring, enables the Microsoft 365 traffic forwarding profile, and confirms that Teams and SharePoint traffic is now being acquired by the service. The client tray icon is green. Sign-in works. A Conditional Access policy is drafted that blocks everything except the All Compliant Network locations named location, on the reasonable-sounding assumption that "our users' traffic now comes through GSA, so it originates from the compliant network." A quick test on a pilot device passes. The project is marked done.
Months later, a security review asks a simple question: show me the web content filtering and TLS inspection logs for traffic to a particular risky SaaS domain. And there is nothing. Not "blocked", not "allowed" - nothing, because that traffic never went through GSA at all. It egressed directly from the endpoint to the internet, exactly as it did before GSA existed. The Internet access profile - the one that acquires general internet and SaaS traffic - was left in its default state, which is disabled. Enabling the Microsoft 365 profile did not enable the other two, and nothing in the portal shouted about it.
To see how misleading the surface can be, look at what Microsoft's own documented example response for the forwarding profiles API returns for a freshly provisioned tenant. It is not a hypothetical - this is the sample payload in the Learn reference:
Two of the three profiles ship disabled. That is the default posture, straight from the reference documentation. If nobody explicitly turned Internet access and Private access on, they are off - and every byte of the traffic they were meant to acquire is leaving your endpoints directly.
Why it happens: traffic is only acquired when a profile is enabled AND a policy inside it matches
Three terms carry the rest of this post, so let me define them plainly. A traffic forwarding profile decides which class of traffic GSA acquires (tunnels) versus skips. A forwarding policy - linked into a profile through a policyLink - defines the specific destinations (FQDNs, IP ranges) that the profile actually matches. And acquisition is the act of the client capturing that traffic and sending it into the GSA service instead of letting it egress locally. If traffic is not acquired, GSA never sees it, and therefore none of your filtering, TLS inspection, or threat-intelligence rules can possibly apply to it.
Microsoft's traffic forwarding concept doc states the evaluation order and the failure mode in one line worth committing to memory: traffic is evaluated "first through the Microsoft access profile, then through the Private access profile, and finally through the Internet access profile. Any traffic that doesn't match these three profiles isn't forwarded to Global Secure Access." A disabled profile matches nothing. So a disabled Internet access profile is functionally identical, from the traffic's point of view, to GSA not existing at all.
enabled by default
Teams, SPO, EXO acquired
disabled by default
private apps NOT acquired
disabled by default
SaaS/web NOT acquired
direct egress
uninspected, unfiltered
There is a second, subtler layer. A profile can be enabled and still acquire nothing useful, because the enabled/disabled state also exists one level down, on each linked policy. In the Graph model a policyLink carries its own state of enabled or disabled. On top of that, the Internet access profile allows bypass rules that take precedence - a wide bypass entry can carve a destination back out of the tunnel even though the profile itself is on. So "the profile is enabled" is necessary but not sufficient; you also need at least one enabled policy that actually matches the destinations you care about, and no bypass quietly excluding them.
How to verify: check one profile's real state by hand before trusting any script
Before running anything tenant-wide, spend two minutes confirming what the API actually returns for your own tenant. This is a single read call, and it is the one manual check I would never skip - it both sanity-checks the script's numbers and shows you the raw state values with no interpretation in the way.
If you see disabled next to internet or private, that traffic class is not being acquired, full stop. To go one level deeper on an enabled profile - to confirm it actually has live policies rather than just being switched on with nothing behind it - expand the linked policies and check their state too:
NetworkAccess.Read.All, never the ReadWrite scope, so it cannot enable, disable, or reconfigure a profile or policy even by accident. It reports what is off; turning anything on stays a deliberate, human decision made in the portal.The fix: Get-GsaProfileCoverageReport.ps1
Opening the Traffic forwarding blade by hand is fine for one tenant on one day. It does not scale to a managed estate, and it does not catch the profile someone quietly disabled last Tuesday. The script below uses the Microsoft Graph beta PowerShell SDK to enumerate all three traffic forwarding profiles, expand their linked policies, and classify each one: enabled and carrying live policies (fine), disabled (that whole traffic class egresses directly), or enabled-but-empty (on, but acquiring nothing). It also flags the two edge cases that matter most - every profile disabled, and an expected profile type missing entirely.
Download Get-GsaProfileCoverageReport.ps1 from the Imran76Awan/Daily-Tasks repository on GitHub — no sign-in required. It is read-only and cannot change anything in your tenant — but always validate any script in your own environment before you rely on it.
It authenticates two ways: unattended app-only certificate auth (-TenantId -ClientId -CertificateThumbprint) for scheduled runs, or an interactive -UseDeviceCode fallback. Either way the only scope it requests is NetworkAccess.Read.All, which is the least-privileged permission the forwarding-profiles API accepts. For delegated sign-in, Microsoft lists the built-in roles that grant exactly this and no more: Global Reader, Global Secure Access Log Reader, Global Secure Access Administrator, or Security Administrator.
The core loop is deliberately blunt: read every profile, expand its policies, and decide. A profile with state -ne 'enabled' is a HIGH finding. An enabled profile with zero policies, or with policies that are all disabled, is a MEDIUM finding - on, but acquiring nothing. And the classification never swallows an error: if the Graph call itself fails, a script-level error flag is set, the script prints a clear error and exits non-zero rather than printing a reassuring "0 findings".
Get-MgBetaNetworkAccessForwardingProfile lives in the Microsoft.Graph.Beta.NetworkAccess module and hits the /beta endpoint. Property names like state and trafficForwardingType are documented today, but beta is explicitly changeable. Treat this as a monitoring aid, not a compliance oracle, and re-validate after a module upgrade.Proof it worked: re-running after the Internet access profile is enabled
Back to the estate from the top of this post. The remediation was not clever - a Global Secure Access Administrator opened the Traffic forwarding blade, enabled the Internet access profile, confirmed it had a live policy matching the SaaS destinations in scope, and let the client pick up the new policy. Note the fix is a deliberate portal action by a privileged admin, not something this read-only script does or should do. Here is the same audit, run again afterwards:
Internet access now reports enabled with a live policy - SaaS and web traffic is finally being acquired and can be inspected. The Private access profile is still deliberately off, because this estate does not publish private apps through GSA yet, and the report correctly keeps flagging it so that decision stays visible and intentional rather than forgotten. That is the whole point: the script does not tell you every profile must be on. It tells you the truth about which are, so the gaps are ones you chose.
enabled to disabled - or a policy count dropping to zero - is exactly the change that otherwise goes unnoticed until a security review. Exit code 2 means findings, 1 means the query failed; wire both into your alerting so a broken run never masquerades as a clean tenant.| What you see | What it actually means |
|---|---|
| Profile state: disabled | That entire traffic class egresses directly. No acquisition, no inspection, no filtering. Same as GSA not existing for that traffic. |
| Profile enabled, 0 policies (or all policy links disabled) | Switched on but matching nothing. Looks deployed; acquires no traffic. |
| All profiles disabled | Client gets an empty policy. Even the always-on Entra traffic profile stops - compliant-network signal disappears and CA assumptions break. |
| Compliant-network CA test passes | Proves auth traffic is acquired (Entra traffic profile). Says nothing about SaaS/private data-plane inspection. |
References
- forwardingProfile resource type (beta) — Microsoft Graph (state: enabled/disabled, trafficForwardingType: m365/internet/private)
- List forwardingProfiles (beta) — Microsoft Graph (GET /networkAccess/forwardingProfiles, NetworkAccess.Read.All, default states)
- Get-MgBetaNetworkAccessForwardingProfile — Microsoft.Graph.Beta.NetworkAccess reference
- Global Secure Access traffic forwarding profiles — Microsoft Learn (evaluation order; unmatched traffic not forwarded; always-on Entra traffic profile)
- Enable Compliant Network Check with Conditional Access — Microsoft Learn
- policyLink resource type (beta) — Microsoft Graph (state: enabled/disabled per linked policy)
- How to Setup Microsoft Entra Internet Access — Daniel Bradley, ourcloudnetwork.com (community walkthrough; supplementary)
Have you actually confirmed that all three of your GSA profiles are enabled and carrying live policies - or are you assuming the estate is covered because the client tray icon is green and a Conditional Access test passed once? Run the audit, diff it against the portal, and tell me how many tenants are sitting on a disabled Internet access profile nobody remembers leaving off.