AWS IAM Roles and Policies with Terraform and OpenTofu

· 2 min read · Terraform & OpenTofu Tutorials

How to create AWS IAM roles and policies with Terraform #

AWS IAM decides who can do what in an AWS account. In Terraform you work with four objects: policies (what is allowed), roles (an identity that someone or something can assume), trust policies (who can assume the role) and instance profiles (how an EC2 instance uses a role). For users, see IAM users with Terraform.

The two policies of a role #

Every role has:

  1. A trust policy (assume role policy): which principal can assume the role (an EC2 instance, a Lambda function, another account, GitHub).
  2. One or more permission policies: what the role can do once assumed.

Build policies with aws_iam_policy_document #

Writing JSON by hand is error-prone. The aws_iam_policy_document data source builds it in HCL and lets you use references:

iam.tf
data "aws_iam_policy_document" "ec2_assume" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["ec2.amazonaws.com"]
    }
  }
}

data "aws_iam_policy_document" "read_bucket" {
  statement {
    sid       = "ListBucket"
    actions   = ["s3:ListBucket"]
    resources = [aws_s3_bucket.assets.arn]
  }

  statement {
    sid       = "ReadObjects"
    actions   = ["s3:GetObject"]
    resources = ["${aws_s3_bucket.assets.arn}/*"]
  }
}

Role, policy and attachment #

iam.tf
resource "aws_iam_role" "app" {
  name               = "ditwl-pro-app-role"
  assume_role_policy = data.aws_iam_policy_document.ec2_assume.json
}

resource "aws_iam_policy" "read_bucket" {
  name   = "ditwl-pro-app-read-bucket"
  policy = data.aws_iam_policy_document.read_bucket.json
}

resource "aws_iam_role_policy_attachment" "read_bucket" {
  role       = aws_iam_role.app.name
  policy_arn = aws_iam_policy.read_bucket.arn
}

# AWS managed policy, for example to use Systems Manager Session Manager
resource "aws_iam_role_policy_attachment" "ssm" {
  role       = aws_iam_role.app.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

An alternative for permissions that belong to only one role is aws_iam_role_policy, an inline policy. Avoid aws_iam_policy_attachment (without role_), which takes exclusive ownership of the policy and can detach it from other identities.

Instance profile for EC2 #

EC2 instances use roles through an instance profile:

ec2.tf
resource "aws_iam_instance_profile" "app" {
  name = "ditwl-pro-app-profile"
  role = aws_iam_role.app.name
}

resource "aws_instance" "app" {
  ami                  = data.aws_ami.ubuntu.id
  instance_type        = "t3.micro"
  iam_instance_profile = aws_iam_instance_profile.app.name
}

The application on the instance now gets temporary credentials from the metadata service. No access keys are stored on disk. See EC2 with Terraform.

Allow another account or a person to assume a role #

cross-account.tf
data "aws_iam_policy_document" "cross_account" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "AWS"
      identifiers = ["arn:aws:iam::111111111111:root"]
    }

    condition {
      test     = "Bool"
      variable = "aws:MultiFactorAuthPresent"
      values   = ["true"]
    }
  }
}

The same pattern is used to give a CI/CD pipeline access: GitHub Actions with OIDC.

Permission boundaries #

A permission boundary is a policy that sets the maximum permissions of a role, whatever policies are attached. It is useful when you let developers create their own roles:

boundary.tf
resource "aws_iam_role" "developer_created" {
  name                 = "app-role"
  assume_role_policy   = data.aws_iam_policy_document.ec2_assume.json
  permissions_boundary = aws_iam_policy.boundary.arn
}

Least privilege checklist #

  • Name actions and resources explicitly. Avoid "Action": "*" and "Resource": "*".
  • Use conditions (source VPC, MFA, tags, aws:PrincipalOrgID).
  • One role per application and per environment.
  • Prefer roles to users and temporary to long-lived credentials.
  • Review the findings of IAM Access Analyzer and the "last used" information.
  • Scan the code with Checkov or Trivy.

Next: S3 with Terraform.

#AWS #AWS IAM #Terraform #OpenTofu #Security