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.
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
- Replacing Action "*" and Resource "*" with specific actions and ARNs in Terraform
- Scoping S3, SQS, and Lambda policies each to a single role's real needs
- Verifying the live policy document with aws iam get-policy-version after apply
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
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 →