IAM Users Should Not Have Direct Policy Attachments
Overview
This check ensures that IAM users do not have policies attached directly to them. Instead, permissions should be assigned through IAM groups or roles. This makes access management simpler, safer, and easier to audit.
Risk
When policies are attached directly to individual users:
- Hard to manage: As your team grows, tracking who has what access becomes difficult
- Security exposure: If a user account is compromised, directly attached permissions give attackers immediate access to resources
- Compliance issues: Auditors expect centralized permission management; direct user policies make this harder
- Difficult cleanup: Removing access requires checking each user individually instead of updating a single group
Remediation Steps
Prerequisites
- Access to the AWS Console with IAM permissions, OR
- AWS CLI configured with appropriate credentials
Setting up AWS CLI (if needed)
If you don't have the AWS CLI installed:
- Download from https://aws.amazon.com/cli/
- Run
aws configureand enter your credentials - Set the default region to
us-east-1
AWS Console Method
Step 1: Find the affected user
- Sign in to the AWS Console
- Go to IAM (search for "IAM" in the top search bar)
- Click Users in the left sidebar
- Click on the user name that failed the check
Step 2: Review current permissions
Before removing anything, note what permissions the user currently has:
- On the user's page, click the Permissions tab
- Write down or screenshot the policies listed under:
- Permissions policies (managed policies)
- Inline policies (if any)
Step 3: Create or choose a group for these permissions
- In the left sidebar, click User groups
- Either:
- Click an existing group that should have these permissions, OR
- Click Create group to make a new one
- If creating a new group:
- Give it a descriptive name (e.g.,
developers,read-only-users) - Attach the same policies the user previously had
- Click Create group
- Give it a descriptive name (e.g.,
Step 4: Add the user to the group
- Click on the group name
- Click the Users tab
- Click Add users
- Select the affected user and click Add users
Step 5: Remove direct policies from the user
- Go back to Users and click on the affected user
- Under Permissions policies:
- For each managed policy: Click the policy, then click Remove
- Under Inline policies (if any):
- Click the policy name, then click Delete
AWS CLI (optional)
List current policies attached to the user
# List managed policies attached directly to the user
aws iam list-attached-user-policies \
--user-name <your-username> \
--region us-east-1
# List inline policies embedded in the user
aws iam list-user-policies \
--user-name <your-username> \
--region us-east-1
Remove managed policies from the user
# Detach each managed policy (repeat for each policy ARN)
aws iam detach-user-policy \
--user-name <your-username> \
--policy-arn <policy-arn> \
--region us-east-1
Remove inline policies from the user
# Delete each inline policy (repeat for each policy name)
aws iam delete-user-policy \
--user-name <your-username> \
--policy-name <policy-name> \
--region us-east-1
Add user to a group (so they keep needed permissions)
# Add user to an existing group
aws iam add-user-to-group \
--user-name <your-username> \
--group-name <group-name> \
--region us-east-1
Complete script to migrate a user's policies to a group
#!/bin/bash
USER_NAME="<your-username>"
GROUP_NAME="<target-group-name>"
# Get attached managed policies
POLICIES=$(aws iam list-attached-user-policies \
--user-name "$USER_NAME" \
--query 'AttachedPolicies[].PolicyArn' \
--output text \
--region us-east-1)
# Attach each policy to the group, then detach from user
for POLICY_ARN in $POLICIES; do
echo "Moving policy: $POLICY_ARN"
aws iam attach-group-policy \
--group-name "$GROUP_NAME" \
--policy-arn "$POLICY_ARN" \
--region us-east-1
aws iam detach-user-policy \
--user-name "$USER_NAME" \
--policy-arn "$POLICY_ARN" \
--region us-east-1
done
# Get inline policies
INLINE_POLICIES=$(aws iam list-user-policies \
--user-name "$USER_NAME" \
--query 'PolicyNames[]' \
--output text \
--region us-east-1)
# For inline policies, get the document and recreate on the group
for POLICY_NAME in $INLINE_POLICIES; do
echo "Moving inline policy: $POLICY_NAME"
POLICY_DOC=$(aws iam get-user-policy \
--user-name "$USER_NAME" \
--policy-name "$POLICY_NAME" \
--query 'PolicyDocument' \
--output json \
--region us-east-1)
aws iam put-group-policy \
--group-name "$GROUP_NAME" \
--policy-name "$POLICY_NAME" \
--policy-document "$POLICY_DOC" \
--region us-east-1
aws iam delete-user-policy \
--user-name "$USER_NAME" \
--policy-name "$POLICY_NAME" \
--region us-east-1
done
# Add user to the group
aws iam add-user-to-group \
--user-name "$USER_NAME" \
--group-name "$GROUP_NAME" \
--region us-east-1
echo "Migration complete!"
CloudFormation (optional)
This template creates an IAM user that inherits permissions from a group rather than having direct policy attachments.
AWSTemplateFormatVersion: '2010-09-09'
Description: IAM User with permissions via group membership only (no direct policies)
Parameters:
UserName:
Type: String
Description: Name for the IAM user
Default: example-user
Resources:
# IAM Group that will hold the permissions
ExampleGroup:
Type: AWS::IAM::Group
Properties:
GroupName: !Sub '${UserName}-group'
ManagedPolicyArns:
- arn:aws:iam::aws:policy/ReadOnlyAccess
# IAM User with NO direct policies attached
ExampleUser:
Type: AWS::IAM::User
Properties:
UserName: !Ref UserName
Groups:
- !Ref ExampleGroup
# Note: ManagedPolicyArns and Policies are intentionally omitted
# to ensure no policies are attached directly to the user
Outputs:
UserArn:
Description: ARN of the created IAM user
Value: !GetAtt ExampleUser.Arn
GroupArn:
Description: ARN of the IAM group
Value: !GetAtt ExampleGroup.Arn
Key points:
- The
AWS::IAM::Userresource does NOT includeManagedPolicyArnsorPoliciesproperties - Permissions are attached to the
AWS::IAM::Groupresource instead - The user is added to the group via the
Groupsproperty
Terraform (optional)
This configuration creates an IAM user that inherits permissions from a group.
# IAM User with permissions via group membership only (no direct policies)
# This configuration ensures compliance with iam_policy_attached_only_to_group_or_roles
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
variable "user_name" {
description = "Name for the IAM user"
type = string
default = "example-user"
}
# IAM Group that will hold the permissions
resource "aws_iam_group" "example" {
name = "${var.user_name}-group"
}
# Attach managed policy to the GROUP (not the user)
resource "aws_iam_group_policy_attachment" "example" {
group = aws_iam_group.example.name
policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
}
# IAM User with NO direct policies
resource "aws_iam_user" "example" {
name = var.user_name
}
# Add user to group (user inherits permissions from group)
resource "aws_iam_user_group_membership" "example" {
user = aws_iam_user.example.name
groups = [aws_iam_group.example.name]
}
# NOTE: Do NOT use these resources - they attach policies directly to users:
# - aws_iam_user_policy (inline policies)
# - aws_iam_user_policy_attachment (managed policies)
output "user_arn" {
description = "ARN of the created IAM user"
value = aws_iam_user.example.arn
}
output "group_arn" {
description = "ARN of the IAM group"
value = aws_iam_group.example.arn
}
Resources to avoid:
aws_iam_user_policy- attaches inline policies directly to usersaws_iam_user_policy_attachment- attaches managed policies directly to users
Use instead:
aws_iam_group_policy- inline policies on groupsaws_iam_group_policy_attachment- managed policies on groupsaws_iam_role_policy_attachment- for service/application access
Verification
After completing the remediation:
- Go to IAM > Users > click on the user
- Check the Permissions tab
- Confirm that:
- No policies appear under "Permissions policies" (direct attachments)
- No policies appear under "Inline policies"
- The user appears in at least one group (visible in the "Groups" section)
Verify with AWS CLI
# Check for managed policies (should return empty)
aws iam list-attached-user-policies \
--user-name <your-username> \
--region us-east-1
# Check for inline policies (should return empty)
aws iam list-user-policies \
--user-name <your-username> \
--region us-east-1
# Verify user is in a group
aws iam list-groups-for-user \
--user-name <your-username> \
--region us-east-1
Expected results:
list-attached-user-policiesreturns"AttachedPolicies": []list-user-policiesreturns"PolicyNames": []list-groups-for-userreturns at least one group
Additional Resources
- AWS IAM Best Practices
- IAM User Groups
- Managed Policies vs Inline Policies
- AWS Security Hub IAM Controls
Notes
- Don't lock yourself out: Before removing policies, make sure the user will still have the access they need through group membership
- Consider roles for applications: If this user is for an application or service, consider using IAM roles with temporary credentials instead of IAM users
- Federation is preferred: For human users, AWS recommends using identity federation (AWS IAM Identity Center, SAML, etc.) rather than IAM users
- Permissions boundaries: If you need to limit what permissions a user can possibly have, consider using permissions boundaries in addition to group-based access