Managing IAM (Identity and Access Management) policies securely is one of the most important parts of working with AWS. Developers may accidentally create overly-permissive policies that grant more access than necessary — for example, allowing iam:PassRole to all roles, or opening up sts:AssumeRole without restriction. Without proper checks in place, these risky permissions can silently make their way into your production environment.

In a organizations with multiple accounts, the impact of such mistakes can multiply. That’s why having strong guardrails like IAM Access Analyzer become critical — ensuring that only safe and intentional access is allowed.

🛡️ Before we go further, it's important to understand that AWS Service Control Policies (SCPs) and Resource Control Policies (RCPs) are the first line of defence in any AWS multi-account setup. They define the maximum permissions any user or role can have, regardless of their IAM policies. This blog will not cover SCPs and RCPs, we’ll focus on validating IAM policies during development to ensure we follow least privilege and catch access issues early.

In this blog, we’ll explore how to bring IAM policy validation into CI/CD pipeline using AWS IAM Access Analyzer. This is an example of shift-left security — catching misconfigurations as early as possible, before anything gets merged or deployed.

We'll walk through:

  • Setting up a GitHub Actions workflow that validates IAM policies.
  • Using IAM Access Analyzer checks.
  • Building CloudFormation templates that intentionally pass and fail to understand how policy validation works in practice.

IAM Access Analyzer

IAM Access Analyzer is a security feature in AWS that helps ensure your IAM policies follow best practices and don’t grant unintended access. It supports several capabilities like:

  • Validating IAM policies against AWS best practices or security standards.
  • Identifying unused permissions
  • Analyzing external sharing of AWS resources
  • Detecting public access to resources.
  • Generating policies based on CloudTrail access logs

In this blog, we’ll focus specifically on Custom policy checks, which help to validate IAM policies against your organizations security standards — like least privilege or restricted actions. IAM Access Analyzer provides custom policy check APIs to validate IAM policies:

- CheckNoNewAccess – Detects if a policy introduces new permissions compared to a baseline.
- CheckAccessNotGranted – Ensures specific actions or access are not allowed.
- CheckNoPublicAccess – Identifies if a resource policy allows public access.

👉 IAM Access Analyzer custom policy check is a paid feature. As of now, AWS charges $0.0020 per API call for these checks. For example, you make 10,000 calls each month to the IAM Access Analyzer APIs to run custom policy checks across 5 accounts signed up for consolidated billing with AWS Organizations, cost will be $0.0020*10,000 API calls = $20 per month

Now let's start with playing around IAM Access Analyzer. Github code for this blog.

Pre-Requisite

  • AWS Account
  • Github Account

CI-CD Flow

github_action_workflow

  • Developer pushes changes to repo.
  • Github action performs IAM Access Analyzer validation.
  • If policy created by developer violates, workflow gets failed.
  • If policy created by developer is as per security standard, workflow proceeds with new checks and depployment.

Hands-On

Step 1 - Configure AWS and Github

In order to, configure Github action, we need to first integrate AWS Account and Github, basically we need to establish trust. To do that follow below steps:

If you already have github configured with AWS, you can skip this step.

  1. Login into AWS account, go to IAM → Identity providers →Add provider.
  2. Once done, create a role for GitHub. We need to set trust policy for role as below. You need to replace values as per your accounts.
    • Principal should be ARN of Identity provider you configured above.
    • For Condition, paste github repo link.
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::012345678910:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
          "token.actions.githubusercontent.com:sub": "repo:/:*"
        }
      }
    }
  ]
}

For this role, I have assigned below permission :

  • AmazonS3ReadOnlyAccess
  • IAMAccessAnalyzerReadOnlyAccess

Depending on your scenario, you can add more permission like creating IAM roles, policies.

Step 2 - Configure Github Secrets

While running Github action, we will be using role which get assumed to perform necessary actions. But instead of directly hardcoding role ARN we will be configuring it as secrete. For that

  • Go to your repo → Setting → Secrete & Variable → New repository secretes. Enter name for secrets and paste role ARN

github_secrets

We will be creating two secretes:

  • IAM Role : This will be used by Github action.
  • Reference policy Object S3 URL : This will be reference policy which we will be using for validation. As good practice, I will be storing this reference policy on restricted S3 bucket. Part of workflow, reference policy gets downloaded. You can create dedicated S3 bucket for this or use existing one, make sure github role have permission to access it.

Step 3 - Reference Policy

I will be using below reference policy. This reference policy allows all actions by default but explicitly denies a list of CloudFormation actions (like CreateStack, UpdateStack, DeleteStack, etc.) on stacks with names under platformteam/*. It’s designed to protect critical infrastructure owned by the platform team from unauthorized changes, even if general CloudFormation access is permitted elsewhere.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "*",
            "Resource": "*"
        },
        {
            "Effect": "Deny",
            "Action": [
                "cloudformation:CancelUpdateStack",
                "cloudformation:ContinueUpdateRollback",
                "cloudformation:CreateChangeSet",
                "cloudformation:CreateStack",
                "cloudformation:DeleteChangeSet",
                "cloudformation:DeleteStack",
                "cloudformation:DescribeChangeSet",
                "cloudformation:DescribeChangeSetHooks",
                "cloudformation:DescribeStackEvents",
                "cloudformation:DescribeStackResource",
                "cloudformation:DescribeStackResourceDrifts",
                "cloudformation:DescribeStackResources",
                "cloudformation:DescribeStacks",
                "cloudformation:DetectStackDrift",
                "cloudformation:DetectStackResourceDrift",
                "cloudformation:ExecuteChangeSet",
                "cloudformation:GetStackPolicy",
                "cloudformation:GetTemplate",
                "cloudformation:GetTemplateSummary",
                "cloudformation:ListChangeSets",
                "cloudformation:ListStackResources",
                "cloudformation:RecordHandlerProgress",
                "cloudformation:RollbackStack",
                "cloudformation:SetStackPolicy",
                "cloudformation:SignalResource",
                "cloudformation:TagResource",
                "cloudformation:UntagResource",
                "cloudformation:UpdateStack",
                "cloudformation:UpdateTerminationProtection"
            ],
            "Resource": "arn:aws:cloudformation:*:*:stack/platformteam/*"
        }
    ]
}

Step 4 - Github Workflow

With below Github workflow:

  • Runs on pull requests and pushes to the main branch
  • Assumes AWS IAM role using OIDC authentication
  • Downloads reference policies from S3 using GitHub secrets
  • Performs two IAM Access Analyzer checks:
    • ✅ CloudFormation Access Check – Ensures no new permissions are introduced (CHECK_NO_NEW_ACCESS)
    • ✅ Principal Check – Verifies only approved principals can assume the role (CHECK_NO_NEW_ACCESS on trust policy)
name: cfn-policy-validator-workflow
on:
  pull_request:
    types: [opened, review_requested]

  push:
    branches:
      - 'main'

permissions:
  id-token: write
  contents: read
  issues: write

jobs: 
  cfn-iam-policy-validation: 
    name: iam-policy-validation
    runs-on: ubuntu-latest
    permissions: write-all
    steps:
      - name: Checkout code
        id: checkOut
        uses: actions/checkout@v4

      - name: Configure AWS Credentials
        id: configureCreds
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.OIDC_IAM_ROLE }}
          aws-region: us-east-1
          role-session-name: GitHubSessionName

      - name: Fetch reference policy from s3
        id: getReferencePolicy
        run: |
          aws s3 cp ${{ secrets.REFERENCE_IDENTITY_POLICY_CLOUDFORMATION }} ./cloudformation-policy-reference.json
          aws s3 cp ${{ secrets.REFERENCE_IDENTITY_POLICY_PRINCIPAL }} ./allowlist-account-principal.json
        shell: bash

      - name: CloudFormation-Access-Checks
        id: run-aws-check-no-new-access
        uses: aws-actions/[email protected]
        with:
          policy-check-type: 'CHECK_NO_NEW_ACCESS'
          template-path: './cloudformation-sample/cfn-iam-pass-role-sample.yaml'
          reference-policy: './cloudformation-policy-reference.json'
          reference-policy-type: "IDENTITY"
          region: us-east-1
      - name: New-Principal-Checks
        id: run-aws-check-no-new-access-assumerole
        uses: aws-actions/[email protected]
        with:
          policy-check-type: 'CHECK_NO_NEW_ACCESS'
          template-path: './cloudformation-sample/cfn-iam-role-principal-sample.yaml'
          reference-policy: './allowlist-account-principal.json'
          reference-policy-type: "RESOURCE"
          region: us-east-1

CloudFormation Template - Failed

Now below template, grants CloudFormation permissions to all stacks (Resource: ''), violating the reference policy by potentially allowing changes to `platformteam/` stacks.

# lambda-pass-template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Resources:
  LambdaFriendlyRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: LambdaSafeCFRole
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: CFNonPlatformTeamAccess
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - cloudformation:CreateStack
                  - cloudformation:UpdateStack
                  - cloudformation:DeleteStack
                Resource: '*'

After pushing changes to main (ideally pushing to main is not best practice but for this blog only i'm doing), we will see github workflow gets failed as policy violates our reference. policy.
When a custom policy check fails, IAM Access Analyzer returns the statement ID (Sid) and statement index of the specific policy statement that caused the failure. The index is zero-based (starting from 0).

cfn_check_fail

cfn_check_fail

CloudFormation Template - Pass

With below template, a lambda function CloudFormation permissions scoped to devteam/* stacks, staying compliant with the reference policy by avoiding access to protected platformteam/* stacks.

# lambda-pass-template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Resources:
  LambdaFriendlyRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: LambdaSafeCFRole
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: CFNonPlatformTeamAccess
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - cloudformation:CreateStack
                  - cloudformation:UpdateStack
                  - cloudformation:DeleteStack
                Resource: arn:aws:cloudformation:us-east-1:*:stack/devteam/*

cfn_check_pass

I hope this example gave you an idea about implementation and how it works. If you would like play more on this, you can find reference repo.


Validating IAM policies early in the development process helps prevent security risks before they reach production. By integrating IAM Access Analyzer with GitHub Actions, you can enforce least privilege, block overly-permissive changes, and shift security left — all without slowing down development.

Thanks, cloud builder! Now go forth and validate those IAM policies like a pro. 🔐🛠️