Public SaaS pages can stay open to everyone while the admin operations behind them refuse anyone outside the VPN. The mechanism that makes that work is not a hidden menu or a separate login page; it is a check on the network path that delivered the request. Get the address evidence wrong and the check either locks out real administrators or admits anyone who can type a header. Start with what the origin actually receives.

What the origin actually receives after the tunnel

When a browser reaches the public site through a VPN that also carries internet traffic, the gateway translates the connection before it leaves. The SaaS sees the gateway's public egress address, typically one address or a small range, and never sees the laptop's tunnel address. That is why allowlisting 10.90.0.42 fails, and why allowlisting all of RFC 1918 fails louder: the second admits every private network that can reach you. The only address worth conditioning on is the one your organisation actually owns at the exit.

curl -s https://ifconfig.me
dig +short admin.example.com

Split tunnelling breaks the assumption quietly. If the VPN only carries private office routes, the request to the public admin hostname leaves through the local ISP and the origin sees a residential address. A connected VPN icon proves nothing about this particular destination, so test the destination rather than the tunnel.

Stop and fix the routing before you write any policy if the exit address varies per user. A condition that matches only some administrators is worse than no condition, because it hides the problem until an incident.

Trust only the proxy that rewrites the address

Most public sites sit behind a CDN or reverse proxy, so the origin's socket address belongs to that frontend and not to the visitor. The frontend usually forwards the original address in a header, and that header is attacker-controlled unless the origin accepts it only from known proxies. Configure the trusted source list explicitly and make the origin unreachable from anywhere else. Without that second half, an attacker skips the proxy and sends the header directly.

set_real_ip_from 192.0.2.0/24;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
curl -H 'X-Forwarded-For: 198.51.100.27' https://origin.example.com/admin

The same reasoning applies to the PROXY protocol: it is trustworthy only when the listener is bound to an interface the proxy can reach and nothing else can. Missing address context must fail the protected check rather than fall through to a default allow. A header that is absent is not evidence of a trusted path.

Attach the condition to every protected operation

Put the check in the authorization boundary that every protected operation passes through, not in the page that renders the menu. Route groups, middleware or a policy layer all work; a per-handler copy does not, because the handler someone adds next quarter will not have it.

user = requireAuthenticatedSession()
requirePermission(user, "admin")
requireVpnEgress(verifiedClientAddress(request))
performProtectedOperation(user, request)

The same boundary has to cover the JSON API, exports, signed download URLs and the WebSocket handshake, since none of them needs the HTML page to be requested first. A download link minted while the VPN was up stays valid afterwards unless the download endpoint re-checks. Long-lived sessions need a re-evaluation rule too, or one authenticated request becomes a permanent exemption. Default denial is the only setting that survives staff turnover.

Shared caches are the other way around the check. A cache that already holds a protected response can serve it without contacting the origin, so mark admin responses private and keep them out of shared storage. If a cache must serve them, the access check has to run before the serve, which usually means it cannot be a shared cache at all.

Test the bypasses you just closed

Exercise the matrix rather than the happy path: an administrator through the VPN, the same administrator off it, a non-administrator through it, and a forged address header. Add a direct-to-origin request and a cached-response request, because those are the two that skip the frontend entirely.

curl -sI https://admin.example.com/export.csv
curl -s -H 'X-Forwarded-For: 198.51.100.27' https://origin.example.com/admin
curl -s https://admin.example.com/api/me -o /dev/null -w '%{http_code}\n'

Each request should return the status the policy promises, and the log should record which address evidence was used. Cover IPv6 separately, because a dual-stack origin may see a different source family than the one you tested. Keep ordinary customer operations working outside the VPN, or the change will be reverted the first time a customer complains. Write the expected result for each row before you run it, so a surprising pass is visible.

The egress address proves a permitted network path, not a person and not a managed device. Keep it as one condition beside authentication and permission, and the admin area stays narrow without turning the whole product into a VPN-only service. That separation is the design, not a limitation of it.

Makomi ya ndenge moko