How to Tighten an Over-Permissive Terraform IAM Policy

Three IAM policies in one main.tf each grant Action "*" on Resource "*" - anyone who assumes those roles owns the whole AWS account. The fix is to scope each policy in Terraform to the exact actions and resource ARN it actually uses: s3:GetObject on one bucket, two sqs actions on one queue, one lambda:InvokeFunction on one function.

Security Engineerawsiamterraform

Why a wildcard Terraform IAM policy is dangerous

A single aws_iam_policy resource that grants everything is common in early infrastructure and dangerous the moment it reaches production:

resource "aws_iam_policy" "api" {
  name   = "api-policy"
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect   = "Allow"
      Action   = "*"
      Resource = "*"
    }]
  })
}

Action = "*" on Resource = "*" means whoever assumes the attached role can do any action on any resource in the account. If the credentials leak - in a log line, an environment variable, a public bucket - an attacker has full control. In this project three policies (api, worker, cron) all carry this same god-mode grant, so there are three separate paths to total account compromise.

Scope each policy to what the role actually needs

Least privilege means listing what each workload does and granting only that. Here each role has exactly one job:

resource "aws_iam_policy" "api" {
  name   = "api-policy"
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect   = "Allow"
      Action   = "s3:GetObject"
      Resource = "arn:aws:s3:::app-uploads/*"
    }]
  })
}

resource "aws_iam_policy" "worker" {
  name   = "worker-policy"
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect   = "Allow"
      Action   = ["sqs:ReceiveMessage", "sqs:DeleteMessage"]
      Resource = "arn:aws:sqs:us-east-1:000000000000:jobs"
    }]
  })
}

resource "aws_iam_policy" "cron" {
  name   = "cron-policy"
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect   = "Allow"
      Action   = "lambda:InvokeFunction"
      Resource = "arn:aws:lambda:us-east-1:000000000000:function:report-generator"
    }]
  })
}

Note the details that matter: the S3 resource ends in /* because s3:GetObject acts on objects inside the bucket, not the bucket itself; the worker uses a list of two SQS actions on the one jobs queue ARN; the cron role gets a single Lambda action on one function ARN. No wildcards remain in either Action or Resource.

Apply and verify the live policy

Apply the change, then read the default policy version back to confirm the live document matches your intent:

cd /workspace && terraform apply -auto-approve

ARN=$(terraform output -raw api_arn)
VID=$(aws iam list-policy-versions --policy-arn "$ARN" \
      --query 'Versions[?IsDefaultVersion].VersionId' --output text)
aws iam get-policy-version --policy-arn "$ARN" --version-id "$VID" \
      --query 'PolicyVersion.Document'

The output should show "Action": "s3:GetObject" and the specific bucket ARN - not "*". Repeat for worker_arn and cron_arn. A statement that still shows "*" in Action or Resource means that policy is still over-permissive.

Iterate toward tight policies in real teams

Never start from "*" and prune by hand. Mature teams deploy a permissive policy, log actual calls in CloudTrail, and let AWS Access Analyzer generate a least-privilege policy from that usage. IAM Access Advisor shows which services a role touched in the last 90 days, so anything unlisted is a candidate to remove. Wire a linter like tfsec or cfn-nag into the pull request so a new wildcard is caught before it ever reaches production.

Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.

Try it in a workspace →

What you'll practice

FAQ

How do I make a Terraform IAM policy least privilege?

Replace Action "*" and Resource "*" with the exact action strings and the specific resource ARN the role uses. In an aws_iam_policy resource, set Action to a single string or a short list, and Resource to the full ARN of the bucket, queue, or function - for example Action = "s3:GetObject" with Resource = "arn:aws:s3:::app-uploads/*".

How do I write an IAM policy in Terraform with jsonencode?

Set the policy argument of aws_iam_policy to jsonencode of a map with Version = "2012-10-17" and a Statement list. Each statement is a map with Effect, Action, and Resource. jsonencode turns the HCL map into the JSON policy document AWS expects, so you keep everything in native Terraform syntax.

How do I verify a Terraform IAM policy after apply?

Get the policy ARN from terraform output, find the default version with aws iam list-policy-versions, then run aws iam get-policy-version with that version id and query PolicyVersion.Document. Confirm the returned Action and Resource are the scoped values and contain no "*".

Why is Action "*" Resource "*" in an IAM policy a security risk?

It grants every action on every resource in the account, so any credential attached to the role can do anything if it leaks. Scoping to specific actions and ARNs limits the blast radius to one operation on one resource instead of the whole account.

Keep learning

Apply AWS IAM Least Privilege to a Wildcard PolicySecurity projectLock Down a Public S3 BucketSecurity projectWrite a Scoped AWS KMS Key PolicySecurity projectSecurity roadmapStep by step to hiredSecurity interview questionsSTAR answersAll Security projectsProjects hub

Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.

Open the workspace →