Securing Your Internal Tools: Implementing Identity-Aware Proxy (IAP) for GKE Resources with CDKTF

Hello, Today I want to share something that’s become increasingly critical in our cloud-native world — securing internal tools and dashboards without the complexity of traditional VPN setups.

Picture this: Your company has grown from a small startup to a mid-sized organization. You have internal dashboards, monitoring tools, admin panels, and various services running on Google Kubernetes Engine (GKE). Initially, maybe you secured these with basic auth or just left them on internal networks. But as your team grows and remote work becomes more common, you realize you need something more robust, more scalable, and frankly, more professional.

That’s where Google’s Identity-Aware Proxy (IAP) comes in, and today I’ll walk you through implementing it using Infrastructure as Code with CDKTF.

What is IAP and Why Should You Care?

Identity-Aware Proxy (IAP) is Google Cloud’s solution to the age-old problem of “how do I securely control access to my applications?” Think of IAP as a sophisticated bouncer at an exclusive club — it checks not just if you have a ticket (authentication), but also if you’re on the guest list for that specific event (authorization).

Here’s the beautiful part: IAP sits between your users and your applications, handling all the authentication and authorization logic without you having to modify your applications. It integrates seamlessly with Google’s identity systems, supports your corporate Google Workspace accounts, and can enforce granular access controls based on user attributes, device security status, and more.

Why IAP is a Game-Changer

  1. Zero Trust Security Model: IAP doesn’t trust anyone by default, not even users inside your corporate network
  2. No VPN Complexity: Users can access internal tools from anywhere with just their corporate Google account
  3. Granular Access Control: You can control who accesses what, when, and from which devices
  4. Audit Trail: Every access attempt is logged, giving you complete visibility
  5. Integration with Google Workspace: Leverage your existing Google accounts and groups

Understanding CDKTF: Infrastructure as Code for the Modern Age

Before we dive into the implementation, let’s talk about CDKTF — Cloud Development Kit for Terraform. If you’ve worked with traditional Terraform, you know it uses HCL (HashiCorp Configuration Language). While HCL is powerful, it can feel limiting when you need complex logic, loops, or want to leverage your existing programming skills.

CDKTF bridges this gap by allowing you to define your infrastructure using familiar programming languages like TypeScript, Python, Java, C#, or Go. For this article, we’ll use TypeScript because of its excellent type safety and IntelliSense support.

Why CDKTF with TypeScript?

  • Type Safety: Catch configuration errors at compile time, not deployment time
  • Code Reusability: Create functions, classes, and modules to reduce duplication
  • IDE Support: Full IntelliSense, autocomplete, and refactoring capabilities
  • Familiar Syntax: If you know TypeScript/JavaScript, you’re already halfway there
  • Testing: Unit test your infrastructure code just like application code

Think of it this way: traditional Terraform is like writing configuration files, while CDKTF is like writing a program that generates those configuration files. The end result is the same, but the development experience is significantly better.

Backend Services vs. Backend Configs: The Kubernetes Ingress Story

Before we implement IAP, it’s crucial to understand the difference between Backend Services and Backend Configs in the GKE context — this tripped me up when I first started working with GKE ingress.

Backend Services

A Backend Service is a Google Cloud resource that defines how traffic should be distributed to your backend instances (in our case, Kubernetes pods). It’s part of Google’s load balancing infrastructure and handles:

  • Health checks
  • Load balancing algorithms
  • Session affinity
  • Traffic distribution

When you create a Kubernetes Service and expose it through an Ingress, GKE automatically creates a corresponding Backend Service in Google Cloud.

Backend Configs

A Backend Config is a Kubernetes Custom Resource Definition (CRD) that allows you to customize the behavior of the automatically created Backend Services. Think of it as a way to tell GKE: “When you create the Backend Service for my Kubernetes Service, please apply these additional configurations.”

Backend Configs can control:

  • Connection draining timeouts
  • Session affinity settings
  • Custom request/response headers
  • Security policies
  • And most importantly for us — IAP settings

The key insight here is that Backend Configs are Kubernetes resources that influence Google Cloud Backend Services. It’s GKE’s way of bridging Kubernetes-native configuration with Google Cloud’s load balancing features.

Hands-On: Implementing IAP for a SonarQube Instance

Now let’s get our hands dirty. We’ll implement IAP for a SonarQube instance — a popular code quality and security analysis tool that’s perfect for demonstrating internal tool security.

Prerequisites

  • A GKE cluster
  • CDKTF installed and configured
  • Google Cloud project with appropriate permissions
  • Basic understanding of Kubernetes

Step 1: Project Setup and Service Enablement

First, we need to enable the IAP API. This is crucial because it also creates an IAP service agent that we’ll need later:

import { enableServices } from "@examplecompany/iac";

// Enable Google IAP Service
enableServices(this, project.name, ["iap.googleapis.com"], false);

What’s happening here? The enableServices function is a helper that enables Google Cloud APIs in your project. When you enable the IAP API (iap.googleapis.com), Google automatically creates a special service account called the “IAP service agent.” This service account is what IAP uses behind the scenes to communicate with your applications. Think of it as giving IAP the keys to act on behalf of your project.

Step 2: Create OAuth Credentials

Before we can use IAP, we need OAuth 2.0 credentials. Head to the Google Cloud Console:

  1. Navigate to APIs & Services > Credentials
  2. Click Create Credentials > OAuth 2.0 Client IDs
  3. Choose Web application
  4. Add your domain to Authorized redirect URIs
  5. Note down the Client ID and Client Secret

We’ll store these as environment variables for security:

export IAP_CLIENT_ID="your-client-id-here"
export IAP_CLIENT_SECRET="your-client-secret-here"

Step 3: Create the Helper Function

Let’s create a reusable function for IAP implementation. This is where the magic happens:

import { Manifest } from "@cdktf/provider-kubernetes/lib/manifest";
import { Construct } from "constructs";

export interface IapConfigProps {
  namespace: string;
  backendConfigName: string;
  oauthSecretName: string;
  clientId: string;
  clientSecret: string;
}

export function createIapResources(scope: Construct, props: IapConfigProps) {
  // Validate credentials are provided
  if (!props.clientId || !props.clientSecret) {
    throw new Error("IAP_CLIENT_ID and IAP_CLIENT_SECRET must be set");
  }

  // Create Kubernetes Secret for OAuth credentials
  new Manifest(scope, "iap-oauth-secret", {
    manifest: {
      apiVersion: "v1",
      kind: "Secret",
      metadata: {
        name: props.oauthSecretName,
        namespace: props.namespace,
      },
      type: "Opaque",
      data: {
        client_id: Buffer.from(props.clientId).toString("base64"),
        client_secret: Buffer.from(props.clientSecret).toString("base64"),
      },
    },
  });

  // Create BackendConfig with IAP enabled
  new Manifest(scope, "iap-backendconfig", {
    manifest: {
      apiVersion: "cloud.google.com/v1",
      kind: "BackendConfig",
      metadata: {
        name: props.backendConfigName,
        namespace: props.namespace,
      },
      spec: {
        iap: {
          enabled: true,
          oauthclientCredentials: {
            secretName: props.oauthSecretName,
          },
        },
        timeoutSec: 60,
        connectionDraining: {
          drainingTimeoutSec: 120,
        },
      },
    },
  });
}

Let me break down what this function is doing step by step:

The Interface: The IapConfigProps interface is like a contract that ensures anyone using this function provides all the required information. It’s TypeScript’s way of saying “these are the mandatory parameters.”

Credential Validation: The first thing we do is check if OAuth credentials are provided. This prevents silent failures where you deploy everything successfully but IAP doesn’t work because credentials are missing.

Creating the Secret: The first Manifest creates a Kubernetes Secret to store our OAuth credentials. Notice how we use Buffer.from().toString("base64") — this is because Kubernetes secrets must be base64 encoded. The secret type is “Opaque,” which is Kubernetes’ way of saying “this is arbitrary user-defined data.”

Creating the BackendConfig: The second Manifest is where the real IAP magic happens. We’re telling GKE: “When you create the Google Cloud Backend Service for any service that references this BackendConfig, please enable IAP and use the OAuth credentials from this secret.” The timeoutSec and connectionDraining are additional configurations to ensure graceful handling of requests during deployments.

Step 4: Create the Main Stack

Now let’s put it all together in our main infrastructure stack:

import { DataGoogleContainerCluster } from "@cdktf/provider-google/lib/data-google-container-cluster";
import { IapWebIamBinding } from "@cdktf/provider-google/lib/iap-web-iam-binding";
import { Namespace } from "@cdktf/provider-kubernetes/lib/namespace";

class SonarQubeStack extends TerraformStack {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    const project = { name: "my-example-project" };

    // Enable IAP service
    enableServices(this, project.name, ["iap.googleapis.com"], false);

    // Reference existing GKE cluster
    const gke = new DataGoogleContainerCluster(this, "gke-cluster", {
      project: project.name,
      location: "us-central1",
      name: "my-cluster",
    });

    // Create namespace
    const namespace = new Namespace(this, "sonarqube-namespace", {
      metadata: {
        name: "sonarqube",
      },
    });

    // Create IAP resources
    createIapResources(this, {
      namespace: namespace.id,
      backendConfigName: "sonarqube-backendconfig",
      oauthSecretName: "sonarqube-iap-oauth-secret",
      clientId: process.env.IAP_CLIENT_ID ?? "",
      clientSecret: process.env.IAP_CLIENT_SECRET ?? "",
    });

    // Grant access to specific users/groups
    new IapWebIamBinding(this, "sonarqube-iap-access", {
      project: project.name,
      role: "roles/iap.httpsResourceAccessor",
      members: [
        "group:developers@examplecompany.com",
        "group:devops@examplecompany.com",
        "user:admin@examplecompany.com",
      ],
    });
  }
}

Here’s what’s happening in our main stack:

Data Source Reference: DataGoogleContainerCluster is not creating a new cluster — it’s referencing an existing one. This is CDKTF’s way of saying “I need information about this resource that already exists.” It’s like looking up a contact in your phone book rather than adding a new one.

Namespace Creation: We create a Kubernetes namespace to isolate our SonarQube resources. Think of namespaces like folders in your filesystem — they help organize and separate different applications.

Calling Our Helper: We call our createIapResources function with specific values. Notice how we use process.env.IAP_CLIENT_ID ?? "" — the ?? operator provides an empty string fallback if the environment variable isn’t set. This prevents crashes but will be caught by our validation later.

IAM Binding – The Access Control: This is crucial! IapWebIamBinding is what actually grants people access. The role roles/iap.httpsResourceAccessor is Google’s predefined role that allows access through IAP. Without this binding, even authenticated users would be denied access. The members array supports both individual users (user:someone@company.com) and Google Groups (group:teamname@company.com).

Step 5: Configure Your Service

The final piece is connecting your Kubernetes Service to the BackendConfig. In your service manifest (or Helm values), add this annotation:

apiVersion: v1
kind: Service
metadata:
  name: sonarqube
  namespace: sonarqube
  annotations:
    cloud.google.com/backend-config: '{"default": "sonarqube-backendconfig"}'
spec:
  # ... rest of your service configuration

If you’re using Helm (like we are with SonarQube), update your values.yaml:

service:
  type: ClusterIP
  externalPort: 9000
  internalPort: 9000
  annotations: 
    cloud.google.com/backend-config: '{"default": "sonarqube-backendconfig"}'
    cloud.google.com/load-balancer-type: "External"

This annotation is the bridge between your Kubernetes Service and the BackendConfig. Here’s what’s happening:

The Magic Annotation: cloud.google.com/backend-config tells GKE’s ingress controller: “When you create a Google Cloud Backend Service for this Kubernetes Service, apply the configuration from this BackendConfig.” The {"default": "sonarqube-backendconfig"} part means “apply this BackendConfig to the default port” — if your service had multiple ports, you could specify different BackendConfigs for each.

Load Balancer Type: The cloud.google.com/load-balancer-type: "External" annotation ensures your service gets an external IP address that can be reached from the internet (after passing through IAP, of course).

The Moment of Truth: Testing Your Implementation

Deploy your infrastructure:

cdktf deploy

After deployment, navigate to your service URL. Instead of direct access, you should see the Google sign-in page. After authentication with your corporate account, IAP will check if you have the necessary permissions and either grant or deny access.

What Could Go Wrong (And How to Fix It)

From my experience implementing IAP across multiple services, here are the common gotchas:

1. OAuth Configuration Issues

Problem: Users see “Error: redirect_uri_mismatch”
Solution: Ensure your OAuth client’s authorized redirect URIs include your actual domain

2. IAM Permission Problems

Problem: Users authenticate but get “You don’t have access”
Solution: Check your IapWebIamBinding members list and verify users are in the specified groups

3. BackendConfig Not Applied

Problem: IAP doesn’t seem to work at all
Solution: Verify the service annotation is correct and the BackendConfig exists in the same namespace

4. SSL Certificate Issues

Problem: IAP works but with SSL warnings
Solution: Ensure you have proper SSL certificates configured for your domain

Best Practices I’ve Learned the Hard Way

  1. Use Google Groups: Instead of individual users, manage access through Google Groups. It’s much easier to maintain.

  2. Environment Separation: Use different OAuth clients for different environments (dev, staging, prod) for better security isolation.

  3. Monitor Everything: Enable IAP access logging and set up alerts for failed authentication attempts.

  4. Test Thoroughly: Always test with users who shouldn’t have access to ensure your permissions are working correctly.

  5. Document Your Groups: Keep clear documentation of which Google Groups have access to which services.

The Payoff

After implementing IAP across our internal tools, the benefits were immediately apparent:

  • Developer Productivity: No more VPN hassles or remembering different passwords
  • Security Compliance: Clear audit trails and granular access control
  • Operational Simplicity: Centralized identity management through Google Workspace
  • Scalability: Easy to add new tools and services under the same security model

The initial setup might seem complex, but once you have the pattern established, securing additional services becomes a matter of copying and adapting your existing code.

Wrapping Up

Identity-Aware Proxy represents a shift from traditional perimeter-based security to a zero-trust model. Combined with Infrastructure as Code practices using CDKTF, you get both security and maintainability.

The implementation we’ve covered here is just the beginning. IAP supports advanced features like device-based access controls, context-aware access based on user location and device security posture, and integration with third-party identity providers.

My advice? Start simple with basic IAP implementation, get comfortable with the concepts and workflows, then gradually add more sophisticated policies as your security requirements evolve.

Remember, security isn’t just about keeping the bad guys out — it’s about making it easy for the good guys to get their work done safely and efficiently.

What internal tools are you planning to secure with IAP? I’d love to hear about your implementation experiences in the comments!

*If you found this helpful, follow me on Linkedin.*for more DevOps and cloud security content.

Similar Posts