Icon close

Using Terraform to create secure IAP tunnels on GCP with conditional IAM policies

Author

Date

Sumary

The why

The problem

The solution

Authentication and Authorisation

In the example above, the client requests an ssh connection to the webserver using Google’s gcloudcommand-line tool.

By using the --tunnel-through-iap flag, we can tell gcloud to tunnel the ssh connection through the IAP servers.

 

Example command:

gcloud compute ssh <instance-name> \ --tunnel-through-iap --project=<project-name> \ --zone=<zone>

What this command will do, is to first attempt to authenticate the user to GCP. Then GCP will check the IAM policy to see if it user has the required permission (roles/iap.tunnelResourceAccessor) to establish an IAP tunnel to the IAP servers.
(The IAP servers sit in this IP address range 35.235.240.0/20)

In this case, we will be using a conditional IAM role whereby the incoming connection will be checked against an Access List (Access Context Manager) to ensure that the IP address range the client device is on is within the range specified and if this evaluates to true the user will be granted the required role: roles/iap.tunnelResourceAccessor

Once the user has been authenticated and had their IAM policy checked to ensure the user is authorised to access the service, the next step is to make a connection from the IAP servers to the VM.

This will require a firewall rule in the VPC allowing incoming connections to the VM from the IAP servers subnet. 35.235.240.0/20

With all this in place, we will be able to securely establish a remote access connection!

Terraform code

Now we know what we are trying to achieve and how we can get stuck into implementing this in Terraform.

Assumptions

Getting started

Create a Linux VM in the project and enable OS Login

# Create an instance
resource "google_compute_instance" "my-instance" {
    project         = var.project_id
    name            = "my-instance-01"
    machine_type    = "e2-standard-2"
    zone            = var.zone

    boot_disk {
        initialize_params {
            image = "debian-cloud/debian-9"
        }
    }
    network_interface {
        network = "default"
  }
    service_account {
      email = "<project-id>-compute@developer.gserviceaccount.com"
      scopes = ["cloud-platform"]
    }
    metadata = {
        enable-os-login = "true"
  }
}

 

Create the Firewall rule to allow incoming ssh connections from Google IAP servers to our instance

## Allow incoming access to our instance via
## port 22, from the IAP servers
resource "google_compute_firewall" "inbound-ip-ssh" {
    name        = "allow-incoming-ssh-from-iap"
    project     = var.project_id
    network     = "default"

    direction = "INGRESS"
    allow {
        protocol = "tcp"
        ports    = ["22"]  
    }
    source_ranges = [
        "35.235.240.0/20"
    ]
    target_service_accounts = ["<project-id>-compute@developer.gserviceaccount.com"]

 

Create the Access List on the default Access Policy

Note: In order to get the ID for the Access Policy, try the following commands:

  • Get your organization ID
    gcloud organizations list
  • Find your Access Policy ID
    gcloud access-context-manager policies list --organization <org-id>

The access-policy-id or name will be the number under the NAME column
e.g.:

NAME ORGANIZATION TITLE965510000000 465000000000

 

Create the Access List

# Creates an Access Level
# This access level will be used in
# a conditional IAM policy to restrict access
# to authorised users coming from authorised networks

resource “google_access_context_manager_access_level” “access-level” {
parent = “accessPolicies/<access-policy-id>”
name = “accessPolicies/<access-policy-id>/accessLevels/<my_access_level_name>”
title = “secure-iap-access-level”
description = “This access level lists the authorised network addresses”
basic {
conditions {
ip_subnetworks = [
“10.0.0.1/32”,
]}
}
}

 

Create the conditional IAM policy assigning the role

roles/iap.tunnelResourceAccessorto a user if the Access List evaluates to true (Eg. the user is making the request from an IP in the defined ip_subnetworks above)

# Create a conditional IAM rule that grants access to establish an IAP tunnel
# IF the user is connecting from an authorised network defined in the access 
# list
resource "google_iap_tunnel_iam_member" "allow-remote-access-to-iap" {
    project = "<your-project-id>"
    role    = "roles/iap.tunnelResourceAccessor"
    member  = "user:calum.hunter@the.cloud"

    condition {
      title = "allow_remote_access_to_iap"
      description = "Allow access to IAP tunnel for authorized users"
      expression = "\"accessPolicies/<access-policy-id>/accessLevels/<my-access-level-name>\" in request.auth.access_levels"
    }
}

 

Ok, not quite.

There are a couple of extra settings we need to make to ensure we can connect to the VM:

 

Assign compute roles to the user to allow them access to the VM

# Define the required roles to access the VM
locals {
    compute_roles = [ 
        "roles/compute.viewer",
        "roles/compute.osLogin",
    ]
}

 
# Apply the roles to a user account
resource "google_project_iam_member" "assign-roles" {
    count   = length(local.compute_roles)
    project = var.project_id
    role    = local.roles[count.index]
    member  = "user:calum.hunter@the.cloud"
}
 

 

Connecting to the instance with SSH via the IAP secure tunnel

gcloud compute ssh my-instance-01 
   --tunnel-through-iap 
   --project=<my-project> 
   --zone=<my-zone>
 

Bonus — Connecting via VNC and RDP

While this post focused on access to a VM using SSH. This solution can easily be extended to use any TCP port. (See: tunneling_other_tcp_connections)

So obtaining remote access via VNC or RDP to a VM is as simple as adding the required port/protocol to the firewall rule that allows connections to the VM from the IAP servers.

Accessing the VM using VNC or RDP, however, is a little bit different.
Instead, we will use the

gcloudcommand to create a tunnel bound to a port on the

localhost.

Then we can point our VNC or RDP client to this localhost address and have it tunnel through to our VM.

 

Update the firewall rule to enable VNC (port 5901) and RDP (port 3389)

## Allow incoming access to our instance via
## port 22, from the IAP servers
resource "google_compute_firewall" "inbound-ip-ssh" {
    name        = "allow-incoming-access-from-iap"
    project     = var.project_id
    network     = "default"

    direction = "INGRESS"
    allow {
        protocol = "tcp"
        ports    = ["22", "3389", "5901"]  
    }
    source_ranges = [
        "35.235.240.0/20"
    ]
    target_service_accounts = ["<project-id>-compute@developer.gserviceaccount.com"]
}

 

Create a tunnel using VNC

gcloud compute start-iap-tunnel <my-instance-name> 5901 
  --local-host-port=localhost:5901

Connect to your VM using your VNC client
Now that we have established a tunnel to the instance using VNC, we can use our VNC viewer application to connect to

localhost:5901

and use the tunnel to make the connection to our VM

Connected to our VM in GCP

The same process as above works for Microsoft RDP as well, simply change the port in the gcloud command from 5901 to 3389 then use your remote desktop application to connect to localhost:3389 and the connection will go through as well.

 

Other possibilities

From here you can also see that it is trivial to open a tunnel over any TCP port to our VM in GCP. (Don’t forget to update the list of ports to allow in from our IAP servers in the firewall rule)

For example, we might have a web server running on our VM in GCP that we wish to access, but without having a public IP address or load balancer setup, we can simply create an IAP tunnel to port 80 (or whatever port our web server is listening on) and then establish that on our local host. From here we could access it in a web browser by going to http://localhost:80

 

Example:

Start a simple web server on our VM in GCP


python -m SimpleHTTPServer

Update our firewall to allow port 8000 from IAP servers to our VM

resource "google_compute_firewall" "inbound-ip-ssh" {
  name        = "allow-incoming-ssh-from-iap"
  project     = var.project_id
  network     = "default"
  direction = "INGRESS"
  
  allow {
    protocol = "tcp"
    ports    = ["22", "5901", "3389", "8000"]
  }
  
  source_ranges = ["35.235.240.0/20"]  target_service_accounts = [
    "<project-id> compute@developer.gserviceaccount.com"
  ]
}

 

Now lets start a tunnel a try connecting to it via the tunnel

gcloud compute start-iap-tunnel <my-instance-name> 8000
  --local-host-port=localhost:8000

Access it in a browser

Thanks to Jake Nelson

Stay up to date in the community!

We love talking with the community. Subscribe to our community emails to hear about the latest brown bag webinars, events we are hosting, guides and explainers.

Share