Back to blog
AWSBest PracticesCloud SecurityIdentity & Access

IAM Group Has Inline Policies: Why It Matters and How to Fix It

Inline policies on IAM groups hide permissions and cause drift. Learn how to detect them, convert to managed policies, and prevent them with CI guardrails.

TL;DR

This check flags IAM groups that carry inline policies instead of managed policies. Inline policies are invisible at scale, hard to audit, and impossible to reuse, which leads to permission drift and missed privilege escalations. Move the permissions into a customer managed policy and attach that instead.

IAM is the part of AWS where small mistakes turn into big incidents. When you grant permissions through an IAM group, you expect those permissions to be visible, reviewable, and consistent across the accounts you manage. Inline policies break that expectation. They hide inside individual groups, never show up in your list of reusable policies, and quietly accumulate until nobody remembers what they grant.

The account_groupinlinepolicies check looks for exactly this: IAM groups that have one or more inline policies attached. Below is what it means, why it is worth fixing, and how to clean it up properly.


What this check detects

The check inspects every IAM group in the account and reports any group that has at least one inline policy. In IAM there are two ways to attach permissions to a group:

  • Managed policies are standalone objects with their own ARN. They can be attached to multiple groups, users, or roles, and AWS keeps version history for them.
  • Inline policies are embedded directly inside a single group (or user, or role). They have no ARN, no version history, and cannot be reused anywhere else.

If a group has any inline policy at all, this check fails for that group. A managed policy attached to the same group is fine and is in fact the recommended approach.

Note: Managed policies come in two flavors. AWS managed policies are created and maintained by AWS (like ReadOnlyAccess). Customer managed policies are ones you create in your own account. For group permissions you control, customer managed policies are usually the right tool.


Why it matters

Inline policies are not insecure by definition. The risk comes from how they behave operationally, especially once you have more than a handful of groups across more than one account.

They are invisible to your policy inventory

When a security engineer runs aws iam list-policies to audit what permissions exist, inline policies do not appear. They live inside the group object and only surface if you specifically query that group. In a large account it is easy to miss a group that quietly grants iam:PassRole or s3:* through an inline document nobody knew about.

They cannot be reused, so they drift

If three groups need the same set of permissions and you use inline policies, you copy the JSON three times. The day someone tightens one of them, the other two fall out of sync. With a single managed policy attached to all three groups, one change updates everything. Inline policies guarantee drift over time.

No version history means no clean rollback

Customer managed policies keep up to five versions. If a change breaks something, you set the previous version as default and you are back to a known good state in seconds. Inline policies have no versions. The only record of the previous state is whatever you happened to save outside AWS.

Warning: Inline policies are deleted along with the group they belong to. If someone removes the group, the policy is gone with no trace and no easy way to recover what it granted. A managed policy survives because it is a separate object.

The realistic attack scenario

An attacker who gains access to an account often starts by mapping permissions. Defenders rely on managed policy inventories and IAM Access Analyzer findings to spot over-permissioned principals. An inline policy on a low-profile group, say a contractor group that picked up iam:CreatePolicyVersion or sts:AssumeRole on a broad resource, is exactly the kind of thing that slips through a routine review. The attacker finds it because they look at the group directly. Your audit tooling did not, because it looked at the policy list.


How to fix it

The fix is to convert each inline policy into a customer managed policy, attach the managed policy to the group, and then delete the inline policy. Here is the full sequence.

Step 1: Find the inline policies on the group

aws iam list-group-policies --group-name Developers

This returns the names of inline policies attached to the group, for example:

{
  "PolicyNames": [
    "DeveloperS3Access"
  ]
}

Step 2: Export the inline policy document

aws iam get-group-policy \
  --group-name Developers \
  --policy-name DeveloperS3Access \
  --query 'PolicyDocument' \
  --output json > developer-s3-access.json

Open developer-s3-access.json and confirm the document looks correct. This is a good moment to review whether the permissions are still appropriate rather than copying them forward blindly.

Step 3: Create a customer managed policy from the document

aws iam create-policy \
  --policy-name DeveloperS3Access \
  --policy-document file://developer-s3-access.json \
  --description "S3 access for the Developers group, migrated from inline"

Note the ARN returned in the response. You will need it in the next step.

Step 4: Attach the managed policy to the group

aws iam attach-group-policy \
  --group-name Developers \
  --policy-arn arn:aws:iam::123456789012:policy/DeveloperS3Access

Step 5: Delete the inline policy

Danger: This permanently removes the inline policy. Only run it after you have confirmed the managed policy is attached and grants the same permissions. Members of the group will lose any access that exists only in the inline document.

aws iam delete-group-policy \
  --group-name Developers \
  --policy-name DeveloperS3Access

Step 6: Verify

# Should now return an empty PolicyNames list
aws iam list-group-policies --group-name Developers

# Should show the new managed policy
aws iam list-attached-group-policies --group-name Developers

Tip: Before deleting anything, test the effective permissions with aws iam simulate-principal-policy against a user in the group. It tells you whether a specific action is still allowed once the managed policy is in place, so you can confirm parity before removing the inline copy.


Fixing it with Terraform

If you manage IAM in Terraform, the inline policy was probably defined with an aws_iam_group_policy resource. Replace it with a standalone aws_iam_policy and an aws_iam_group_policy_attachment.

Before:

resource "aws_iam_group_policy" "developer_s3" {
  name  = "DeveloperS3Access"
  group = aws_iam_group.developers.name

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect   = "Allow"
      Action   = ["s3:GetObject", "s3:PutObject"]
      Resource = "arn:aws:s3:::my-app-bucket/*"
    }]
  })
}

After:

resource "aws_iam_policy" "developer_s3" {
  name        = "DeveloperS3Access"
  description = "S3 access for the Developers group"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect   = "Allow"
      Action   = ["s3:GetObject", "s3:PutObject"]
      Resource = "arn:aws:s3:::my-app-bucket/*"
    }]
  })
}

resource "aws_iam_group_policy_attachment" "developer_s3" {
  group      = aws_iam_group.developers.name
  policy_arn = aws_iam_policy.developer_s3.arn
}

Warning: When you remove the aws_iam_group_policy resource, Terraform deletes the inline policy. Apply during a maintenance window or confirm the new attachment lands in the same plan so there is no gap where the group has no permissions.


How to prevent it from happening again

Cleaning up the existing groups is half the job. The other half is making sure new inline policies do not creep back in.

Lint Terraform in CI with Checkov

Checkov ships a check (CKV_AWS_40 and related rules) that flags inline IAM policies. Add it as a required step in your pipeline:

checkov -d . --framework terraform

Fail the build if the scan reports inline policy findings. This stops the pattern at the pull request, before it reaches AWS.

Use a custom Config rule or SCP guardrail

For a runtime backstop, AWS Config can evaluate IAM groups on a schedule and report any with inline policies. You can pair that with an automated remediation or simply a notification to the security team. If you want to block the action outright, a service control policy can deny iam:PutGroupPolicy in accounts where inline policies are never allowed.

{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "DenyInlineGroupPolicies",
    "Effect": "Deny",
    "Action": "iam:PutGroupPolicy",
    "Resource": "*"
  }]
}

Tip: Run the Lensix account_groupinlinepolicies check on a schedule across every account in your organization. A periodic scan catches inline policies that were created by hand in the console, which is the most common way they sneak past IaC pipelines.


Best practices

  • Default to customer managed policies. Use them for any permission set you author yourself. Reserve AWS managed policies for broad, well-understood baselines.
  • Attach permissions to groups, not users. Groups are the right place to manage permissions for people. Just make sure those permissions come from managed policies.
  • Keep policies small and named for intent. A policy called DeveloperS3Access tells a reviewer exactly what it is for. A grab-bag inline document does not.
  • Grant least privilege. Use IAM Access Analyzer to generate policies based on real CloudTrail activity instead of starting with wildcards.
  • Review attachments regularly. Run list-attached-group-policies across your groups during access reviews. Because everything is a managed policy, the review is fast and complete.
  • Audit roles and users too. Inline policies are equally problematic on IAM users and roles. The same conversion process applies, so extend the cleanup beyond groups.

Inline policies feel convenient in the moment because they live right next to the thing they apply to. That convenience is exactly what makes them dangerous at scale. Move them to managed policies, gate the pattern in CI, and scan for regressions, and your IAM permissions stay visible, reviewable, and easy to keep tight.