Recently, there is an initiative to tighten up security control on terraform deployment. In this post, I will demo how to use provider alias and tell terraform to switch role for resource management
At present, my terraform code are ran by an IAM role with full access. Security team wants to tighten the control and implement the least privileged design. This will require some serious refactoring on terraform code.
Declare provider aliases in root module
In the root module, we must declare all the provider aliases. Each alias tells terraform to switch to an IAM role with limited permissions. The default “aws” provider will have read only access. Then I added a few more such as NetworkAdmin, IamAdmin, etc.
provider "aws" {
region = var.aws-region
assume_role {
role_arn = "arn:aws:iam::111122223333:role/iac-ReadOnly"
session_name = "terraform-ReadOnly"
}
}
provider "aws" {
region = var.aws-region
alias = "NetworkAdmin"
assume_role {
role_arn = "arn:aws:iam::111122223333:role/iac-NetworkAdmin"
session_name = "terraform-NetworkAdmin"
}
}
provider "aws" {
region = var.aws-region
alias = "IamAdmin"
assume_role {
role_arn = "arn:aws:iam::111122223333:role/iac-IamAdmin"
session_name = "terraform-IamAdmin"
}
}
provider "aws" {
region = var.aws-region
alias = "CloudwatchAdmin"
assume_role {
role_arn = "arn:aws:iam::111122223333:role/iac-CloudWatch"
session_name = "terraform-Cloudwatch"
}
}
provider "aws" {
region = var.aws-region
alias = "Ec2Admin"
assume_role {
role_arn = "arn:aws:iam::111122223333:role/iac-Ec2Admin"
session_name = "terraform-Ec2Admin"
}
}
terraform {
required_version = ">= 1.3.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 4.40"
}
}
}
Provide the provider aliases to child modules
Next, tell child modules how to map the provider aliases in the providers block. This will make more sense once we are onto the next step. This allows child modules to manage resources with different provider aliases.
module "vpc" {
providers = {
aws.NetworkAdmin = aws.NetworkAdmin
aws.IamAdmin = aws.IamAdmin
aws.CloudwatchAdmin = aws.CloudwatchAdmin
}
source = "./vpc_subnets"
application = "iac-test"
aws-region = var.aws-region
customer-name = var.customer-name
default-tags = local.default-tags
environment = var.environment
project = var.project
vpc-cidr = "10.77.0.0/16"
vpcflowlog-cwl-loggroup-key-arn = ""
enable-flow-log = true
number-of-private-subnets-per-az = 0
number-of-public-subnets-per-az = 1
}
Specify provider aliases in child provider.tf
In the child module, create a provider.tf with all the provider aliases
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 3.25"
configuration_aliases = [ aws.NetworkAdmin, aws.IamAdmin, aws.CloudwatchAdmin]
}
}
}
Specify providers in each resource inside the child module
Next, in my child module, I need to specify which provider to use for each resource. This is not fun. Essentially, all existing modules need to be refactored, and I need to know which provider (or role) is needed to manage each resource.
resource "aws_flow_log" "vpc-flowlog" {
provider = aws.NetworkAdmin
count = var.enable-flow-log ? 1 : 0
iam_role_arn = aws_iam_role.vpcflowlog-role.arn
log_destination = aws_cloudwatch_log_group.vpcflowlog-loggroup[0].arn
traffic_type = "ALL"
vpc_id = aws_vpc.vpc.id
tags = merge(
var.default-tags,
{
Name = "${local.resource-prefix}-vpcflowlog"
},
)
}
resource "aws_cloudwatch_log_group" "vpcflowlog-loggroup" {
provider = aws.CloudwatchAdmin
count = var.enable-flow-log ? 1 : 0
name_prefix = "vpcflowlog/${aws_vpc.vpc.id}/"
kms_key_id = var.vpcflowlog-cwl-loggroup-key-arn
retention_in_days = var.vpcflowlog-retain-days
tags = var.default-tags
}
Design the roles
Now the missing piece is to come up with a list of IAM roles. Each role will have limited but sufficient permissions to perform their tasks. I use the job function roles as a start but quickly find them inadequate. For example, the NetworkAdministrator managed policy alliows iam:PassRole only if the flowlog role name matches AWS standard (flow-logs-*). In my module, flow logs are named with a different convention. An additional policy needs to be attached to allow iam:PassRole. Below is a snippet from the NetworkAdministrator managed policy, showing the resource arn restriction.
{
"Effect": "Allow",
"Action": [
"iam:GetRole",
"iam:ListRoles",
"iam:PassRole"
],
"Resource": "arn:aws:iam::*:role/flow-logs-*"
}
That’s just a proof of concept. To apply this to a real project, I imagine the amount of time will be 3-4 times more than using a single admin role. Also beware of circular dependency. I have experienced that in the past and I’ll write more once I have an example.