This check flags IAM users with policies attached directly to them instead of inherited through groups. Direct attachments scatter permissions across your account and make audits painful. Fix it by moving the user into an IAM group that carries the policy, then detaching the inline or managed policy from the user.
Attaching a policy straight onto an IAM user feels like the fast path. You need to grant someone S3 access, you run one command, done. The problem shows up six months later when you have forty users, each with a slightly different bundle of directly attached policies, and nobody can answer the question "who can write to this bucket?" without clicking through every user in the console.
The user_directpolicies check catches this pattern early. It looks at every IAM user in your AWS account and reports any that have policies bound directly to the user identity rather than received through group membership.
What this check detects
AWS lets you grant permissions to an IAM user in several ways:
- Group-attached policies — the user belongs to a group, and the group carries managed policies. This is the recommended model.
- Directly attached managed policies — an AWS managed or customer managed policy bound straight to the user.
- Inline policies — a policy embedded inside the user object itself with no separate ARN.
The check flags users that fall into the second or third category. If a user has any managed or inline policy attached to the user identity directly, it gets reported.
Note: IAM groups are not security principals. You cannot assume a group or set it as a trust target. They exist purely as containers that distribute permissions to the users inside them. That is exactly why they are the right tool for managing human access at scale.
Why it matters
Direct policy attachment is not a vulnerability on its own. A user with one directly attached AmazonS3ReadOnlyAccess policy is not going to get you breached. The risk is operational, and it compounds over time.
Audits become guesswork
When permissions live on groups, you answer access questions by looking at group membership. A small number of groups, each with a clear purpose, maps cleanly to job functions. When permissions live on individual users, every access review turns into a per-user inspection. That is slow, error prone, and the kind of work that quietly gets skipped.
Permission drift and over-provisioning
Direct attachments tend to accumulate. Someone needs temporary access for a migration, a policy gets attached, the migration finishes, and the policy stays. Multiply that across a team and you end up with users holding permissions nobody remembers granting. This is the raw material for privilege escalation if any one of those credentials leaks.
Inconsistent access for the same role
Two engineers doing the same job should have the same permissions. With group-based access that happens automatically. With direct attachment you rely on whoever set up each user remembering to apply the exact same set of policies. They never do.
Warning: Long-lived IAM users with directly attached policies are a favorite target for attackers who find leaked access keys in commits, CI logs, or public buckets. The wider the permission set on that user, the more an attacker gets from a single leaked key.
How to fix it
The goal is to move the permissions onto a group and remove the direct attachment from the user. Here is the full sequence.
1. See what is attached to the user
aws iam list-attached-user-policies --user-name alice
aws iam list-user-policies --user-name alice
The first command lists managed policies attached directly. The second lists inline policies. You need both, because they are removed with different commands.
2. Create or pick a group that matches the user's role
aws iam create-group --group-name developers
3. Attach the policy to the group
aws iam attach-group-policy \
--group-name developers \
--policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
4. Add the user to the group
aws iam add-user-to-group \
--group-name developers \
--user-name alice
5. Confirm the user still has effective access, then detach the direct policy
Before removing anything, verify the new group grants what you expect. The IAM policy simulator is the safe way to do this:
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::123456789012:user/alice \
--action-names s3:GetObject \
--resource-arns arn:aws:s3:::my-bucket/*
Danger: Detaching policies removes access immediately. If the user relies on those permissions for production work or automated jobs, confirm the group grants equivalent access first. Removing the wrong policy can break a running pipeline or lock someone out of a resource they need.
Once you have confirmed access flows through the group, remove the direct attachment.
For a managed policy:
aws iam detach-user-policy \
--user-name alice \
--policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
For an inline policy:
aws iam delete-user-policy \
--user-name alice \
--policy-name s3-temp-access
Tip: If you have inline policies you want to keep, convert them to customer managed policies first with aws iam create-policy, then attach the new policy to a group. That makes the policy reusable and version controlled instead of trapped inside a single user.
Doing this with infrastructure as code
If you manage IAM in Terraform, the fix is structural. Define groups, attach policies to groups, and add users with a membership resource. Never put a aws_iam_user_policy_attachment on a human user.
resource "aws_iam_group" "developers" {
name = "developers"
}
resource "aws_iam_group_policy_attachment" "dev_s3" {
group = aws_iam_group.developers.name
policy_arn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
}
resource "aws_iam_user" "alice" {
name = "alice"
}
resource "aws_iam_user_group_membership" "alice_groups" {
user = aws_iam_user.alice.name
groups = [aws_iam_group.developers.name]
}
With this layout, granting a new engineer the same access is a one-line change to the membership list. Permissions stay defined in one place per role.
How to prevent it from happening again
Manual cleanup fixes today's problem. Preventing recurrence means catching direct attachments before they land.
Block the pattern in code review
If you use Terraform, a policy-as-code gate catches the bad pattern in CI. This OPA/Conftest rule rejects any plan that attaches a policy directly to a user:
package iam
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_iam_user_policy_attachment"
msg := sprintf("Policy attached directly to user %v. Use a group instead.", [resource.address])
}
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_iam_user_policy"
msg := sprintf("Inline policy on user %v. Move it to a group.", [resource.address])
}
Wire this into your pipeline so a plan that introduces a direct attachment fails before merge.
Use a service control policy
If you run AWS Organizations, an SCP can deny the relevant API calls account-wide. This stops direct attachment even for users acting through the console.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyDirectUserPolicyAttachment",
"Effect": "Deny",
"Action": [
"iam:AttachUserPolicy",
"iam:PutUserPolicy"
],
"Resource": "*"
}
]
}
Warning: An SCP that blocks iam:AttachUserPolicy applies to everyone in the targeted accounts, including your automation and break-glass roles. Test it in a non-production OU first, and make sure your IAM provisioning tooling uses group attachments before you roll it out.
Run the check continuously
Configuration drifts. Someone with console access can always click around the gate. Running user_directpolicies on a schedule with Lensix means a new direct attachment surfaces in your findings within the next scan rather than at your next annual audit.
Best practices
- Map groups to job functions. Keep group names readable, like
developers,billing-readonly, ordb-admins, so membership maps cleanly to what people actually do. - Prefer roles over long-lived users entirely. The cleanest version of this fix is to not have IAM users at all. Use IAM Identity Center or federated roles for humans, so access is temporary and centrally managed. Direct attachment becomes a non-issue when there are no standing user credentials.
- Keep groups small in number. A handful of well-scoped groups beats dozens of overlapping ones. If you are creating a group per person, you have recreated the problem with extra steps.
- Review group membership, not user permissions. Once permissions live on groups, your quarterly access review becomes a check of who is in which group. Far faster than per-user inspection.
- Avoid inline policies for human users. Inline policies are invisible in most permission inventories and cannot be reused. Reserve them for the rare cases where a policy must be tightly coupled to a single resource, like a role's inline trust scoping.
Direct policy attachment is one of those issues that costs nothing to ignore on day one and a great deal to untangle on day five hundred. Moving permissions to groups now keeps your IAM surface legible, and legible IAM is the foundation everything else in cloud security stands on.

