Terraform on GCP - DFIR Lab Hello World!

    

By: Jason Alvarez,
Follow me on twitter @0xBanana


Deploying a digital forensics lab in the cloud using terraform! Click it & ship it (part 3)!

This is part 3 in an on going series on deploying a simple forensics lab in Google Cloud Platform. In this post we learn about using Terraform to deploy the same network we established in part 1 with the startup scripts we configured in part 2. (Be sure to check those out to get up to speed.)

What is terraform

You may have heard people talk about Terraform before and you may even have heard the term Infrastructure-as-Code (IAC), but what is terraform and what does it

From the HashiCorp (vendor) docs

Terraform is a tool for building, changing, and versioning infrastructure safely and efficiently. Terraform can manage existing and popular service providers as well as custom in-house solutions.

Configuration files describe to Terraform the components needed to run a single application or your entire datacenter.

Okay, but what does that mean?

It means, terraform can create, modify, delete ANY resource outlined in the configuration file. The configuration file is the code that outlines all the infrastructure: network, compute, server less functions, and other vendor supplied resources.

You can deploy and destroy your entire infrastructure with two commands and a config file, interested? Read on!

Setup terraform service account

For terraform to successfully deploy infrastructure on our behalf we need to provide some credentials with the appropriate access permissions. All cloud providers give you the ability to create service accounts aka non-human accounts to access cloud resources.

A service account is a special type of Google account intended to represent a non-human user that needs to authenticate and be authorized to access data in Google APIs.

We’re going to use that service account to enable terraform to perform actions on our behalf, let’s walk through the process of creating a service account (in Google Cloud Platform) with the appropriate roles, and downloading a key for Terraform.

Step 1. Create the service account ☰ IAM & Admin > Service Accounts From the Service accounts window click “+ CREATE SERVICE ACCOUNT” at the top.

In the window that pops up, give our account a name, a project unique ID and a description.

Next we’re going to define the roles (permissions) the account has access to.

For this account we’re going to need to create/modify/delete instances as well as networking and a storage bucket.

Whenever we create a new account or assign roles we want to always consider giving the account as few permissions as possible. For this project and the need to create a new VPC resource, we need to provide the account “Project Admin” rights.

Continue into the 3rd page where we do not need to change any settings, and finish.

The service account is now created and we then have to download a key file for Terraform to use to authenticate. On the main IAM Dashboard page (where you should be redirected to after SA creation) click the overflow menu of the account and select Create Key ⠇ > Create Key

For our purposes we want a JSON key, so choose that option and save it to your project folder.

Great, now you have a service account capable of creating the infrastructure you design and a JSON key file to authenticate to GCP with.

Next, let’s install Terraform.


Install terraform

I use OS X and also homebrew - if you have this setup, the fastest installation method is to use brew.

brew install terraform

Otherwise please refer to the installation page for your specific operating system instructions here - Download Terraform - Terraform by HashiCorp

Verify it is installed correctly by running it at the command line.

➜  ~ terraform
Usage: terraform [-version] [-help] <command> [args]

The available commands for execution are listed below.
The most common, useful commands are shown first, followed by
less common or more advanced commands. If you're just getting
started with Terraform, stick with the common commands. For the
other commands, please read the help and docs before usage.

[...SNIP...]

Create file & folder structure

Before we can begin with terraform we have to have a small file structure in place. Create the structure you see below.

➜  ~ tree .
.
├── main.tf
├── service_account.json
└── variables.tfvars

Deploying with Terraform

There are three steps to deploying your infrastructure as code with Terraform.

terraform init - Initialize the directory with Terraform terraform plan - View the proposed changes to the current state of the environment terraform apply - Apply the changes to the environment

INIT

Use terraform init to (re)initialize Terraform with details from main.tf.

If in the course of modifying your configuration the modules or backend configuration for Terraform changes, you need to rerun terraform init to be aware of these changes.

➜  ~ terraform init

Initializing the backend...

Initializing provider plugins...
- Finding latest version of hashicorp/google...
- Installing hashicorp/google v3.36.0...
- Installed hashicorp/google v3.36.0 (signed by HashiCorp)

[... SNIP ...]

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

PLAN

The planning state of deployment is where we create the contents of our main.tf file if we haven’t already, and verify that we’ve set everything up right.

In our setup we have two files that control everything, main.tf and variables.tfvars you’ll see where variables are defined and used in main.tf but not given a value, thats where variables.tfvars comes into play.

main.tf

As the name implies we’re going to be looking at blocks of code that define cloud resources and we’re going to have various key/value pairs to shape the resource to our needs.

What does a resource block look like ?


resource "<type of resource>" "<unique identifier>" {
  Key0         = "test"
  Key1         = "us-west1"

}

(be aware of the quotes, they’re important!) If this looks a little cryptic don’t worry, It’ll become clearer as we go.

resource is a reserved keyword that lets terraform know a resource is being declared.

”type of resource” is cloud vendor specific for the kind of resource you are trying to deploy.

”unique identifier is a Terraform plan specific identifier, like a variable name in programming they need to be unique.

{...} resource specific key value pairs and sub blocks that allow you to customize and configure the resource.

Reviewing the network map we created in part 2 of this series

Our setup will have the following resources 1. 1 VPC Network 2. 3 compute instances 3. 2 firewall rules 4. 1 cloud storage bucket

The most encompassing piece of our network is the VPC so let’s create that first. Below is the resource block that defines it.

resource "google_compute_network" "vpc_network" {
  name                    = "bananaco-blog-dev-vpc-1"
  auto_create_subnetworks = "true”
}

In “plain English”:

This block is going to create a “google_compute_network” (VPC) named in GCP as “bananaco-blog-dev-vpc-1” and it will automatically create subnets for me in each region, globally. The name may be a bit long for a temporary network but I chose to follow Google’s suggestions for VPC design best practices.

That wasn’t too bad, how about something more complicated.

resource "google_compute_instance" "compute_kali" {
  name         = "kali-linux"
  machine_type = var.kali_machine_type
  zone         = var.zone_name
  tags         = ["admin"]
  
  # Configure the system's boot disk and OS image
  boot_disk {
    initialize_params {
      image = "debian-cloud/debian-9"
      size = 10
    }
  }
	
	# Setup our network connections 
  network_interface {
    network = google_compute_network.vpc_network.self_link
    
    # This is needed for public IP access
    access_config {
    }
  }

  # Set the preemptible flag in the scheduler (and disable auto restart)
  scheduling {
    preemptible = true
    automatic_restart = false
  }

  # configure the instance startup script
   metadata_startup_script = var.kali_startup_script
}
# End Compute Kali

This is substantially longer but if we look at it in pieces it’s easy to understand, from the top!

resource "google_compute_instance" "compute_kali" {
  name         = "kali-linux"
  machine_type = var.kali_machine_type
  zone         = var.zone_name
  tags         = ["admin"]

  ...

We’re creating a “google_compute_instance” which will have the terraform unique identifier ”compute_kali" and the GCP name ”Kali-linux”. It has a machine type of var.kali_machine_type tags [“admin”] and is located in the var.zone_name zone.

Wait, what? What about those var things?

Terraform allows us to set values as variables which may make it a bit hard to read at first, but gives us added power and flexibility when it comes to consistency across resources and deploys.

Variable values can be found in variables.tfvars and the structure of the file is very simple.

# Project Variables
project_name = "lab"
region_name = "us-east1"
zone_name = "us-east1-c"
...
kali_machine_type = "n1-standard-2"

JUST KEY VALUE PAIRS!

Use good variable names and once your infrastructure is setup to your needs you may never have to touch main.tf again. Neat!

Going back to the compute_kali instance we now can see it’s going to be located in the us-east1-c zone and use a n1-standard-2 machine. Just like we used in the previous post.

Whats next?

  # Configure the system's boot disk and OS image
  boot_disk {
    initialize_params {
      image = "debian-cloud/debian-9"
      size = 10
    }
  }

Here we configure the boot disk the system will use; the operating system image and the size of the disk. Any image family supplied by google can be used inplace of debian-cloud/debian-9 or any private image your account has access to.

What about networking?

  network_interface {
    network = google_compute_network.vpc_network.self_link
    
    # This is needed for public IP access
    access_config {
    }
  }

In this block we declare what network settings we should. The network key tells Terraform what VPC it should attach this to, we could use the name of the terraform resource, but in this instance we’re using the self_link.

The access_config sub block is needed for the instance to have a public facing IP address, if you don’t want that, feel free to remove the sub block!

Okay, what else?

# Set the preemptible flag in the scheduler (and disable auto restart)
  scheduling {
    preemptible = true
    automatic_restart = false
  }

For machines that I would consider disposable I try to use preemptible instances wherever I can. They have some drawbacks but the cost savings for non-production things can’t be beat. If you’re going to set the preemptible flag, it’s required you also set automatic_restart.

There’s more?

 # configure the instance startup script
   metadata_startup_script = var.kali_startup_script
}
# End Compute Kali

This last key pair sets the startup script to run on system boot. For readability it is also a variable defined in our variables.tfvars file. The resource block is now closed with the final } and this block of code can be repurposed for our other two compute instances.

The next big important piece of our design is network firewall rules to allow traffic to the systems on the ports we’ve specified. They are as you guessed, their own type of resource, let’s jump into it.

resource "google_compute_firewall" "allow_admin" {
  name    = "lab1-ingress-allow-ssh-admin"
  network = google_compute_network.vpc_network.self_link

  allow {
    protocol = "icmp"
  }

  allow {
    protocol = "tcp"
    ports = ["22", "64295", "64297"]
  }

  target_tags = ["admin"]
  source_ranges = ["0.0.0.0/0"]
}

Reading this over, maybe you’re beginning to see similarities in block definitions and key value pairs. In “plain English”:

The block above creates a google_compute_firewall resource with the terraform identifier allow_admin and GCP name "lab1-ingress-allow-ssh-admin" attached to the vpc_network VPC (the one we defined above). It allows ICMP connections as well as TCP connections on ports 22, 64295, and 64297 from any IP address to any compute instances tagged "admin"

Using this model we can create any manner of firewall rule that fits our needs. There are other options that can be set and ways to configure these resources, so be sure to check out the documentation when you’re unsure or dealing with unfamiliar resources.

There are two more blocks you’ll need that are important to know: variable declarations and (cloud) provider.

You can’t use a variable before its declared and they should be declared at the top of your main.tf file like the following

variable "project_name" {
  type = string
}
variable "zone_name" {
  type = string
}

Any variable that gets used needs to be declared else Terraform will throw an error, and if it does thats okay because declaring a variable is an easy thing to do!

Last but (definitely) not least

provider "google" {
  credentials = file("service_account.json")
  project = var.project_name
  region  = var.region_name
}

This small block tells Terraform what cloud provider to use, the credentials, and the project (as well as the region). The credentials in this case is the JSON file we downloaded from our GCP Service Account.

variables.tfvars

This file is where the variables you declared in main.tf get defined. The file format is a simple list of key = value pairs.

project_name = "honeypot-village"
region_name = "us-east1"
zone_name = "us-east1-c"
vpc_name = "banana-lab-dev-vpc-1"
...

There is one thing I want to highlight with regards to the variables file, the startup script.

In the last post the startup script was a long form bash file, in this case, to fit in the constraints of a double quoted string variable, we are going to covert the long form to a “simple 1 liner”. To do that, we’re going to leverage && (logical AND) to run multiple commands in sequence as one long string

Using the shortest of the three scripts (remnux) we replace every newline with && and make some minor changes to our if statement.

From this

if [[ ! -z /FINISHED.FLAG ]] then
  wget https://REMnux.org/remnux-cli mv remnux-cli remnux
  chmod +x remnux
  mv remnux /usr/local/bin
  remnux inatall --mode=cloud
  touch /FINISHED.FLAG
fi

To this

[ ! -z /FINISHED.FLAG ] && wget https://REMnux.org/remnux-cli && mv remnux-cli remnux && chmod +x remnux && mv remnux /usr/local/bin && remnux inatall --mode=cloud && touch /FINISHED.FLAG

There are other interesting ways to inline more complicated scripts that I’ll cover in a future post.

Now we’ve got our infrastructure coded and all our variables declared and defined, we can test our plan before deploying it to the cloud.

Verification of infrastructure in main.tf

Before deploying we want to verify our config and Terraform has a plan option for just that

➜ ~ terraform plan -var-file variables.tfvars

This command is a convenient way to check whether the execution plan for a set of changes matches your expectations without making any changes to real resources or to the state.

Another option I like to use is terraform-visual

Terraform Visual is a simple but powerful tool to help you understand your Terraform plan easily.

This post won’t go over installation or use of terraform-visual, but the link should help you out and its pretty straight forward. The output of terraform-visual against our config is below and it looks pretty good!

Apply aka - Deploy and enjoy

When we are happy with our configuration and variables all that is left to do is deploy, which is accomplished by the service account we created earlier and executed by running the following command and then typing “yes” to approve the changes when prompted.

➜  ~ terraform apply  -var-file variables.tfvars

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

[... SNIP ...]

Plan: 7 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value:

Accept

google_compute_instance.compute_remnux: Creating...
google_compute_instance.compute_tpot: Creating...
google_compute_instance.compute_kali: Creating...
google_compute_instance.compute_kali: Still creating... [10s elapsed]
google_compute_instance.compute_tpot: Still creating... [10s elapsed]
google_compute_instance.compute_remnux: Still creating... [10s elapsed]
google_compute_instance.compute_kali: Creation complete after 13s [id=projects/lab/zones/us-east1-c/instances/kali-linux]
google_compute_instance.compute_tpot: Creation complete after 14s [id=projects/lab/zones/us-east1-c/instances/tpot]
google_compute_instance.compute_remnux: Creation complete after 14s [id=projects/lab/zones/us-east1-c/instances/remnux-linux]

Apply complete! Resources: 7 added, 0 changed, 0 destroyed.

Once we get a success message we can verify our instances have been created using the gcloud command to list our compute resources.

➜  ~ gcloud compute instances list
NAME          ZONE           MACHINE_TYPE   PREEMPTIBLE  INTERNAL_IP  EXTERNAL_IP     STATUS
kali-linux    us-east1-c     n1-standard-2  true         10.142.0.2   35.231.173.255  RUNNING
remnux-linux  us-east1-c     n1-standard-2  true         10.142.0.4   34.75.110.1     RUNNING
tpot          us-east1-c     n1-standard-4  true         10.142.0.3   34.75.100.245   RUNNING

They’re all up and running and after some time for the startup scripts to complete we’ll have deployed our lab in the cloud!

Tear It down

When we’re ready to tear down the infrastructure Terraform is ready to help. No additional configuration is needed and we only need to use the destroy option. Once again we are prompted to say we’re sure we want to make changes.

➜  ~ terraform destroy  -var-file variables.tfvars
google_compute_network.vpc_network: Refreshing state... [id=projects/lab/global/networks/bananaco-blog-dev-vpc-1]
google_compute_firewall.allow_tpot: Refreshing state... [id=projects/lab/global/firewalls/lab1-ingress-allow-all-honeypot]
[... SNIP ...]

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  - destroy

Terraform will perform the following actions:

  # google_compute_firewall.allow_admin will be destroyed
 [...SNIP...]
  # google_compute_firewall.allow_tpot will be destroyed
[...SNIP...]
  # google_compute_instance.compute_kali will be destroyed
[...SNIP...]
  # google_compute_instance.compute_remnux will be destroyed
[...SNIP...]
  # google_compute_instance.compute_tpot will be destroyed
[...SNIP...]
  # google_compute_network.vpc_network will be destroyed
[...SNIP...]

Plan: 0 to add, 0 to change, 7 to destroy.

Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: 

Accept

google_compute_firewall.allow_admin: Destroying... [id=projects/lab/global/firewalls/lab1-ingress-allow-ssh-admin]
google_compute_instance.compute_remnux: Destroying... [id=projects/lab/zones/us-east1-c/instances/remnux-linux]

[...SNIP...]

google_compute_network.vpc_network: Destruction complete after 41s

Destroy complete! Resources: 7 destroyed.

Again, with gcloud we can verify our instances have been removed.

➜  ~ gcloud compute instances list
NAME          ZONE           MACHINE_TYPE   PREEMPTIBLE  INTERNAL_IP  EXTERNAL_IP     STATUS

Wow that was a lot! Let’s go over what we’ve accomplished

  • We successfully installed Terraform
  • Created a service account to be used by Terraform
  • Using the practice of least privilege we granted the necessary roles to our SA
  • Migrated our BASH based startup script to a Terraform Configuration
  • Validated & verified the deployment
  • Validated & verified the destruction

Now you should have multiple ways to deploy this lab network and also the skills to take this information and utilize it for your own ideas and creations.


The full code for this lab can be found below

Subscribe to the cloud.weekly newsletter!

I write a weekly-ish newsletter on cloud, devops, and privacy called cloud.weekly!
It features the latest news, guides & tutorials and new open source projects. You can sign up via email below.