IAM Least Privilege in Terraform: Step-by-Step Guide

Last reviewed: 2026-05-27 · 10 min read

Rost Mironenko
Rost Mironenko·Founder, ArchGuard

5+ years AWS engineering · Open-source contributor

Last reviewed: 2026-05-27

IAM least privilege in Terraform means granting identities — roles, users, and service principals — only the permissions required for their stated function. Every major AWS breach in the past decade involved at least one identity with more permissions than it needed. This guide shows how to enforce least privilege directly in Terraform HCL using aws_iam_policy_document, condition blocks, and IAM Access Analyzer — before any infrastructure reaches production.

Step 1 — Replace inline JSON with aws_iam_policy_document

The most error-prone IAM pattern in Terraform is the inline JSON string inside an aws_iam_role_policy or aws_iam_policy resource. JSON strings are not validated at plan time, produce single-line diffs that hide permission changes, and break silently when ARN interpolation is incorrect.

main.tf✗ Before
resource "aws_iam_role_policy" "bad" {  name = "lambda-s3-access"  role = aws_iam_role.lambda.id  policy = jsonencode({    Version = "2012-10-17"    Statement = [{      Effect   = "Allow"      Action   = "s3:*"      Resource = "*"    }]  })}
main.tf✓ After
data "aws_iam_policy_document" "lambda_s3" {  statement {    sid    = "ReadTargetBucket"    effect = "Allow"    actions = [      "s3:GetObject",      "s3:ListBucket",    ]    resources = [      aws_s3_bucket.uploads.arn,      "${aws_s3_bucket.uploads.arn}/*",    ]  }}resource "aws_iam_role_policy" "lambda_s3" {  name   = "lambda-s3-read"  role   = aws_iam_role.lambda.id  policy = data.aws_iam_policy_document.lambda_s3.json}

The aws_iam_policy_document data source is validated by the AWS Terraform provider during terraform plan. Actions and Resources appear as discrete list items in pull request diffs, making reviews actionable. ARN references to other Terraform resources are typed expressions, not string interpolation.

Step 2 — Narrow Actions to the minimum required

The AWS Well-Architected Security pillar (SEC03-BP02) requires granting access based on least privilege. Wildcard actions like s3:* or ec2:* grant permissions that will never be used — and create exploitable surface area when credentials are compromised.

Common over-privilege patterns to eliminate

  • s3:*s3:GetObject, s3:PutObject, s3:ListBucket
  • ec2:*ec2:DescribeInstances, ec2:StartInstances
  • Resource = "*"aws_s3_bucket.data.arn
  • iam:*iam:PassRole (with Condition: iam:PassedToService)

Step 3 — Add condition blocks to restrict context

Condition blocks add a second dimension to IAM: they restrict when and from where a permission applies. The AWS Well-Architected Framework considers conditions a requirement for sensitive actions, particularly for cross-account trust, MFA-sensitive operations, and service-linked role assumptions.

trust-policy.tf
data "aws_iam_policy_document" "assume_role_mfa" {  statement {    sid     = "AllowAssumeWithMFA"    effect  = "Allow"    actions = ["sts:AssumeRole"]    principals {      type        = "AWS"      identifiers = [var.trusted_account_arn]    }    condition {      test     = "Bool"      variable = "aws:MultiFactorAuthPresent"      values   = ["true"]    }    condition {      test     = "StringEquals"      variable = "aws:RequestedRegion"      values   = ["eu-west-1", "us-east-1"]    }  }}

MFA and region conditions reduce blast radius when credentials are compromised

Useful condition keys for least-privilege enforcement:

  • aws:MultiFactorAuthPresentRequire MFA for sensitive console actions
  • aws:RequestedRegionRestrict service calls to your active regions
  • aws:PrincipalOrgIDRestrict cross-account access to your AWS Organization
  • aws:SourceVpcRestrict S3 bucket access to resources inside your VPC
  • iam:PassedToServiceScope iam:PassRole to a specific AWS service

Step 4 — Enable IAM Access Analyzer per region

IAM Access Analyzer is an AWS-native service that monitors deployed IAM policies, S3 bucket policies, KMS key policies, and SQS queue policies for findings that indicate unintended external access. Enable it in every region where you have active workloads. Access Analyzer validates what is deployed — not the Terraform source — so it is a complementary control, not a replacement for code review.

security.tf
resource "aws_accessanalyzer_analyzer" "account" {  analyzer_name = "${var.env}-account-analyzer"  type          = "ACCOUNT"  tags = {    Environment = var.env    ManagedBy   = "terraform"  }}# For multi-account setups: deploy ORGANIZATION type on the management accountresource "aws_accessanalyzer_analyzer" "org" {  count = var.is_management_account ? 1 : 0  analyzer_name = "org-wide-analyzer"  type          = "ORGANIZATION"}

ACCOUNT type covers a single account; ORGANIZATION covers all accounts in an AWS Organization

Step 5 — Avoid five common over-privilege anti-patterns

1

AdministratorAccess on Lambda execution roles

Lambda execution roles need only CloudWatch Logs permissions plus access to the specific downstream resources the function calls. AdministratorAccess on a compute resource means a single function compromise equals full account access.

2

iam:PassRole without a PassedToService condition

Allowing iam:PassRole on Resource = "*" lets an attacker escalate privileges by passing an admin role to a new Lambda, ECS task, or EC2 instance. Scope it with Condition: iam:PassedToService = "lambda.amazonaws.com".

3

Static AWS credentials in Terraform provider blocks

Hard-coded access_key and secret_key in provider configurations expose long-lived credentials. Use OIDC federation (GitHub Actions, CircleCI) or IAM instance profiles instead. Rotate any existing static credentials immediately.

4

NotAction with wildcard Resource

"Allow everything except iam:Delete*" is not least privilege — it is a maintainability trap that grows as AWS adds new services and actions. Model permissions by what is needed, not by what you are trying to exclude.

5

Shared execution roles across multiple Lambda functions

Each Lambda function should have its own dedicated execution role with only the permissions that function needs. Shared roles mean one function's compromise affects all functions that share the role.

Related Security pillar articles

Frequently asked questions

What does least privilege mean in AWS IAM?

Least privilege means granting an identity only the permissions it needs to perform its intended function — no more. In practice: specific Actions (not s3:*), specific Resources (not *), and Condition blocks that restrict when and from where the permission applies.

Why use aws_iam_policy_document over inline JSON in Terraform?

aws_iam_policy_document is validated by the Terraform AWS provider during plan, supports references to other resource ARNs as typed expressions, and produces readable diffs in pull requests. Inline JSON strings are opaque blobs that silently fail when malformed and hide ARN interpolation errors.

Does IAM Access Analyzer work with Terraform?

IAM Access Analyzer is an AWS service you enable per-region using the aws_accessanalyzer_analyzer Terraform resource. It continuously monitors deployed IAM policies and S3 bucket policies for external access findings. It validates the deployed policies — not the Terraform source code itself.

What is the most common IAM misconfiguration in Terraform?

Resource = "*" combined with broad Actions (s3:*, ec2:*). The second most common is a missing Condition block that would restrict access by MFA, source VPC, or aws:RequestedRegion. Both patterns are detectable in Terraform HCL before deployment.

Get an automated IAM least-privilege review

ArchGuard reviews your Terraform for IAM anti-patterns across the AWS Well-Architected Security pillar and delivers a branded PDF in 24 hours.

See how it works