[Update] IAM Policy Autopilot now supports Terraform plan files
This page has been translated by machine translation. View original
Introduction
Hello everyone, this is Akaike.
Are the IAM role permissions used during Terraform execution effectively equivalent to Admin or PowerUser?
Mine are.
Since Terraform touches resources across many different services, identifying every single required action for all of them and assembling a least-privilege role is, honestly, quite a laborious task.
So I've had the experience many times of thinking "Let's just attach AdministratorAccess (or PowerUserAccess) for now and tighten it up later!" and then leaving it that way for months... (and I'm still doing it today)
In that context, there was an update announcing that IAM Policy Autopilot now supports Terraform plan files.
It apparently generates the necessary IAM policies from a plan, which sounds pretty useful.
So this time, I'll try it out all the way through generating an IAM policy from a Terraform plan.
I'll also show how to use it via an MCP server.
What is IAM Policy Autopilot?
IAM Policy Autopilot is an open-source IAM policy auto-generation tool announced at re:Invent 2025.
It deterministically performs static analysis on AWS SDK calls and Terraform plans, and generates a "draft" policy scoped down to only the necessary permissions.
This means there's no need to execute code or accumulate CloudTrail logs — the analysis completes locally. (No additional charges either)
The CLI has three commands, which you can use to generate IAM policies tailored to your code's content.
generate-policies- Generates IAM policies from source code or Terraform plans
fix-access-denied- Analyzes AccessDenied errors and generates/applies the necessary policies
mcp-server- Starts an MCP server (integrates with Claude Desktop, Kiro, etc.)
Previous Limitations
Previously, the analysis targets were application source code that calls the AWS SDK.
(Supported languages include Python's Boto3, and the Go/Java/JavaScript/TypeScript SDKs, etc.)
For example, it would look at boto3's s3.put_object() and deduce that s3:PutObject is required.
In other words, what was generated were the permissions needed by the application at runtime.
Newly Added Features
With this update, it can now generate policies from Terraform plan files.
A plan contains everything about "which resources to create/modify/delete and with what configuration." When you pass a plan (JSON) to generate-policies, it generates a policy scoped to the necessary actions based on that content.
Moreover, wherever possible, it references specific resource ARNs rather than wildcards.
In other words, it reverse-engineers the permissions needed to run terraform apply from the plan. Let's actually try it out.
Trying It Out (CLI)
Prerequisites (Installing uv)
IAM Policy Autopilot can be installed in several ways, but this time I'll use uv (uvx), which can be executed without any additional installation.
If you don't have uv, install it using the official installer.
# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
Using uvx, you can run packages directly without explicitly installing them.
uvx iam-policy-autopilot
However, the above command resulted in version 0.2.3 in my environment, so I specified the official PyPI as the index to get the latest version.
(In this blog, the uvx commands going forward include the --default-index https://pypi.org/simple option)
uvx --default-index https://pypi.org/simple iam-policy-autopilot version
You're good to go if you get 0.3.0.
iam-policy-autopilot 0.3.0
Setting Up a Terraform Configuration for Testing
This time I prepared a simple configuration that just creates an S3 bucket and a DynamoDB table.
terraform {
required_version = ">= 1.15"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "ap-northeast-1"
}
resource "aws_s3_bucket" "app" {
bucket = "autopilot-demo-bucket"
}
resource "aws_dynamodb_table" "app" {
name = "autopilot-demo-table"
billing_mode = "PAY_PER_REQUEST"
hash_key = "id"
attribute {
name = "id"
type = "S"
}
}
Generating the Plan and Converting It to JSON
IAM Policy Autopilot accepts plan files in JSON format.
Save the plan with terraform plan, then convert it to JSON using terraform show -json.
# Initialize
terraform init
# Save the plan to a file
terraform plan -out=plan.tfplan
# Convert the saved plan to JSON
terraform show -json plan.tfplan > plan.json
This creates plan.json.
Generating the Policy
Now let's pass the generated plan.json to IAM Policy Autopilot.
There's no need to specify the type of input — the tool automatically detects it.
(--pretty is an option to format the output JSON)
uvx --default-index https://pypi.org/simple iam-policy-autopilot generate-policies plan.json --pretty
The actually generated JSON was as follows.
{
"Policies": [
{
"Policy": {
"Id": "IamPolicyAutopilot",
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"dynamodb:AssociateTableReplica",
"dynamodb:BatchWriteItem",
"dynamodb:CreateGlobalTableWitness",
"dynamodb:CreateTable",
"dynamodb:CreateTableReplica",
"dynamodb:DeleteGlobalTableWitness",
"dynamodb:DeleteItem",
"dynamodb:DescribeContinuousBackups",
"dynamodb:DescribeImport",
"dynamodb:DescribeTable",
"dynamodb:DescribeTimeToLive",
"dynamodb:GetItem",
"dynamodb:ImportTable",
"dynamodb:ListTagsOfResource",
"dynamodb:PutItem",
"dynamodb:PutResourcePolicy",
"dynamodb:Query",
"dynamodb:ReadDataForReplication",
"dynamodb:ReplicateSettings",
"dynamodb:RestoreTableFromBackup",
"dynamodb:Scan",
"dynamodb:TagResource",
"dynamodb:UntagResource",
"dynamodb:UpdateContinuousBackups",
"dynamodb:UpdateItem",
"dynamodb:UpdateTable",
"dynamodb:UpdateTimeToLive",
"dynamodb:WriteDataForReplication"
],
"Resource": [
"arn:*:dynamodb:*:*:table/autopilot-demo-table",
"arn:*:dynamodb:*:*:table/autopilot-demo-table/backup/*",
"arn:*:dynamodb:*:*:table/autopilot-demo-table/import/*",
"arn:*:dynamodb:*:*:table/autopilot-demo-table/index/*",
"arn:*:dynamodb:*:*:table/autopilot-demo-table/stream/*"
]
},
{
"Effect": "Allow",
"Action": [
"iam:PassRole"
],
"Resource": [
"*"
],
"Condition": {
"StringEquals": {
"iam:PassedToService": [
"s3.amazonaws.com"
]
}
}
},
{
"Effect": "Allow",
"Action": [
"kms:CreateGrant",
"kms:DescribeKey"
],
"Resource": [
"arn:*:kms:*:*:key/*"
],
"Condition": {
"StringLike": {
"kms:ViaService": [
"dynamodb.*.amazonaws.com"
]
}
}
},
{
"Effect": "Allow",
"Action": [
"s3:CreateBucket",
"s3:DeleteBucketPolicy",
"s3:DeleteBucketWebsite",
"s3:GetAccelerateConfiguration",
"s3:GetBucketAcl",
"s3:GetBucketCORS",
"s3:GetBucketLogging",
"s3:GetBucketObjectLockConfiguration",
"s3:GetBucketPolicy",
"s3:GetBucketRequestPayment",
"s3:GetBucketTagging",
"s3:GetBucketVersioning",
"s3:GetBucketWebsite",
"s3:GetEncryptionConfiguration",
"s3:GetLifecycleConfiguration",
"s3:GetReplicationConfiguration",
"s3:ListBucket",
"s3:PutAccelerateConfiguration",
"s3:PutBucketAcl",
"s3:PutBucketCORS",
"s3:PutBucketLogging",
"s3:PutBucketObjectLockConfiguration",
"s3:PutBucketOwnershipControls",
"s3:PutBucketPolicy",
"s3:PutBucketRequestPayment",
"s3:PutBucketTagging",
"s3:PutBucketVersioning",
"s3:PutBucketWebsite",
"s3:PutEncryptionConfiguration",
"s3:PutLifecycleConfiguration",
"s3:PutReplicationConfiguration"
],
"Resource": [
"arn:*:s3:*:*:accesspoint/*",
"arn:*:s3:::akaike-autopilot-demo-bucket"
]
},
{
"Effect": "Allow",
"Action": [
"s3:DeleteObjectTagging",
"s3:DeleteObjectVersionTagging",
"s3:GetObjectTagging",
"s3:GetObjectVersionTagging",
"s3:PutObjectTagging",
"s3:PutObjectVersionTagging"
],
"Resource": [
"arn:*:s3:*:*:accesspoint/*/object/*",
"arn:*:s3:::akaike-autopilot-demo-bucket/*"
]
},
{
"Effect": "Allow",
"Action": [
"s3-object-lambda:DeleteObjectTagging",
"s3-object-lambda:GetObjectTagging",
"s3-object-lambda:PutObjectTagging"
],
"Resource": [
"arn:*:s3:*:*:accesspoint/*/object/*",
"arn:*:s3:::*/*"
]
},
{
"Effect": "Allow",
"Action": [
"s3express:GetLifecycleConfiguration",
"s3express:PutLifecycleConfiguration"
],
"Resource": [
"arn:*:s3:::*"
]
}
]
},
"PolicyType": "Identity"
}
],
"Warnings": [
{
"WarningType": "WildcardResource",
"Location": {
"PolicyIndex": 0,
"StatementIndex": 1
},
"Message": "Statement could not be scoped to specific resources and uses Resource \"*\". Review whether broad resource access is intended."
}
]
}
It's great that Resource is properly scoped to specific resource names (like autopilot-demo-table and autopilot-demo-bucket).
The bucket name and table name written in the plan appear to be reflected directly in the ARNs.
Also, regarding the Warnings at the end — for entries where it couldn't scope to a specific resource and used Resource: "*" (in this case iam:PassRole), it issues a warning saying "Please verify whether this broad access is intentional."
It's helpful that it points out which parts need to be reviewed.
Making ARNs More Specific with region / account
In the previous output, the region and account ID in the ARNs were wildcards like arn:*:dynamodb:*:*:....
By specifying --region and --account, these will be replaced with concrete values.
uvx --default-index https://pypi.org/simple iam-policy-autopilot generate-policies plan.json \
--region ap-northeast-1 \
--account XXXXXXXXXXXX \
--pretty
This brings the policy closer to least privilege, so if a more secure configuration is desired, it's a good idea to specify the deployment target region and account.
arn:aws:dynamodb:ap-northeast-1:XXXXXXXXXXXX:table/autopilot-demo-table
Creating the Generated Policy Directly in IAM with upload-policies
Instead of just outputting the generated policy to stdout, you can add --upload-policies to immediately create it as an IAM policy.
Internally, iam:CreatePolicy is called.
# Create with the default name
uvx --default-index https://pypi.org/simple iam-policy-autopilot generate-policies plan.json --upload-policies
# Create with a specified prefix
uvx --default-index https://pypi.org/simple iam-policy-autopilot generate-policies plan.json --upload-policies TerraformDeployRole
The policy name will be IamPolicyAutopilotGeneratedPolicy_1 if no prefix is specified, or <prefix>_1 with sequential numbering if a prefix is given.
(It automatically selects a number that doesn't conflict with existing policies)

Note: An error occurs with aws login profiles
At this point I encountered an error.
Error: Failed to upload policies to AWS IAM
Caused by: AWS IAM list policies error: dispatch failure
Caused by: the credentials provider was not properly configured
Caused by: ProfileFile provider could not be built: This behavior requires following cargo feature(s) enabled: credentials-login. In order to use an active login session, the `credentials-login` feature must be enabled.
This was not an issue with insufficient IAM permissions, but rather a problem with how credentials are obtained.
My profile was authenticated using aws login.
IAM Policy Autopilot is written in Rust (AWS SDK for Rust), and reading a profile that uses login_session requires building the SDK with the credentials-login feature enabled.
Looking at IAM Policy Autopilot's Cargo.toml, aws-config had no such feature specified, which is likely why the error occurs. (This is my assumption)
The solution is to pass static credentials via environment variables that the tool can read.
The AWS CLI's export-credentials can export the current session to environment variables.
# Replace the profile name with the one you use
# With this, credentials can be read from environment variables, resolving the error
eval "$(aws configure export-credentials --profile default --format env)"
Trying It Out (MCP)
IAM Policy Autopilot also works as an MCP (Model Context Protocol) server.
I'll use Claude Code as an example here.
Registering with Claude Code
You can register it with the claude mcp add command,
claude mcp add iam-policy-autopilot \
--env AWS_PROFILE=default \
--env AWS_REGION=ap-northeast-1 \
-- uvx --default-index https://pypi.org/simple iam-policy-autopilot mcp-server
or if you want to configure it at the project level, write it in .mcp.json or similar.
{
"mcpServers": {
"iam-policy-autopilot": {
"command": "uvx",
"args": [
"--default-index",
"https://pypi.org/simple",
"iam-policy-autopilot",
"mcp-server"
],
"env": {
"AWS_PROFILE": "default",
"AWS_REGION": "ap-northeast-1"
}
}
}
}
Tools Exposed by the MCP Server
When I actually connected to the MCP server and called tools/list, I found the following three tools exposed.
The Terraform plan, which is the main topic of this article, can be handled by the main generate_application_policies tool.
| Tool Name | Role |
|---|---|
generate_application_policies |
Generates IAM policies from source code or Terraform plans (the main tool) |
generate_policy_for_access_denied |
Generates the necessary policies from AccessDenied error messages |
fix_access_denied |
Applies the generated AccessDenied policies to the actual AWS account |
How to Use It
After registering, simply ask Claude Code something like this:
Please create an IAM policy with the permissions needed to apply Terraform.
Claude Code will then call the generate_application_policies tool and present the generated policy JSON.
The result is the same as calling the CLI directly, but the convenience is being able to continue with review and modification discussions right within the chat flow.
Note that the CLI's --upload-policies feature for directly creating IAM policies doesn't seem to be supported in the MCP version.
I tried several times, but it doesn't appear to be supported in the MCP version at all.
(It doesn't seem to be implemented)
Closing
That's it — I tried out IAM Policy Autopilot's support for Terraform plan files.
I found it quite practical that the "permissions needed for the Terraform execution role," which I used to hunt for by bouncing ideas off an AI, can now be automatically scoped and generated from a plan.
It also seems like it could save a fair number of AI tokens, so I plan to start using it going forward.
If you've created a Terraform execution role with Admin-equivalent permissions and left it that way, or if you're struggling with IAM permission design, why not give it a try?
