DoiT Cloud Intelligence™

Modernizing GKE Internal Applications Access: From VPN to IAP-Enabled External Gateway

By Nir ForerJan 13, 20256 min read
Modernizing GKE Internal Applications Access: From VPN to IAP-Enabled External Gateway

Ditching that VPN in favor of a modern Zero Trust solution for internal HTTPS applications

Are you still managing VPN connections for your remote workforce to access internal GKE (Google Kubernetes Engine) applications? There’s a better way. In this post, we’ll explore how to replace traditional VPN-based access with Google Cloud’s Identity-Aware Proxy (IAP) using GKE External Gateway. This modern approach not only reduces operational overhead, but also provides more granular access control through IAM authentication and authorization.

Why Make the Switch?

Traditional VPN-based access to internal applications comes with several challenges:

  • High operational overhead in managing VPN tunnels
  • Additional costs for VPN infrastructure
  • Complex network routing configurations
  • Limited granular access control

By implementing an IAP-enabled External Gateway, you can:

  • Consolidate multiple applications behind a single HTTPS load balancer
  • Manage access across different GKE namespaces efficiently
  • Leverage Google Cloud’s robust authentication system
  • Reduce infrastructure costs and complexity

GKE Ingress vs Gateway: IAP Implementation Comparison

When implementing IAP with GKE, you have two main approaches: using GKE Ingress or GKE Gateway. Understanding their differences is crucial for choosing the right solution for your environment.

GKE Ingress Limitations

  • Namespace Restrictions: GKE Ingress can only reference Services within the same namespace where the Ingress resource is deployed:

GKE Ingress in namespace Fu cannot reference Service in namespace Bar

  • Load Balancer Resources: May require multiple load balancers if applications span different namespaces:

Must use 2 different HTTPS LB when services are in different namespaces — must also use domain-based routing which adds another layer of complexity

GKE Gateway Advantages

  • Cross-namespace Support: Can route traffic to Services across different namespaces from a single Gateway resource
  • Simplified Architecture: One load balancer can handle multiple applications regardless of their namespace
  • More Flexible Routing: Easier to implement complex routing patterns across namespaces

GKE Gateway in one namespace can send traffic to Services in other namespaces with a single HTTPS LB

Implementation Guide

Prerequisites

  • GKE Standard v1.24+ or GKE Autopilot v1.26+
  • VPC-native cluster configuration
  • HttpLoadBalancing add-on enabled

Step 1: Create GKE Cluster

gcloud container clusters create test-gateway \
 --region=us-central1 \
 --release-channel=regular \
 --enable-ip-alias \
 --num-nodes=1 \
 --addons HttpLoadBalancing \
 --gateway-api=standard

gcloud container clusters get-credentials test-gateway --region us-central1

Step 2: Configure SSL Certificate

cat <<EOF | kubectl apply -f -
apiVersion: networking.gke.io/v1
kind: ManagedCertificate
metadata:
 name: gateway-nir-dns-tests-google-com
 namespace: default
spec:
 domains:
 - gateway.nir-dns-tests-google-com
EOF

Step 3: Set Up External IP and Point a DNS A Record Towards this IP

To get the ManagedCertificate provisioned you must have a DNS A record pointing towards the HTTPS LB and have the certificate attached to the HTTPS LB (which will be handled on the next step)

gcloud compute addresses create gateway --project=nir-playground --global
RESERVED_IP=$(gcloud compute addresses describe gateway --global | grep address: | awk '{print $2}')

gcloud dns --project=nir-playground record-sets create gateway.nir-dns-tests-google-com. \
  --zone="nir-dns-tests-google-com" --type="A" --ttl="60" --rrdatas=$RESERVED_IP

Step 4: Deploy Gateway Resource

Notice that the gatewayClassNamerepresents a Global External HTTPS LB, and that the listeners array specify allowance of HTTPRoutes to be attached from any namespace. Also note that we’re setting the reserved IP with the addresses[0].typebeing NamedAddresseswith a value being the name we’ve used when reserving the static IP (“gateway” in this example):

CERT_NAME=$(kubectl get managedcertificate gateway-nir-dns-tests-google-com -o=jsonpath="{.status.certificateName}")

cat <<EOF | kubectl apply -f -
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1beta1
metadata:
 name: external-http
spec:
 gatewayClassName: gke-l7-global-external-managed
 listeners:
 - name: https
   protocol: HTTPS
   port: 443
   allowedRoutes:
     namespaces:
       from: All
   tls:
     mode: Terminate
     options:
       networking.gke.io/pre-shared-certs: $CERT_NAME
 addresses:
 - type: NamedAddress
   value: gateway
EOF

# Verify Gateway IP assignment
while true; do
 GATEWAY_IP=$(kubectl get gateway external-http -o=jsonpath="{.status.addresses[0].value}")
 if [ "$GATEWAY_IP" != "" ]; then
   break
 fi
 sleep 3
done

Step 5: Verify ManagedCertificate Status

# Verify certificate status
while true; do
 STATUS=$(kubectl get managedcertificate gateway-nir-dns-tests-google-com -o=jsonpath="{.status.certificateStatus}")
 if [ "$STATUS" = "Active" ]; then
   break
 fi
 echo "Certificate is in $STATUS status, will check again if it became active in 3 seconds"
 sleep 3
done

Step 6: Deploy Sample Applications

kubectl create namespace first-namespace
kubectl create namespace second-namespace

# Deploy App A in first-namespace
cat <<EOF | kubectl apply -n first-namespace -f -
apiVersion: apps/v1
kind: Deployment
metadata:
 name: app-a
spec:
 replicas: 2
 selector:
   matchLabels:
     app: app-a
 template:
   metadata:
     labels:
       app: app-a
   spec:
     containers:
     - name: app-a
       image: hashicorp/http-echo
       args: ["-text=Hello from App A"]
       ports:
       - containerPort: 5678
EOF

# Create Service for App A
cat <<EOF | kubectl apply -n first-namespace -f -
apiVersion: v1
kind: Service
metadata:
 name: service-a
spec:
 selector:
   app: app-a
 ports:
 - protocol: TCP
   port: 80
   targetPort: 5678
EOF

# Deploy App B in second-namespace
cat <<EOF | kubectl apply -n second-namespace -f -
apiVersion: apps/v1
kind: Deployment
metadata:
 name: app-b
spec:
 replicas: 2
 selector:
   matchLabels:
     app: app-b
 template:
   metadata:
     labels:
       app: app-b
   spec:
     containers:
     - name: app-b
       image: hashicorp/http-echo
       args: ["-text=Hello from App B"]
       ports:
       - containerPort: 5678
EOF

# Create Service for App B
cat <<EOF | kubectl apply -n second-namespace -f -
apiVersion: v1
kind: Service
metadata:
 name: service-b
spec:
 selector:
   app: app-b
 ports:
 - protocol: TCP
   port: 80
   targetPort: 5678
EOF

Step 7: Enable IAP and Create a k8s Secret for the IAP Credentials

echo -n CLIENT_SECRET_REDACTED > iap-secret.txt
kubectl create secret generic iap --from-file=key=iap-secret.txt

Step 8: Create GCPBackendPolicy that Enforces IAP

# Enable IAP for App A
cat <<EOF | kubectl apply -n first-namespace -f -
apiVersion: networking.gke.io/v1
kind: GCPBackendPolicy
metadata:
 name: backend-policy
spec:
 default:
   iap:
     enabled: true
     oauth2ClientSecret:
       name: iap
     clientID: REPLACE_WITH_YOUR_IAP_OAUTH_CLIENT_ID
 targetRef:
   group: ""
   kind: Service
   name: service-a
EOF

# Enable IAP for App B
cat <<EOF | kubectl apply -n second-namespace -f -
apiVersion: networking.gke.io/v1
kind: GCPBackendPolicy
metadata:
 name: backend-policy
spec:
 default:
   iap:
     enabled: true
     oauth2ClientSecret:
       name: iap
     clientID: REPLACE_WITH_YOUR_IAP_OAUTH_CLIENT_ID
 targetRef:
   group: ""
   kind: Service
   name: service-b
EOF

Step 9: Create HTTPRoute

# Create route for App A
cat <<EOF | kubectl apply -f -
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
 name: route-a
 namespace: first-namespace
spec:
 parentRefs:
 - name: external-http
   kind: Gateway
   namespace: default
 rules:
 - matches:
   - path:
       value: /first
   backendRefs:
   - name: service-a
     port: 80
EOF

# Create route for App B
cat <<EOF | kubectl apply -f -
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
 name: route-b
 namespace: second-namespace
spec:
 parentRefs:
 - name: external-http
   kind: Gateway
   namespace: default
 rules:
 - matches:
   - path:
       value: /second
   backendRefs:
   - name: service-b
     port: 80
EOF

Step 10: Browse the IAP Protected Apps

Navigate to the IAP protected URL. You will be prompted with the Google Sign In page. Any user who belongs to the same Google Cloud organization the application is deployed to, and is assigned with the IAP- Secured Web App User IAM permissions, will be able to access the application:

[email protected] is a user on the doit.com GCP organization , and is assigned with the required IAM permissions to access the application. [email protected] isn’t from the mentioned organization so they won’t be able to access the application

When a user outside of the organization or a user which wasn’t assigned permissions tries to sign in and access the application, they will be presented with a permissions-denied page:

Permissions denied page displayed for non authorized principals

Modernizing your GKE access patterns from VPN to IAP-enabled External Gateway represents just one of many ways to enhance your Google Cloud infrastructure’s security and operational efficiency. This approach not only improves your security posture, but also reduces operational overhead and costs.

While this guide focuses on GKE access modernization, similar transformation opportunities exist across your cloud infrastructure. From cost optimization to infrastructure automation, and from security hardening to cloud architecture design, DoiT International offers extensive expertise across multiple cloud domains. To explore how DoiT can help modernize other aspects of your cloud infrastructure or to learn about our other cloud engineering solutions, visit doit.com/expertise/.