[Update] IAM Policy Autopilot now supports Terraform plan files

[Update] IAM Policy Autopilot now supports Terraform plan files

Doesn't Terraform execution permission essentially become Admin permission? IAM Policy Autopilot, which solves this concern, now supports Terraform plan files. In this article, we will actually try out the method of automatically generating the necessary IAM policies from a plan.
2026.08.19

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.

https://aws.amazon.com/about-aws/whats-new/2026/08/iam-policy-autopilot-now-supports-terraform-plan-files/

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.

https://aws.amazon.com/jp/about-aws/whats-new/2025/11/iam-policy-autopilot-generate-iam-policies-code/
https://aws.amazon.com/jp/blogs/news/simplify-iam-policy-creation-with-iam-policy-autopilot-a-new-open-source-mcp-server-for-builders/

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

https://github.com/awslabs/iam-policy-autopilot

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

https://docs.astral.sh/uv/getting-started/installation/

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.

main.tf
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.

Example output excerpt
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)

スクリーンショット 2026-08-19 21.59.17

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.

https://github.com/awslabs/aws-sdk-rust/blob/main/sdk/aws-config/src/profile/credentials.rs

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)

https://github.com/awslabs/iam-policy-autopilot/blob/5e3e168268cff3cd3154e78bb158da28651e7371/Cargo.toml#L83

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.

.mcp.json
{
  "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)

https://github.com/awslabs/iam-policy-autopilot/blob/main/iam-policy-autopilot-mcp-server/src/tools/generate_policy.rs

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?

Share this article

AWSのお困り事はクラスメソッドへ