Skip to content

Instantly share code, notes, and snippets.

@swateek
Created April 26, 2026 17:10
Show Gist options
  • Select an option

  • Save swateek/c0687515d43c8cca75f0a614f4f30a2d to your computer and use it in GitHub Desktop.

Select an option

Save swateek/c0687515d43c8cca75f0a614f4f30a2d to your computer and use it in GitHub Desktop.
An IAM Audit for Terraform Resources
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

Terraform IAM Audit Skill

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.

Required Inputs

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.

Workflow

Step 1 — Resolve path

Use the provided argument as the Terraform root directory, or default to CWD. Run:

find <PATH> -name "*.tf" -not -path "*/.terraform/*" | sort

If the output is empty, stop with: "No Terraform files found at <PATH>. Please provide a valid path to a Terraform root or module directory."

Step 2 — Extract resource and data source types

Run:

grep -rh --include="*.tf" -E '^(resource|data) "aws_' <PATH> \
  --exclude-dir=".terraform" \
  | grep -oP '"aws_[^"]*"' | tr -d '"' | sort -u

Collect 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

Step 3 — Detect Terraform backend

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/destroy
  • s3:PutObject# apply/destroy
  • s3:DeleteObject# destroy
  • s3:ListBucket# plan/apply/destroy
  • s3:GetBucketLocation# plan/apply/destroy

If a DynamoDB state lock table is referenced:

  • dynamodb:GetItem# plan/apply/destroy
  • dynamodb:PutItem# plan/apply/destroy
  • dynamodb:DeleteItem# plan/apply/destroy

Step 4 — Detect iam:PassRole usage

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).

Step 5 — Map each type to its AWS docs page

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 calls
  • aws_region — resolved from the provider configuration, makes no AWS API calls
  • aws_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.

Step 5a — Fetch the IAM actions from AWS docs

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:

  1. 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_functionCreateFunction, GetFunction, UpdateFunctionCode, UpdateFunctionConfiguration, DeleteFunction)
  2. Match those verbs to exact actions from the fetched actions table (e.g. lambda:CreateFunction)

  3. Assign phase labels:

    • # plan — Read and List access-level actions (called during terraform plan)
    • # apply — Write and Tagging access-level actions for create/update (called during terraform apply)
    • # destroy — Write access-level actions for delete (called during terraform destroy)
    • # apply/destroy — Write actions invoked for both create and delete paths
  4. Include dependent actions: if the fetched page lists a dependent action for a create/update operation (e.g. iam:PassRole), include it labelled # apply

  5. 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, try list_aws<slug>, and vice versa
  • If both fail, add the type to ## Unmapped Resource Types noting the URLs attempted

Do not fabricate actions. Every action in the output must appear in the fetched page's Actions table for that service.

Step 6 — Output the result

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>)

Key Rules

  • 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 # plan actions 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 emit ses:* or ses: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, and aws_partition are local Terraform constructs that make zero AWS API calls. Omit them from the permissions output entirely.
  • iam:PassRole must be listed for any resource type that accepts a role, role_arn, execution_role_arn, or task_role_arn argument.
  • Never fabricate. All output must be derived from actual .tf files (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 Types section.
  • Phase labels are required on every action line so the user understands which Terraform operations need each permission.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment