Why Your VPN is a Liability: Zero-Trust Network Access in Modern SaaS
Zero-Trust Network Access in Modern SaaS: Tear Down Your VPNs
We need to talk about your perimeter security. If you are still relying on a traditional Virtual Private Network (VPN) to secure internal services in 2026, you are building a liability. The classic "castle and moat" model is dead. Once an attacker breaches the moat, they have unrestricted lateral movement across your entire internal network. This is unacceptable for modern Software as a Service (SaaS) environments.
The only viable path forward is Zero-Trust Network Access (ZTNA). In a zero-trust model, the network itself is considered hostile. Every single request, whether it originates from a remote worker in a coffee shop or a microservice within your own Kubernetes cluster, must be explicitly authenticated, authorized, and continuously validated.
This post breaks down the problem with traditional perimeters, the architectural shift required for ZTNA, and a concrete implementation strategy using modern identity-aware proxies.
The Problem: Perimeter Security is a Fantasy
Traditional network security relies on IP addresses and network boundaries. You place a firewall at the edge of your infrastructure and a VPN gateway for remote access. Once a user authenticates to the VPN, they are issued an internal IP address and granted broad access to the corporate LAN.
This architecture has three fatal flaws:
- Implicit Trust: The system implicitly trusts any entity operating from an internal IP address. If a developer's laptop is compromised, the attacker has a direct tunnel into your production environment.
- Lack of Granularity: VPNs operate at OSI Layer 3 or 4. They grant access to entire network segments (subnets), not individual applications. You cannot easily say, "Alice can access the internal metrics dashboard, but not the billing API," without managing a labyrinth of complex network ACLs.
- Poor User Experience: Routing all traffic through a central VPN gateway introduces massive latency and bandwidth bottlenecks.
You are securing the wrong thing. You are securing the network, when you should be securing the application.
Why It's Hard: The Complexity of Identity-Aware Access
If Zero-Trust Network Access is so superior, why isn't everyone doing it? Because shifting from network-centric security to identity-centric security is conceptually and operationally difficult.
It requires abandoning IP addresses as a unit of trust and replacing them with cryptographic identity and context.
Here is what makes the transition painful:
- Identity Federation: You must centralize identity management. Every application must integrate with your Identity Provider (IdP) - usually Google Workspace, Okta, or Azure AD. Retrofitting legacy internal tools that only support Basic Auth or no auth at all is a massive headache.
- Policy Management: In a VPN world, access is binary: you are on the network or you are not. In a ZTNA world, access policies are highly granular and contextual. You have to define rules based on the user's role, the device posture (is it a corporate-managed device? is disk encryption enabled?), the time of day, and the sensitivity of the application.
- Performance and Latency: Every request must be intercepted, authenticated, and authorized. If your identity-aware proxy is slow, your entire application suite feels sluggish.
Despite these challenges, the shift is mandatory. A compromised perimeter is a matter of "when," not "if."
Architecture: The Zero-Trust Network Access Blueprint
A robust Zero-Trust Network Access architecture in a SaaS environment replaces the VPN gateway with an Identity-Aware Proxy (IAP). The proxy sits directly in front of your internal applications, mediating every HTTP request.
The core components of this architecture are:
- Identity Provider (IdP): The source of truth for user identities and group memberships (e.g., Okta).
- Device Trust Provider: A system that evaluates the health and security posture of endpoints (e.g., CrowdStrike, Kolide).
- Policy Engine: A centralized service that stores and evaluates access rules.
- Identity-Aware Proxy (IAP): The enforcement point. It intercepts requests, consults the Policy Engine, and either forwards the request to the upstream application or rejects it.
The Request Flow
When a developer attempts to access an internal service (e.g., metrics.internal.yourcompany.com), the following flow occurs:
- The user's DNS resolves the hostname to the public IP of the Identity-Aware Proxy.
- The user's browser initiates a TLS connection to the proxy.
- The proxy checks for a valid cryptographic session cookie. If none exists, it redirects the user to the IdP via OpenID Connect (OIDC).
- The user authenticates with the IdP (requiring phishing-resistant MFA, such as a YubiKey).
- The IdP redirects the user back to the proxy with an identity token.
- The proxy passes the user's identity, device context, and the requested URL to the Policy Engine.
- The Policy Engine evaluates the request against defined rules (e.g., "Only engineers on company-owned devices can access metrics").
- If authorized, the proxy forwards the request to the upstream application. Crucially, the proxy injects an assertion (often a signed JWT) into the request headers.
- The upstream application validates the JWT to ensure the request came from the trusted proxy, not from a rogue process within the cluster.
This architecture provides continuous verification at Layer 7.
Implementation: Building ZTNA with Pomerium and Kubernetes
Let's look at a concrete implementation. We will use Pomerium as our identity-aware proxy, deploying it on a Kubernetes cluster (v1.29+). Pomerium is an excellent choice because it is open-source, incredibly fast, and integrates natively with standard IdPs.
We assume you have a Kubernetes cluster running and a service you want to expose securely, such as a Grafana dashboard.
Step 1: Deploying Pomerium
First, we need to configure Pomerium to connect to our IdP. We use a standard Helm chart. Here is an example values.yaml configuration for Pomerium, binding it to Google Workspace.
# pomerium-values.yaml
authenticate:
idp:
provider: google
clientID: "YOUR_GOOGLE_CLIENT_ID"
clientSecret: "YOUR_GOOGLE_CLIENT_SECRET"
serviceAccount: "base64_encoded_service_account_json"
ingress:
enabled: true
className: "nginx"
hosts:
- authenticate.internal.yourcompany.com
tls:
- secretName: pomerium-tls
hosts:
- authenticate.internal.yourcompany.com
config:
# The shared secret for communication between Pomerium components
sharedSecret: "generate_a_random_base64_string_here"
cookieSecret: "generate_another_random_base64_string_here"
Apply the helm chart:
helm repo add pomerium https://helm.pomerium.io
helm install pomerium pomerium/pomerium -f pomerium-values.yaml --namespace pomerium --create-namespace
Step 2: Defining Access Policies
Now we need to secure our Grafana instance. We do this by defining an Ingress resource with specific annotations that Pomerium understands. Instead of a standard Kubernetes Ingress, we will use Pomerium's Custom Resource Definition (CRD), PomeriumRoute.
This is where the power of ZTNA shines. We define policy as code alongside our application deployment.
# grafana-route.yaml
apiVersion: ingress.pomerium.io/v1
kind: PomeriumRoute
metadata:
name: grafana-secure-route
namespace: monitoring
spec:
from: https://metrics.internal.yourcompany.com
to: http://grafana.monitoring.svc.cluster.local:80
policy:
- allow:
and:
- domain:
is: yourcompany.com
- claim/groups:
has: "engineering-team@yourcompany.com"
This configuration states: Allow access to https://metrics.internal.yourcompany.com ONLY IF the user authenticates with a @yourcompany.com email address AND is a member of the engineering-team group.
Apply the route:
kubectl apply -f grafana-route.yaml
Step 3: Upstream Validation (The Critical Step)
If you stop at Step 2, you have a vulnerability. What happens if an attacker compromises a pod inside your Kubernetes cluster? They could bypass Pomerium entirely and send requests directly to http://grafana.monitoring.svc.cluster.local:80.
To achieve true Zero-Trust Network Access, the upstream application (Grafana) must verify that every request passed through Pomerium.
Pomerium injects an X-Pomerium-Jwt-Assertion header into every request it proxies. This JWT is signed by Pomerium's private key.
Your application must validate this JWT. If you are building custom Go microservices (e.g., using Go 1.22), you implement middleware to perform this check.
package main
import (
"context"
"crypto/rsa"
"fmt"
"net/http"
"strings"
"github.com/golang-jwt/jwt/v5"
"github.com/lestrrat-go/jwx/v2/jwk"
)
// JWKS URL for Pomerium
const pomeriumJWKSURL = "https://authenticate.internal.yourcompany.com/.well-known/pomerium/jwks.json"
func requirePomeriumAssertion(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertion := r.Header.Get("X-Pomerium-Jwt-Assertion")
if assertion == "" {
http.Error(w, "Missing Pomerium Assertion", http.StatusUnauthorized)
return
}
// Fetch and cache the public keys from Pomerium
ctx := context.Background()
set, err := jwk.Fetch(ctx, pomeriumJWKSURL)
if err != nil {
http.Error(w, "Failed to fetch keys", http.StatusInternalServerError)
return
}
// Parse and validate the JWT
token, err := jwt.Parse(assertion, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodES256); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
kid, ok := token.Header["kid"].(string)
if !ok {
return nil, fmt.Errorf("missing kid header")
}
key, ok := set.LookupKeyID(kid)
if !ok {
return nil, fmt.Errorf("key %v not found", kid)
}
var rawKey interface{}
if err := key.Raw(&rawKey); err != nil {
return nil, err
}
return rawKey, nil
})
if err != nil || !token.Valid {
http.Error(w, "Invalid Assertion", http.StatusForbidden)
return
}
// Proceed to the application
next.ServeHTTP(w, r)
})
}
By enforcing JWT validation at the application layer, you render network boundaries irrelevant. Even if an attacker is on the same subnet, they cannot bypass the authorization checks.
Pitfalls: Where ZTNA Deployments Fail
Deploying an identity-aware proxy is straightforward. Actually transitioning an organization to a zero-trust model is difficult. Here are the common failure modes.
1. Ignoring Legacy Applications
Modern SaaS apps speak HTTP and natively understand OAuth or OIDC. Legacy internal tools often do not. They might rely on hardcoded IP addresses or basic authentication.
Do not try to rewrite these applications immediately. Instead, use your proxy to inject headers or perform basic auth translation. If an application uses a non-HTTP protocol (like SSH or RDP), you need a proxy that supports tunneling (Pomerium and Teleport both handle this well).
2. The "Break Glass" Antipattern
Teams often implement strict ZTNA policies, but leave a backdoor VPN running "just in case" the identity provider goes down. This defeats the purpose. Attackers will find the VPN.
Instead of a parallel, insecure network, design your ZTNA architecture for high availability. Use redundant IdPs or ensure your proxy can cache policy decisions and cryptographic keys to survive brief IdP outages.
3. Alert Fatigue
A zero-trust architecture generates an immense volume of logs. Every single request is an authorization event. If you dump all these logs into a SIEM without aggressive filtering and correlation, your security team will be overwhelmed by alert fatigue.
Focus on logging denied requests that originate from known corporate devices, or impossible travel scenarios within your identity logs.
Outcome: A Resilient Modern SaaS
Moving to Zero-Trust Network Access is a significant engineering investment. It requires retraining teams, updating infrastructure, and adopting new operational mindsets.
But the outcome is a fundamentally more resilient organization.
When you eliminate the VPN, you eliminate the concept of internal vs. external networks. You grant access based on identity and context, not IP addresses. You gain microscopic visibility into who is accessing what, and when.
Most importantly, you drastically reduce the blast radius of a compromised endpoint. In a perimeter-based world, a stolen developer laptop is a catastrophic breach. In a zero-trust world, it is a contained incident.
Tear down your moats. Secure your applications. Start building a zero-trust architecture today.
