Back to Blog
June 18, 2026

Catching Risky IAM in CloudFormation Templates Before You Deploy

Most IAM security conversations center on the AWS console or raw JSON policy documents. But a significant share of production IAM misconfigurations originates somewhere else entirely: CloudFormation templates. By the time a CF stack is deployed, the IAM roles and policies inside it are live — and unless you reviewed the template before running cdk deploy or aws cloudformation create-stack, you may not have noticed what you just granted.

Why CloudFormation IAM Risk Is Different

With a standalone IAM policy, the permissions are obvious — the JSON is right there. In a CloudFormation template, IAM roles are buried alongside compute resources, storage buckets, and networking config. Reviewers focused on the overall architecture often skim past the Properties.Policies block on a Lambda execution role without reading every action and resource it grants.

There are also compounding effects unique to infrastructure-as-code. A single CloudFormation template may create five roles, two managed policy attachments, and three inline policies — all in a single deploy. The blast radius of an overly-permissive role is multiplied by every resource that role is attached to. And because CF stacks can be parameterized, the effective permissions may differ between environments in ways that are hard to spot from the template alone.

The Most Common CF IAM Mistakes

These four patterns account for the majority of CloudFormation IAM findings in practice:

  • AdministratorAccess on Lambda execution roles. It is tempting to attach the AWS-managed AdministratorAccess policy to a Lambda function role during development so it can reach any service it needs. That policy grants Action: * on Resource: *. If the template ships to production unchanged, the function — and anyone who can invoke it — effectively holds root-level access.
  • Missing permission boundaries on auto-created roles. CloudFormation stacks that create IAM roles on behalf of other services (such as CodePipeline, ECS tasks, or Step Functions state machines) often omit permission boundaries. Without a boundary, any policy attached to that role later is unconstrained, even if an organizational SCP was supposed to cap the maximum permissions.
  • Action: * in inline policies. Inline policies defined inside a AWS::IAM::Roleresource are easy to overlook during review. A wildcard action block in an inline policy grants the same scope as a standalone admin policy — but it is harder to audit because it does not appear in the IAM console's managed policy list.
  • Wildcard Resource on DynamoDB and S3. Scoping actions correctly but leaving Resource: "*" means those actions apply to every table or every bucket in the account. A Lambda that needs to write to one DynamoDB table can, if misconfigured this way, read or write any table — including tables owned by other workloads or containing sensitive data.

How CDK Makes It Easier to Slip Up

AWS CDK abstracts CloudFormation into higher-level constructs, which is powerful — but it also makes IAM risk less visible. When you call a convenience method like table.grantReadWriteData(fn) or bucket.grantRead(role), CDK generates IAM policy statements on your behalf. Those generated statements are correct for the named resource, but the output CloudFormation template is what actually gets deployed, and that template may contain more than you expect.

CDK also provides escape hatches — addToRolePolicy, addOverride, raw PolicyStatement objects — that let developers bypass the guardrails entirely. A single escape-hatch call deep in a construct can inject an Action: * statement into an otherwise well-scoped role, and nothing in the CDK build output will flag it. The risk lands in the synthesized CloudFormation template, not in the TypeScript or JavaScript CDK code.

Catching It Before Deploy

The safest place to catch CloudFormation IAM risk is before the stack is created or updated, not after. There are two complementary approaches:

  • Static analysis of the template. Analyze the raw CloudFormation YAML or JSON — either the authored template or the CDK-synthesized output from cdk synth — before any deploy command runs. This catches wildcards, missing boundaries, and dangerous managed policy attachments at the source, with no AWS account access required.
  • Policy review gates in CI. Add a template analysis step to your CI pipeline that runs on every pull request touching infrastructure files. Fail the pipeline if a critical IAM finding is present. This creates a hard gate: a CloudFormation stack with an AdministratorAccess attachment or an unconstrained wildcard cannot reach a deploy stage without an explicit review.

Both approaches rely on the same input: the CloudFormation template file. For CDK projects, the synthesized templates live in cdk.out/ after running cdk synth, and those files are the most accurate representation of what will be deployed.

What Safe CF IAM Looks Like

Here is a CloudFormation IAM role definition that is typical of a risky first draft — broad actions, wildcard resource, and no permission boundary:

# Dangerous: overly-permissive Lambda execution role
LambdaExecutionRole:
  Type: AWS::IAM::Role
  Properties:
    AssumeRolePolicyDocument:
      Version: "2012-10-17"
      Statement:
        - Effect: Allow
          Principal:
            Service: lambda.amazonaws.com
          Action: sts:AssumeRole
    Policies:
      - PolicyName: AllAccess
        PolicyDocument:
          Version: "2012-10-17"
          Statement:
            - Effect: Allow
              Action: "*"
              Resource: "*"

The scoped version restricts the role to exactly the operations the function requires, names the specific resources it may access, and attaches a permission boundary so that even if additional policies are attached later, the effective permissions cannot exceed a defined maximum:

# Safe: scoped Lambda execution role with permission boundary
LambdaExecutionRole:
  Type: AWS::IAM::Role
  Properties:
    PermissionsBoundary: !Sub "arn:aws:iam::${AWS::AccountId}:policy/AppBoundary"
    AssumeRolePolicyDocument:
      Version: "2012-10-17"
      Statement:
        - Effect: Allow
          Principal:
            Service: lambda.amazonaws.com
          Action: sts:AssumeRole
    Policies:
      - PolicyName: OrdersTableAccess
        PolicyDocument:
          Version: "2012-10-17"
          Statement:
            - Effect: Allow
              Action:
                - dynamodb:GetItem
                - dynamodb:PutItem
                - dynamodb:UpdateItem
                - dynamodb:Query
              Resource:
                - !Sub "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/orders"
                - !Sub "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/orders/index/*"
            - Effect: Allow
              Action:
                - logs:CreateLogGroup
                - logs:CreateLogStream
                - logs:PutLogEvents
              Resource: !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*"

The difference between the two is concrete: the first role grants full account access to any identity that can invoke the function; the second grants write access to one DynamoDB table and log access to one log group prefix — nothing else.

Applying this pattern consistently across a CloudFormation template takes time, especially in large stacks. The practical approach is to start with a tool that reads the template and flags every statement that falls short of least-privilege, then work through the findings systematically before the next deploy.

Analyze CloudFormation IAM before every deploy

Upload a CloudFormation template and get AI-Powered security analysis in seconds — free, no credit card.

Amazon Web Services (AWS) is a trademark of Amazon.com, Inc. Shieldly is not affiliated with, endorsed by, or sponsored by Amazon Web Services.