Skip to content

How to guide AI agents to use your IAM permission boundaries

7 minute read
Content level: Advanced
0

Provide AI agents with specific organizational context so that they can write infrastructure as code for IAM resources in the same way your organization does it.

AWS customers with multi-account strategy make use of permission boundaries when they provision developer roles and automation roles for individual AWS accounts. This is a great way to reduce permissions and limit exposure to privilege escalation. IAM permission boundaries, created by the platform teams, are enforced during the creation and modification of IAM roles with IAM conditions in policy.

When attached to the developer and CI/CD roles, the following IAM policy (PlatformPermissionBoundaryPolicy) helps ensure that the developers can create, modify, and update roles, but only when the permissions boundary policy is attached to the new role.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "EnforceIAMRoleActionsHaveBoundary",
      "Effect": "Deny",
      "Action": [
        "iam:AttachRolePolicy",
        "iam:CreateRole",
        "iam:DetachRolePolicy",
        "iam:PutRolePolicy",
        "iam:DeleteRolePolicy",
        "iam:PutRolePermissionsBoundary"
      ],
      "Resource": "*",
      "Condition": {
        "StringNotLike": {
          "iam:PermissionsBoundary": "arn:aws:iam::*:policy/PlatformPermissionBoundaryPolicy"
        }
      }
    },
    {
      "Sid": "DenyChangesToBoundaryPolicy",
      "Effect": "Deny",
      "Action": [
        "iam:DeletePolicy",
        "iam:CreatePolicyVersion",
        "iam:CreatePolicy",
        "iam:DeletePolicyVersion",
        "iam:SetDefaultPolicyVersion"
      ],
      "Resource": "arn:aws:iam::*:policy/PlatformPermissionBoundaryPolicy"
    }
  ]
}

This IAM best practice poses some challenges for developers using AI coding agents. Most public AWS infrastructure as code samples don't use permission boundaries this way, so when developers want to use out of the box code samples, or use AI agents trained on those samples, they often face permission errors.

Developers can use agent skills specification to teach their coding agents how to work with IAM roles in their organization which can save hours of debugging effort, and results in code that conforms to their organizational patterns.

How to create the skill

Agent skills specification follows a directory structure which contains a SKILL.md file with YAML front-matter for the skill to help AI agents decide when to activate this skill, followed by the markdown content. In this case the SKILL.md is the only file for this skill.

Create the file .skills/aws-iam-permission-boundary/SKILL.md with the following content:

---
name: aws-iam-permission-boundary
description: >
    Enforces organization permission boundary policies on all IAM roles and users created through AWS infrastructure as code. Applies to CloudFormation, CDK, SAM, and Terraform. Use this skill whenever creating, modifying, or reviewing IAM resources to ensure every role includes the required permission boundary.
compatibility: >
    Requires access to file-system read/write tools. Works with any IDE agent that can edit CloudFormation YAML/JSON, CDK TypeScript/Python, SAM YAML, or Terraform HCL files.
metadata:
	category: security
	cloud: aws

---
# AWS IAM Permission Boundary Skill
## Purpose

Ensure every IAM role or user created through infrastructure as code includes the organization's permission boundary policy. Omitting the boundary will cause deployment failures and policy violations.

## Permission Boundary ARN Pattern

All IAM roles and users MUST attach this permission boundary:

```
arn:aws:iam::<ACCOUNT_ID>:policy/PlatformPermissionBoundaryPolicy
```

Where:

- `<ACCOUNT_ID>` is the AWS account ID (resolved at deploy time)

## Rules

1. Every `AWS::IAM::Role`, `AWS::IAM::User`, `aws_iam_role`, or `aws_iam_user` resource MUST have a `PermissionsBoundary` (CloudFormation/SAM/CDK) or `permissions_boundary` (Terraform) property set.
2. The boundary ARN must follow the pattern above exactly.
3. For constructs that create IAM roles implicitly (SAM `AWS::Serverless::Function`, `AWS::Serverless::StateMachine`, CDK L2/L3 constructs like `lambda.Function`), the boundary MUST be applied via `Globals` (SAM) or at the Stack/Stage level (CDK).
4. In Terraform, use a shared module or policy-as-code tool to enforce boundaries since there is no provider-level default.

## How to Apply

When you encounter or generate IAM resources, check for the permission boundary. If missing, add it. If present, verify it matches the pattern. Flag any hardcoded account names or IDs.

## Examples

### CloudFormation / SAM

```yaml
Resources:
  MyRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: my-role
      PermissionsBoundary: !Sub 'arn:aws:iam::${AWS::AccountId}:policy/PlatformPermissionBoundaryPolicy'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole

```

### AWS CDK (TypeScript)

```typescript
import * as cdk from 'aws-cdk-lib';
import * as iam from 'aws-cdk-lib/aws-iam';


const boundary = iam.ManagedPolicy.fromManagedPolicyArn(
  stack,
  'PermissionBoundary',
  `arn:aws:iam::${cdk.Aws.ACCOUNT_ID}:policy/PlatformPermissionBoundaryPolicy`,
);

const role = new iam.Role(stack, 'MyRole', {
  roleName: `my-role`,
  assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
  permissionsBoundary: boundary,
});

```

### AWS CDK (Python)

```python
from aws_cdk import (
    CfnParameter, Tags, Aws,
    aws_iam as iam,
)

boundary = iam.ManagedPolicy.from_managed_policy_arn(
    stack,
    "PermissionBoundary",
    f"arn:aws:iam::{Aws.ACCOUNT_ID}:policy/PlatformPermissionBoundaryPolicy",
)

role = iam.Role(
    stack,
    "MyRole",
    role_name=f"my-role",
    assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"),
    permissions_boundary=boundary,
)

```

### Terraform

```hcl

data "aws_caller_identity" "current" {}

resource "aws_iam_role" "my_role" {
  name                 = "my-role"
  permissions_boundary = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/PlatformPermissionBoundaryPolicy"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { Service = "lambda.amazonaws.com" }
      Action    = "sts:AssumeRole"
    }]
  })
}
```

## Implicit IAM Roles (Stack-Level Boundaries)

Some IaC constructs create IAM roles automatically without the user declaring an `AWS::IAM::Role` resource. You MUST still ensure the permission boundary is applied.

### SAM — `Globals` Section

`AWS::Serverless::Function` and `AWS::Serverless::StateMachine` both create execution roles implicitly. Use the `Globals` section to apply the boundary to every function and state machine in the template at once:

```yaml
Globals:
  Function:
    PermissionsBoundary: !Sub 'arn:aws:iam::${AWS::AccountId}:policy/PlatformPermissionBoundaryPolicy'
  StateMachine:
    PermissionsBoundary: !Sub 'arn:aws:iam::${AWS::AccountId}:policy/PlatformPermissionBoundaryPolicy'

Resources:
  MyFunction:
    Type: AWS::Serverless::Function
    Properties:
      Runtime: python3.12
      Handler: app.handler
      CodeUri: ./src
      # No explicit Role — SAM creates one with the boundary from Globals
```

You can also set `PermissionsBoundary` on individual `AWS::Serverless::Function` or `AWS::Serverless::StateMachine` resources. This only works when SAM generates the role (i.e. you have NOT set the `Role` property).

### CDK — `PermissionsBoundary` on Stack or Stage

CDK L2/L3 constructs (e.g. `lambda.Function`, `stepfunctions.StateMachine`) create IAM roles internally. Use `PermissionsBoundary` at the Stack or Stage level to automatically apply the boundary to ALL roles and users created within that scope:

TypeScript:

```typescript
import { App, Stack, PermissionsBoundary } from 'aws-cdk-lib';

const app = new App();

// Apply to an entire Stage (recommended)
const prodStage = new Stage(app, 'Prod', {
  permissionsBoundary: PermissionsBoundary.fromName('PlatformPermissionBoundaryPolicy'),
});

// Or apply to a single Stack
const stack = new Stack(app, 'MyStack', {
  permissionsBoundary: PermissionsBoundary.fromName('PlatformPermissionBoundaryPolicy'),
});
```

Python:

```python
from aws_cdk import App, Stack, Stage, PermissionsBoundary

app = App()

# Apply to an entire Stage (recommended)
prod_stage = Stage(app, "Prod",
    permissions_boundary=PermissionsBoundary.from_name("PlatformPermissionBoundaryPolicy"),
)

# Or apply to a single Stack
stack = Stack(app, "MyStack",
    permissions_boundary=PermissionsBoundary.from_name("PlatformPermissionBoundaryPolicy"),
)
```

`PermissionsBoundary.fromName(name)` builds the full ARN using the stack's account. Use `PermissionsBoundary.fromArn(arn)` if you need to specify the full ARN explicitly. The ARN supports placeholders: `${AWS::Partition}`, `${AWS::Region}`, `${AWS::AccountId}`, `${Qualifier}`.

### Terraform — No Provider-Level Equivalent

Terraform does not have a provider-level setting to auto-apply permission boundaries. Every `aws_iam_role` and `aws_iam_user` resource must explicitly include `permissions_boundary`. Use a shared Terraform module to enforce this consistently:

```hcl
# modules/iam-role/main.tf
variable "name" { type = string }
variable "assume_role_policy" { type = string }
variable "permissions_boundary_arn" {
  type    = string
  default = null
}

data "aws_caller_identity" "current" {}

locals {
  boundary = coalesce(
    var.permissions_boundary_arn,
    "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/PlatformPermissionBoundaryPolicy"
  )
}

resource "aws_iam_role" "this" {
  name                 = var.name
  assume_role_policy   = var.assume_role_policy
  permissions_boundary = local.boundary
}
```

Alternatively, use an OPA/Sentinel policy or a `tflint` rule to detect any `aws_iam_role` missing `permissions_boundary`.

## Common Mistakes to Catch

- IAM role or user created without any `PermissionsBoundary` / `permissions_boundary`
- Boundary ARN with a hardcoded account ID instead of a dynamic reference
- SAM template with `AWS::Serverless::Function` but no `PermissionsBoundary` in `Globals` or on the resource itself
- CDK stack creating L2/L3 constructs (e.g. `lambda.Function`) without `permissionsBoundary` set on the Stack or Stage
- Terraform module creating `aws_iam_role` without `permissions_boundary`

Conclusion

The example skill helps agents discover how to utilize permission boundaries when they are writing infrastructure code that creates or modifies IAM roles. This skill is focused on permissions boundary implementation across IaC tools. You can modify the policy naming to match your setup. You can also simplify to only include examples for the IaC tools within your organization. You can also extend the skill to include other patterns in the the organization such as resource naming conventions and required tagging strategy.