Cloudflare Gateway PAC builder
Builds a PAC file for a Cloudflare Gateway proxy endpoint with the required HTTPS directive and port 443, the identity-provider bypass Cloudflare marks as critical, and the private-network and plain-hostname bypasses of Cloudflare's template without a DNS lookup per request.
Cloudflare’s templates are a good base: correct RFC 1918 masks, loopback as a /8, lower-cased host,
IdP bypass first. Two things are changed here. The private-network test is guarded by an IP-literal
check instead of calling dnsResolve(host) for every request (one DNS dependency is qualitatively
different from none), and the *.local bypass is optional because some networks still use .local
as an internal DNS suffix.
Generated proxy.pac
[{"default":"abc123def0","help":"From the proxy endpoint you created in Cloudflare One (Networks \u003e Resolvers and proxies \u003e Proxy endpoints).","id":"subdomain","label":"Proxy endpoint subdomain","pattern":"^[a-z0-9]+$","pattern_help":"lower-case letters and digits (the part before .proxy.cloudflare-gateway.com)","type":"text"},{"default":"entra","help":"Cloudflare's template marks the IdP bypass as critical for authorization endpoints.","id":"idp","label":"Identity provider (bypassed to prevent authentication loops)","options":[{"label":"Microsoft Entra ID (login.microsoftonline.com, aadcdn.msauth.net, aadcdn.msftauth.net)","value":"entra"},{"label":"Okta (okta.com, oktacdn.com)","value":"okta"},{"label":"Google Workspace (accounts.google.com, gstatic.com)","value":"google"},{"label":"Other (list the hosts below)","value":"none"}],"type":"select"},{"default":[],"help":"One per line; the domain and all its subdomains are matched.","id":"idp_domains","item_pattern":"^[a-z0-9-]+(\\.[a-z0-9-]+)+$","label":"Additional identity-provider domains that go direct","max":20,"pattern_help":"must be a domain such as login.idp.example","type":"list"},{"default":["corp.example"],"help":"One per line, without a leading dot; the domain and all its subdomains are matched.","id":"internal_domains","item_pattern":"^[a-z0-9-]+(\\.[a-z0-9-]+)+$","label":"Internal DNS suffixes that go direct","max":50,"pattern_help":"must be a domain such as corp.example (no leading dot, no wildcard)","type":"list"},{"default":true,"help":"Uses an IP-literal test in front of isInNet, so host names never trigger a DNS lookup.","id":"bypass_private","label":"Plain hostnames and private (RFC 1918) addresses go direct","rules":["isinnet-on-hostname"],"type":"checkbox"},{"default":false,"help":"Cloudflare's template includes this. Leave it off if .local is an internal DNS suffix in your network; the checker notes it either way.","id":"bypass_mdns_local","label":"*.local names go direct (mDNS / Bonjour)","rules":["dot-local-direct"],"type":"checkbox"}]// proxy.pac for a Cloudflare Gateway proxy endpoint, generated by findproxyforurl.net
function FindProxyForURL(url, host) {
host = host.toLowerCase();
{{#if idp == "entra"}}
// identity provider (Microsoft Entra ID) is bypassed to prevent authentication loops
if (host == "login.microsoftonline.com" || host == "aadcdn.msauth.net" || host == "aadcdn.msftauth.net") {
return "DIRECT";
}
{{/if}}
{{#if idp == "okta"}}
// identity provider (Okta) is bypassed to prevent authentication loops
if (dnsDomainIs(host, ".okta.com") || dnsDomainIs(host, ".oktacdn.com")) {
return "DIRECT";
}
{{/if}}
{{#if idp == "google"}}
// identity provider (Google Workspace) is bypassed to prevent authentication loops
if (host == "accounts.google.com" || dnsDomainIs(host, ".gstatic.com")) {
return "DIRECT";
}
{{/if}}
{{#if idp_domains}}
// identity provider hosts are bypassed to prevent authentication loops
if ({{#each idp_domains sep=" ||\n "}}dnsDomainIs(host, ".{{.}}"){{/each}}) {
return "DIRECT";
}
{{/if}}
{{#if bypass_private}}
// plain hostnames (no dot) stay on the local network
if (isPlainHostName(host)) {
return "DIRECT";
}
// RFC 1918 and loopback addresses: literal IPs only, no DNS lookup for names
if (/^\d+\.\d+\.\d+\.\d+$/.test(host) &&
(isInNet(host, "10.0.0.0", "255.0.0.0") ||
isInNet(host, "172.16.0.0", "255.240.0.0") ||
isInNet(host, "192.168.0.0", "255.255.0.0") ||
isInNet(host, "127.0.0.0", "255.0.0.0"))) {
return "DIRECT";
}
{{/if}}
{{#if internal_domains}}
// internal domains and their subdomains go direct
if ({{#each internal_domains sep=" ||\n "}}dnsDomainIs(host, ".{{.}}"){{/each}}) {
return "DIRECT";
}
{{/if}}
{{#if bypass_mdns_local}}
// mDNS / Bonjour names (deliberate: .local is not an internal DNS suffix here)
if (shExpMatch(host, "*.local")) {
return "DIRECT";
}
{{/if}}
// default: everything else goes to the Gateway proxy endpoint (TLS to the proxy, hence HTTPS)
return "HTTPS {{subdomain}}.proxy.cloudflare-gateway.com:443";
}
Deploying it
Host the file as a Cloudflare-hosted PAC (
https://pac.cloudflare-gateway.com/<account-id>/<slug>,
256 KB limit) or on your own server with Cache-Control: max-age set, so that browsers pick up
changes without clearing their cache. Safari and iOS/iPadOS cannot use an HTTPS proxy endpoint.What the Cloudflare pack checks
A Gateway proxy endpoint is a TLS-terminating proxy, so the PAC must return
HTTPS <subdomain>.proxy.cloudflare-gateway.com:443;
a PROXY directive to the same host fails. Chromium browsers and Firefox support HTTPS proxies through a
PAC file; Safari and iOS/iPadOS do not. Cloudflare’s templates place identity-provider, private-network and
streaming bypasses in the PAC because Gateway has no other place for them in this deployment model.
Two refinements this site applies on top of the published templates: resolve private ranges without a DNS
lookup on every request (guard isInNet with an IP-literal test instead of dnsResolve(host)), and set
Cache-Control: max-age on the hosted file instead of asking users to clear their browser cache.- Directive: HTTPS, not PROXY; port 443. Authorization endpoints require TLS inspection and a browser that can complete the Access login.
- Safari and iOS/iPadOS do not support the HTTPS proxy type; WinHTTP support is not yet verified in a lab run.
- Hosted PAC files: https://pac.cloudflare-gateway.com/<account-id>/<slug>, 256 KB per file.
- The published templates call dnsResolve(host) for every request; the generator here keeps the same bypasses with an IP-literal guard, so hostnames never trigger a lookup.
- PAC-C027Cloudflare Gateway proxy endpoint with PROXY instead of HTTPS high
- PAC-C028Cloudflare Gateway proxy endpoint on a port other than 443 high
- PAC-C022Identity provider not excluded from the SSE proxy medium
- PAC-C026Plain hostnames or private addresses are sent to the cloud proxy medium
- PAC-B015Selected vendor pack, but the PAC never routes to that vendor info