Table of Contents

IAM With Terraform: Security-First Approach

IAM is the foundation of AWS security. Getting it right in Terraform means following the principle of least privilege and making permissions auditable.

IAM Role With Policy

# Role with trust policy
resource "aws_iam_role" "lambda" {
  name = "lambda-execution-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Principal = {
        Service = "lambda.amazonaws.com"
      }
    }]
  })
}

# Managed policy attachment
resource "aws_iam_role_policy_attachment" "lambda_basic" {
  role       = aws_iam_role.lambda.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

# Custom inline policy
resource "aws_iam_role_policy" "s3_access" {
  name = "s3-read-access"
  role = aws_iam_role.lambda.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect   = "Allow"
      Action   = ["s3:GetObject", "s3:ListBucket"]
      Resource = [
        aws_s3_bucket.data.arn,
        "${aws_s3_bucket.data.arn}/*"
      ]
    }]
  })
}

IAM Policy Document Data Source

For complex policies, use the aws_iam_policy_document data source:

data "aws_iam_policy_document" "s3_policy" {
  statement {
    effect    = "Allow"
    actions   = ["s3:GetObject"]
    resources = ["${aws_s3_bucket.data.arn}/*"]

    condition {
      test     = "StringEquals"
      variable = "aws:RequestedRegion"
      values   = ["us-east-1"]
    }
  }
}

resource "aws_iam_policy" "s3_read" {
  name   = "s3-read-policy"
  policy = data.aws_iam_policy_document.s3_policy.json
}

Cross-Account Role

resource "aws_iam_role" "cross_account" {
  name = "cross-account-deploy"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Principal = {
        AWS = "arn:aws:iam::123456789012:root"
      }
      Action = "sts:AssumeRole"
      Condition = {
        StringEquals = {
          "sts:ExternalId" = var.external_id
        }
      }
    }]
  })
}

Best Practices

  1. Least privilege — grant only the permissions needed
  2. Use roles, not users — for services and CI/CD
  3. Use jsonencode() — over heredoc JSON for policy documents
  4. Separate policies by concern — one policy per service/action group
  5. Use conditions — restrict by IP, region, or MFA
  6. Avoid wildcards"Resource": "*" is almost always too broad
  7. Audit with Access Analyzer — find unused permissions

Learn More