| name | terraform-iam-audit |
|---|---|
| description | Analyse Terraform code in a given path (or CWD) and output the exact minimum AWS IAM permissions an IAM user needs to run terraform apply and terraform destroy against that project. Grouped by resource type, no wildcards. Invoke with /terraform-iam-audit [optional-path], or when user asks "what IAM permissions does this Terraform need", "what AWS permissions are required to deploy this", "list permissions for this terraform project". |
| tools | Bash, Read, Glob, WebFetch |
| version | 2.0.0 |
Scan Terraform source files and emit a precise, grouped list of AWS IAM actions required to deploy and destroy the infrastructure. No wildcards. No over-permissioning. Every action must be justified by an actual Terraform resource or data source in the code, verified against the live AWS Service Authorization Reference.
Accept an optional path argument. If no path is given, use the current working directory. If the resolved path contains no .tf files (excluding .terraform/), stop and ask the user to provide a valid Terraform root path.
Use the provided argument as the Terraform root directory, or default to CWD. Run:
find <PATH> -name "*.tf" -not -path "*/.terraform/*" | sortIf the output is empty, stop with: "No Terraform files found at <PATH>. Please provide a valid path to a Terraform root or module directory."
Run:
grep -rh --include="*.tf" -E '^(resource|data) "aws_' <PATH> \
--exclude-dir=".terraform" \
| grep -oP '"aws_[^"]*"' | tr -d '"' | sort -uCollect two separate lists:
- Resources: lines that started with
resource "aws_*"— these need full CRUD permissions - Data sources: lines that started with
data "aws_*"— these need read-only permissions only
Run:
grep -rh --include="*.tf" -A3 'backend "' <PATH> --exclude-dir=".terraform"If an s3 backend is detected, include a Terraform State (S3 Backend) section at the top of the output with these permissions (no lookup needed — these are fixed):
s3:GetObject—# plan/apply/destroys3:PutObject—# apply/destroys3:DeleteObject—# destroys3:ListBucket—# plan/apply/destroys3:GetBucketLocation—# plan/apply/destroy
If a DynamoDB state lock table is referenced:
dynamodb:GetItem—# plan/apply/destroydynamodb:PutItem—# plan/apply/destroydynamodb:DeleteItem—# plan/apply/destroy
Run:
grep -rh --include="*.tf" -E '^\s*(role|role_arn|execution_role_arn|task_role_arn|iam_role_arn)\s*=' <PATH> --exclude-dir=".terraform"Note which resource types contain role assignment fields. iam:PassRole must be added to those resource types in the output (labelled # apply).
For each unique type discovered in Step 2, determine the AWS Service Authorization Reference page to fetch.
Special cases — zero AWS API calls, skip entirely:
aws_iam_policy_document— local Terraform construct, makes no AWS API callsaws_region— resolved from the provider configuration, makes no AWS API callsaws_partition— resolved from the provider configuration, makes no AWS API calls
For all other types, use the following table to map the Terraform resource prefix to the correct docs page slug:
| Terraform prefix | AWS docs slug |
|---|---|
acm |
awscertificatemanager |
api_gateway |
amazonapigateway |
apigatewayv2 |
amazonapigateway |
appsync |
awsappsync |
autoscaling |
amazonautoscaling |
cloudfront |
amazoncloudfront |
cloudtrail |
awscloudtrail |
cloudwatch |
amazoncloudwatch |
cloudwatch_event |
amazoneventbridge |
cloudwatch_log |
amazoncloudwatchlogs |
codebuild |
awscodebuild |
codeartifact |
awscodeartifact |
cognito |
amazoncognitouserpools |
db |
amazonrds |
dynamodb |
amazondynamodb |
ebs |
amazonec2 |
ec2 |
amazonec2 |
ecr |
amazonelasticcontainerregistry |
ecs |
amazonelasticcontainerservice |
eip |
amazonec2 |
eks |
amazonelastickubernetesservice |
elasticache |
amazonelasticache |
elasticbeanstalk |
awselasticbeanstalk |
firehose |
amazonkinesisfirehose |
flow_log |
amazonec2 |
glue |
awsglue |
iam |
awsiam |
instance |
amazonec2 |
internet_gateway |
amazonec2 |
kafka |
amazonmanagedstreamingforapachekafka |
kinesis_firehose |
amazonkinesisfirehose |
kinesis |
amazonkinesis |
kms |
awskeymanagementservice |
lambda |
awslambda |
launch_template |
amazonec2 |
lb |
awselasticloadbalancingv2 |
alb |
awselasticloadbalancingv2 |
logs |
amazoncloudwatchlogs |
msk |
amazonmanagedstreamingforapachekafka |
nat_gateway |
amazonec2 |
network_acl |
amazonec2 |
rds |
amazonrds |
route53 |
amazonroute53 |
route_table |
amazonec2 |
route |
amazonec2 |
s3 |
amazons3 |
secretsmanager |
awssecretsmanager |
security_group |
amazonec2 |
ses |
amazonsimpleemailservicev1 |
sesv2 |
amazonsimpleemailservicev2 |
sfn |
awsstepfunctions |
sns |
amazonsns |
sqs |
amazonsqs |
ssm |
awssystemsmanager |
states |
awsstepfunctions |
subnet |
amazonec2 |
transit_gateway |
amazonec2 |
vpc |
amazonec2 |
wafv2 |
awswafv2 |
To derive the prefix from a resource type: take aws_<type>, strip aws_, then match the longest prefix in the table. For example, aws_cloudwatch_log_group → try cloudwatch_log first (matches), giving slug amazoncloudwatchlogs.
If the prefix is not in the table, construct the slug attempt as amazon<prefix> and proceed to Step 5a.
For each unique slug identified in Step 5, fetch the AWS Service Authorization Reference page using WebFetch:
https://docs.aws.amazon.com/service-authorization/latest/reference/list_<slug>.html
From the fetched page, extract the full Actions table, including:
- Action name (e.g.
CreateFunction) - Access level (Read, List, Write, Tagging, Permissions management)
- Dependent actions
Then, for each Terraform resource or data source type that maps to this service:
-
Reason about the provider's API calls from the resource name:
- A managed resource (
resource "aws_*") uses Create + Read + Update + Delete API calls - A data source (
data "aws_*") uses only Read/List API calls - Use the resource name to infer which specific API verbs the Terraform AWS provider calls (e.g.
aws_lambda_function→CreateFunction,GetFunction,UpdateFunctionCode,UpdateFunctionConfiguration,DeleteFunction)
- A managed resource (
-
Match those verbs to exact actions from the fetched actions table (e.g.
lambda:CreateFunction) -
Assign phase labels:
# plan— Read and List access-level actions (called duringterraform plan)# apply— Write and Tagging access-level actions for create/update (called duringterraform apply)# destroy— Write access-level actions for delete (called duringterraform destroy)# apply/destroy— Write actions invoked for both create and delete paths
-
Include dependent actions: if the fetched page lists a dependent action for a create/update operation (e.g.
iam:PassRole), include it labelled# apply -
Data sources are read-only: only include Read/List actions, all labelled
# plan
If a page cannot be fetched (404, connection error, or no Actions table in the response):
- Try the alternate prefix: if
list_amazon<slug>failed, trylist_aws<slug>, and vice versa - If both fail, add the type to
## Unmapped Resource Typesnoting the URLs attempted
Do not fabricate actions. Every action in the output must appear in the fetched page's Actions table for that service.
Render the permissions as markdown, structured exactly as follows:
## IAM Permissions Required
**Terraform path:** `<resolved path>`
**Resource types found:** <count>
**Data source types found:** <count>
---
### Terraform State (S3 Backend)
<list only if s3 backend detected>
### <AWS Service Name> (e.g. IAM, Lambda, S3, SES, Route 53)
One H3 section per AWS service. Collect and deduplicate all actions across every Terraform resource and data source that belongs to that service. Within the section, sort actions by phase: all `# plan` first, then `# apply`, then `# destroy`, then `# apply/destroy`. No subheadings — phase label is an inline annotation only.
**IAM** (example):
- `iam:GetPolicy` — `# plan`
- `iam:GetPolicyVersion` — `# plan`
- `iam:ListPolicyVersions` — `# plan`
- `iam:CreatePolicy` — `# apply`
- `iam:CreatePolicyVersion` — `# apply`
- `iam:DeletePolicyVersion` — `# apply`
- `iam:TagPolicy` — `# apply`
- `iam:UntagPolicy` — `# apply`
- `iam:DeletePolicy` — `# destroy`
### <Next AWS Service>
<same structure>
...
### Notes
- Data sources require read-only permissions and are labelled `# plan`.
- `iam:PassRole` is required wherever a resource references an IAM role ARN.
- Actions labelled `# plan` are called during `terraform plan` (read phase).
- Actions labelled `# apply` are called during `terraform apply` (create/update phase).
- Actions labelled `# destroy` are called during `terraform destroy` (delete phase).
- Permissions sourced from the AWS Service Authorization Reference (live lookup).
If any resource types could not be mapped even after the Step 5a WebFetch lookup, add:
## Unmapped Resource Types
The following types could not be resolved. Verify their required permissions in the AWS IAM documentation:
- <type_1> (attempted: <url>)
- <type_2> (attempted: <url>)
- One section per AWS service, not per Terraform resource type. Merge all actions from every
aws_iam_*,aws_lambda_*,aws_s3_*,aws_ses_*,aws_route53_*, etc. into a single H3 per service (e.g.### IAM,### Lambda,### S3,### SES,### Route 53). Deduplicate — if two resource types require the same action, list it once. - Within each service section, sort by phase — no subheadings. All
# planactions first, then# apply, then# destroy, then# apply/destroy. Phase label is an inline annotation on each line only. - No wildcards. Every action must be an exact string like
ses:CreateReceiptRule. Never emitses:*orses:Create*. - No over-permissioning. Do not add actions "just in case". Only include what the Terraform AWS provider actually calls for the resource lifecycle.
- Data sources are read-only. A
data "aws_*"block never needs Create, Update, or Delete actions. aws_iam_policy_document,aws_region, andaws_partitionare local Terraform constructs that make zero AWS API calls. Omit them from the permissions output entirely.iam:PassRolemust be listed for any resource type that accepts arole,role_arn,execution_role_arn, ortask_role_arnargument.- Never fabricate. All output must be derived from actual
.tffiles (Steps 1–2) and the live AWS docs pages (Steps 5–5a). If you are uncertain about a mapping, put it in the## Unmapped Resource Typessection. - Phase labels are required on every action line so the user understands which Terraform operations need each permission.