AWS IAM Roles and Policies with Terraform and OpenTofu
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:
- A trust policy (assume role policy): which principal can assume the role (an EC2 instance, a Lambda function, another account, GitHub).
- 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:
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 #
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:
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 #
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:
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.