George Richardson

Testing IAM permissions in Terraform

I have recently been writing a Terraform module for least privilege access to centralised Terraform state and locking in AWS. Although its not the most complicated module in the world, I did want to be sure that it was generating least privilege IAM policies, and will continue to do so in the future. It turns out the Terraform test command, combined with the iam_principal_policy_simulation data source, makes it suprisingly easy to unit test your permissions policies.

A painting of a hard hat on a scale.

Let’s test a simple IAM role which we will grant the ability to download any objects from an S3 bucket that are prefixed with “accessible/”. We can define the role and bucket in a root module like this:

# main.tf

variable "bucket_name" {}
variable "role_name" {}

data "aws_caller_identity" "current" {}

# Our role we are going to test
resource "aws_iam_role" "role" {
  name               = var.role_name
  assume_role_policy = data.aws_iam_policy_document.trust.json
  inline_policy {
    name   = "get-s3"
    policy = data.aws_iam_policy_document.s3_get_object.json
  }
}

# The permissions policy we are going to test
data "aws_iam_policy_document" "s3_get_object" {
  statement {
    effect    = "Allow"
    actions   = ["s3:GetObject"]
    resources = ["${aws_s3_bucket.objects.arn}/accessible/*"]
  }
}

data "aws_iam_policy_document" "trust" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "AWS"
      identifiers = [data.aws_caller_identity.current.arn]
    }
  }
}

resource "aws_s3_bucket" "objects" {
  bucket = var.bucket_name
}

# Outputs for wiring up the tests
output "role_arn" {
  value = aws_iam_role.role.arn
}

output "bucket_arn" {
  value = aws_s3_bucket.objects.arn
}


Now let’s create a test helper module that contains the aws_iam_principal_policy_simulation data source which we can call from our test files.

# tests/policy_simulation/main.tf

# These variables will just get passed straight through to the 
# aws_iam_principal_policy_simulation data source. If you need to configure
# other arguments of the simulation, you can add more variables.
variable "action_names" {
  type = list(string)
}

variable "policy_source_arn" {
  type = string
}

variable "resource_arns" {
  type = list(string)
}

# Here's where the magic happens!
data "aws_iam_principal_policy_simulation" "test" {
  action_names      = var.action_names
  policy_source_arn = var.policy_source_arn
  resource_arns     = var.resource_arns
}

# Output all attributes of the simulation for access in our test file.
output "test_results" {
  value = data.aws_iam_principal_policy_simulation.test
}

Finally we can write some tests. One test will check we can get an object with the correct prefix, another will ensure we cannot get an object with an incorrect prefix.

# tests/role_test.tftest.hcl
run "system_under_test" {
  variables {
    role_name   = "test-role"
    bucket_name = "iam-test-example-bucket"
  }
}

run "can_get_prefixed_object" {
  # Use our helper module
  module {
    source = "./tests/policy_simulation"
  }

  variables {
    action_names      = ["s3:GetObject"]
    policy_source_arn = run.system_under_test.role_arn
    resource_arns     = ["${run.system_under_test.bucket_arn}/accessible/file"]
  }

  assert {
    # all_allowed is an attribute of aws_iam_principal_policy_simulation
    condition     = output.test_results.all_allowed
    error_message = "Cannot get object 'accessible/file'."
  }
}

run "cannot_get_unprefixed_object" {
  module {
    source = "./tests/policy_simulation"
  }

  variables {
    action_names      = ["s3:GetObject"]
    policy_source_arn = run.system_under_test.role_arn
    resource_arns     = ["${run.system_under_test.bucket_arn}/wrongprefix/file"]
  }

  assert {
    condition     = !output.test_results.all_allowed
    error_message = "Can get object 'wrongprefix/file'."
  }
}

At this point we can run terraform test and validate our role is functioning as intended:

> terraform test
tests/role_test.tftest.hcl... in progress
  run "system_under_test"... pass
  run "can_get_prefixed_object"... pass
  run "cannot_get_unprefixed_object"... pass
tests/role_test.tftest.hcl... tearing down
tests/role_test.tftest.hcl... pass

Success! 3 passed, 0 failed.

Now is this useful? In this simplictic example, probably not! However, if you are using complex logic to generate your IAM policies, this can be a great way to ensure they are working as expected.

You can see more complete examples in my module’s tests directory.

My head shot
Hi! I'm George. I do things in the cloud and sometimes write about it.
If you like what you read, join my newsletter below.