Use Cases (Updated: 6/3/2026)

Claude Code and AWS IAM: A Practical Least-Privilege Policy Guide

Design and validate least-privilege AWS IAM policies with Claude Code, Access Analyzer, and CDK examples.

Claude Code and AWS IAM: A Practical Least-Privilege Policy Guide

AWS IAM decides who can do what to which AWS resources. A policy is only a JSON document, but a careless Action: "*" or Resource: "*" can give a Lambda function, CI job, or human user far more power than the task requires. Claude Code is useful for drafting and reviewing IAM, but it must not become an unchecked policy generator.

This guide shows the workflow I use: describe the use case, let Claude Code draft a policy, validate it with IAM Access Analyzer, review it as code, and test both allowed and denied actions. The beginner translation is simple: a policy is the permission rule, a role is the temporary identity a workload assumes, and a principal is the user or service using the permission.

I changed my own process after finding a temporary broad S3 policy still attached to a production Lambda role weeks after a hotfix. Nothing leaked, but the incident made the review rule obvious: Claude Code can reduce drafting time, not accountability.

Use AWS docs as the baseline

This article follows the current AWS guidance in these official references:

The practical message is consistent: prefer temporary credentials and roles, apply least privilege, validate policies with Access Analyzer, use conditions carefully, and remove unused permissions regularly.

flowchart LR
  A["Write the use case"] --> B["Claude Code draft"]
  B --> C["Human ARN review"]
  C --> D["IAM Access Analyzer"]
  D --> E["CDK implementation"]
  E --> F["Allowed and denied tests"]

Start with concrete use cases

Do not prompt Claude Code with only “write an IAM policy.” Give it the actor, resources, allowed operations, and operations that must stay forbidden.

Use casePrincipalRequired permissionsExplicitly avoid
Thumbnail LambdaLambda execution roleRead one S3 prefix, write one S3 prefix, write one DynamoDB table, publish one SNS topic, write logsS3 delete, all-bucket read, IAM operations
Admin upload helperAPI Lambda rolePutObject to one S3 prefix, GetItem for one DynamoDB tableWhole-bucket List, KMS key management
GitHub Actions deployOIDC CI roleUpdate one CloudFormation stack and target LambdaAdministratorAccess, all-region operations
Incident read-only roleFederated human userSearch CloudWatch Logs, read X-Ray traces, GetItem on relevant tableUpdates, deletes, Secrets Manager read

The “avoid” column matters. Generative tools are much better when the negative boundary is explicit.

Copy-paste Lambda policy

This policy is for a Lambda function that reads images from one S3 prefix, writes thumbnails to another bucket prefix, records metadata in DynamoDB, publishes an SNS alert, and writes logs. The log group is assumed to be created by infrastructure code, so the role does not need logs:CreateLogGroup.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ReadSourceImages",
      "Effect": "Allow",
      "Action": ["s3:GetObject"],
      "Resource": "arn:aws:s3:::user-uploads-prod/incoming/*"
    },
    {
      "Sid": "WriteThumbnails",
      "Effect": "Allow",
      "Action": ["s3:PutObject"],
      "Resource": "arn:aws:s3:::user-thumbnails-prod/thumbnails/*",
      "Condition": {
        "StringEquals": {
          "s3:x-amz-server-side-encryption": "AES256"
        }
      }
    },
    {
      "Sid": "WriteMetadata",
      "Effect": "Allow",
      "Action": ["dynamodb:PutItem", "dynamodb:UpdateItem"],
      "Resource": "arn:aws:dynamodb:ap-northeast-1:123456789012:table/image-metadata"
    },
    {
      "Sid": "PublishAlerts",
      "Effect": "Allow",
      "Action": ["sns:Publish"],
      "Resource": "arn:aws:sns:ap-northeast-1:123456789012:alert-topic"
    },
    {
      "Sid": "WriteLambdaLogs",
      "Effect": "Allow",
      "Action": ["logs:CreateLogStream", "logs:PutLogEvents"],
      "Resource": "arn:aws:logs:ap-northeast-1:123456789012:log-group:/aws/lambda/image-worker-prod:*"
    }
  ]
}

Save it as policy-lambda-image-worker.json and run:

aws accessanalyzer validate-policy \
  --policy-document file://policy-lambda-image-worker.json \
  --policy-type IDENTITY_POLICY \
  --query "findings[?findingType!='SUGGESTION']"

Then ask Claude Code for a structured review:

claude -p "Review policy-lambda-image-worker.json as an AWS IAM least-privilege policy.
Context: Lambda reads S3 incoming/, writes thumbnails/, writes DynamoDB image-metadata, publishes SNS alert-topic, and writes CloudWatch Logs.
Check: wildcards, delete permissions, broad Resources, Condition correctness, and log scope.
Return a table with Sid, risk, reason, and safer fix."

Access Analyzer checks grammar, ARNs, actions, condition keys, and security findings. It does not know whether your product really needs the permission. That final decision stays with the engineer.

Implement the role with CDK

Use infrastructure as code so the reviewed policy is versioned. This lib/image-worker-iam-stack.ts example creates the log group and Lambda execution role.

import * as cdk from "aws-cdk-lib";
import { Stack, StackProps, aws_iam as iam, aws_logs as logs } from "aws-cdk-lib";
import { Construct } from "constructs";

export class ImageWorkerIamStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const account = Stack.of(this).account;
    const region = Stack.of(this).region;

    new logs.LogGroup(this, "ImageWorkerLogGroup", {
      logGroupName: "/aws/lambda/image-worker-prod",
      retention: logs.RetentionDays.ONE_MONTH,
      removalPolicy: cdk.RemovalPolicy.RETAIN,
    });

    const role = new iam.Role(this, "ImageWorkerRole", {
      assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
      description: "Execution role for image-worker-prod Lambda",
    });

    role.addToPolicy(new iam.PolicyStatement({
      sid: "ReadSourceImages",
      actions: ["s3:GetObject"],
      resources: ["arn:aws:s3:::user-uploads-prod/incoming/*"],
    }));

    role.addToPolicy(new iam.PolicyStatement({
      sid: "WriteThumbnails",
      actions: ["s3:PutObject"],
      resources: ["arn:aws:s3:::user-thumbnails-prod/thumbnails/*"],
      conditions: {
        StringEquals: {
          "s3:x-amz-server-side-encryption": "AES256",
        },
      },
    }));

    role.addToPolicy(new iam.PolicyStatement({
      sid: "WriteMetadataAndAlerts",
      actions: ["dynamodb:PutItem", "dynamodb:UpdateItem", "sns:Publish"],
      resources: [
        `arn:aws:dynamodb:${region}:${account}:table/image-metadata`,
        `arn:aws:sns:${region}:${account}:alert-topic`,
      ],
    }));

    role.addToPolicy(new iam.PolicyStatement({
      sid: "WriteLambdaLogs",
      actions: ["logs:CreateLogStream", "logs:PutLogEvents"],
      resources: [
        `arn:aws:logs:${region}:${account}:log-group:/aws/lambda/image-worker-prod:*`,
      ],
    }));
  }
}

Use OIDC for CI, not long-term keys

For CI/CD, avoid storing long-term AWS access keys in repository secrets. AWS guidance favors temporary credentials, and GitHub Actions can assume an IAM role through OIDC.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          "token.actions.githubusercontent.com:sub": "repo:example-org/example-repo:ref:refs/heads/main"
        }
      }
    }
  ]
}

This is a trust policy: it decides who can assume the role. The permissions policy decides what the role can do after it is assumed. Review those two documents separately.

Pitfalls to catch in review

The first pitfall is temporary admin access that never gets removed. If emergency access is unavoidable, create a ticket with an owner, expiration date, CloudTrail check, and follow-up Access Analyzer run.

The second is mixing S3 bucket and object ARNs. s3:ListBucket uses arn:aws:s3:::bucket-name; object actions such as s3:GetObject and s3:PutObject use arn:aws:s3:::bucket-name/prefix/*.

The third is adding IP conditions to workload roles without understanding service calls. aws:SourceIp can make sense for human or public API requests, but it can break AWS service-to-service flows.

The fourth is testing only success. Also test that s3:DeleteObject, unrelated buckets, unrelated DynamoDB tables, and IAM actions are denied.

For adjacent implementation details, read the AWS Lambda guide, AWS S3 guide, AWS CloudWatch guide, and Claude Code security best practices.

If your team wants a repeatable Claude Code and AWS permission review process, the training and consultation page is the practical next step. Solo readers can start with the free cheat sheet and reusable materials on the products page.

I tested this workflow by drafting the policy with Claude Code, validating the JSON, and reviewing the generated CDK against the same use case table. The biggest improvement came from writing the forbidden operations first: broad S3 permissions and unnecessary log wildcards were easier to catch before deployment.

#claude-code #aws #iam #security #typescript #infrastructure
Free

Free PDF: Claude Code Cheatsheet

Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.

We handle your data with care and never send spam.

Level up your Claude Code workflow

Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.

Masa

About the Author

Masa

Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.