Audit an IAM User’s Policies and Groups with AWS CLI

Cloud identity connected to policies, groups, and a permissions boundary during an access audit

What You’ll Learn

In this lesson, you’ll use AWS CLI IAM commands to audit the permission sources associated with a specific IAM user. You will inspect policies attached directly to the user, inline policies, group membership, group policies, and the user’s permissions boundary.

  • List managed policies attached directly to an IAM user.
  • Find inline policies attached directly to the user.
  • Identify the IAM groups the user belongs to.
  • Inspect managed and inline policies attached through those groups.
  • Recognize why an IAM policy listing is not always the same as a complete effective-permissions analysis.

The Concept

An IAM user’s permissions can come from several sources. The most visible sources are identity-based policies attached directly to the user and policies inherited through IAM groups.

A managed policy is a reusable policy object with its own ARN. An inline policy is embedded directly in a user or group. The following AWS CLI commands are especially useful during an audit:

  • list-attached-user-policies lists managed policies attached directly to a user.
  • list-user-policies lists inline policy names attached directly to a user.
  • list-groups-for-user lists the groups containing the user.
  • list-attached-group-policies lists managed policies attached to a group.
  • list-group-policies lists inline policy names attached to a group.
  • get-user can show whether a permissions boundary is configured.

This information is useful when investigating why a user can access a resource, reviewing access before removing an account, or checking whether a user’s permissions match an access-control design.

These commands show permission sources, not a final allow-or-deny decision for every possible request. Service control policies, resource policies, permission boundaries, session policies, explicit denies, and the requested resource can also affect the result. Treat this audit as an inventory of sources, and use policy simulation or service-specific testing when you need to evaluate a particular action.

Basic Example

The following commands audit a user named auditor@example.com. Replace the value of IAM_USER with the actual IAM user name in your account.

IAM_USER="auditor@example.com"

printf '%s\n' "--- Managed policies attached directly to the user ---"
aws iam list-attached-user-policies \
    --user-name "$IAM_USER" \
    --query 'AttachedPolicies[*].[PolicyName,PolicyArn]' \
    --output table

printf '%s\n' "--- Inline policies attached directly to the user ---"
aws iam list-user-policies \
    --user-name "$IAM_USER" \
    --output table

printf '%s\n' "--- Groups containing the user ---"
aws iam list-groups-for-user \
    --user-name "$IAM_USER" \
    --query 'Groups[*].[GroupName,Arn]' \
    --output table

printf '%s\n' "--- Permissions boundary ---"
aws iam get-user \
    --user-name "$IAM_USER" \
    --query 'User.PermissionsBoundary.PolicyArn' \
    --output text

Expected Output

The exact results depend on your AWS account. A user might produce output similar to this:

--- Managed policies attached directly to the user ---
--------------------------------------------------------------
|                 ListAttachedUserPolicies                   |
+----------------------+-------------------------------------+
|  ReadOnlyAccess      |  arn:aws:iam::aws:policy/ReadOnlyAccess |
+----------------------+-------------------------------------+

--- Inline policies attached directly to the user ---
---------------------------------
|       ListUserPolicies        |
+-------------------------------+
|  TemporaryAuditAccess         |
+-------------------------------+

--- Groups containing the user ---
--------------------------------------------------------------
|                   ListGroupsForUser                       |
+----------------------+-------------------------------------+
|  SecurityAuditors    |  arn:aws:iam::123456789012:group/SecurityAuditors |
+----------------------+-------------------------------------+

--- Permissions boundary ---
arn:aws:iam::123456789012:policy/AuditBoundary

If the user has no permissions boundary, the final command commonly returns None. Empty policy or group lists may display as an empty table.

How the Code Works

An IAM user connects to direct managed and inline policies, to groups that provide additional managed and inline policies, and to an optional permissions boundary. These sources feed an inventory audit, which is not itself a final effective-permissions decision.
Audit an IAM user’s direct policies, group-inherited policies, and permissions boundary before evaluating effective access.

IAM_USER stores the name once so every command audits the same account. Quoting "$IAM_USER" prevents shell word-splitting and is a good habit in reusable scripts.

The --query option uses JMESPath to select only useful fields from the AWS response. For example, AttachedPolicies[*].[PolicyName,PolicyArn] displays each directly attached managed policy’s name and ARN instead of the entire response.

list-user-policies returns inline policy names, not their policy documents. To inspect an individual inline policy document, use get-user-policy:

aws iam get-user-policy \
    --user-name "$IAM_USER" \
    --policy-name "TemporaryAuditAccess"

Similarly, list-groups-for-user identifies membership but does not list the policies inherited through each group. You must query each group separately.

Another Example

This script expands the group portion of the audit. It lists direct policies, the permissions boundary, every group containing the user, and both managed and inline policies attached to each group.

#!/usr/bin/env bash

set -euo pipefail

IAM_USER="${1:-auditor@example.com}"

printf '%s\n' "--- Direct managed policies for $IAM_USER ---"
aws iam list-attached-user-policies \
    --user-name "$IAM_USER" \
    --query 'AttachedPolicies[*].[PolicyName,PolicyArn]' \
    --output table

printf '%s\n' "--- Direct inline policies for $IAM_USER ---"
aws iam list-user-policies \
    --user-name "$IAM_USER" \
    --output table

boundary_arn=$(aws iam get-user \
    --user-name "$IAM_USER" \
    --query 'User.PermissionsBoundary.PolicyArn' \
    --output text)

printf '\nPermissions boundary: %s\n' "$boundary_arn"

group_names=$(aws iam list-groups-for-user \
    --user-name "$IAM_USER" \
    --query 'Groups[].GroupName' \
    --output text | tr '\t' '\n')

if [ -z "$group_names" ]; then
    printf '%s\n' "The user is not a member of any IAM groups."
else
    while IFS= read -r group_name; do
        [ -z "$group_name" ] && continue

        printf '\nGroup: %s\n' "$group_name"

        printf '%s\n' "Managed policies:"
        aws iam list-attached-group-policies \
            --group-name "$group_name" \
            --query 'AttachedPolicies[*].[PolicyName,PolicyArn]' \
            --output table

        printf '%s\n' "Inline policies:"
        aws iam list-group-policies \
            --group-name "$group_name" \
            --output table
    done <<< "$group_names"
fi

Passing a user name as the first script argument makes the audit reusable:

./audit-iam-user.sh finance-reviewer

The script uses set -euo pipefail so failures, unset variables, and pipeline errors are less likely to go unnoticed. The tr command converts tab-separated AWS CLI text output into one group name per line, allowing the while loop to process each group independently.

Common Mistakes

  • Checking only direct user policies: A user may inherit significant access from one or more groups. Always run list-groups-for-user during a user audit.
  • Confusing policy names with policy documents: The list commands identify policies but do not show their statements. Use get-user-policy or get-group-policy for inline documents. For managed policies, retrieve the default version with get-policy and get-policy-version.
  • Assuming listed permissions are automatically effective: A permissions boundary limits identity-based permissions; it does not grant permissions by itself. Explicit denies, SCPs, resource policies, and session policies can also change the result.
  • Using the wrong identity or region context: IAM is generally a global service, but your AWS CLI profile and account still matter. Confirm the active identity with aws sts get-caller-identity before auditing a production account.
  • Ignoring IAM paths: Policy ARNs and group ARNs can include paths. Prefer the ARN returned by AWS rather than reconstructing it manually.

Try It Yourself

Choose a test IAM user and run the basic audit commands. Then answer these questions:

  • Does the user have any managed policies attached directly?
  • Does the user have any inline policies?
  • Which groups contain the user?
  • Which policies come from those groups?
  • Is a permissions boundary configured?

For any inline policy name you discover, use get-user-policy or get-group-policy to inspect its document. Perform this exercise with read-only IAM permissions and avoid changing policies while auditing.

Challenge

Write a Bash script named summarize-user-access.sh that accepts an IAM user name as its first argument and reports:

  • The names of managed policies attached directly to the user.
  • The names of inline policies attached directly to the user.
  • The permissions boundary ARN, or a clear message when no boundary exists.
  • Each group containing the user.
  • The managed and inline policy names attached to each group.

Use AWS CLI queries to keep the output focused. Also make the script stop when an AWS CLI request fails.

Solution

The following solution meets all of the challenge requirements and prints policy names rather than large policy responses.

#!/usr/bin/env bash

set -euo pipefail

IAM_USER="${1:?Usage: $0 IAM_USER_NAME}"

printf '%s\n' "--- Direct managed policies ---"
aws iam list-attached-user-policies \
    --user-name "$IAM_USER" \
    --query 'AttachedPolicies[].PolicyName' \
    --output text

printf '%s\n' "--- Direct inline policies ---"
aws iam list-user-policies \
    --user-name "$IAM_USER" \
    --query 'PolicyNames[]' \
    --output text

boundary_arn=$(aws iam get-user \
    --user-name "$IAM_USER" \
    --query 'User.PermissionsBoundary.PolicyArn' \
    --output text)

if [ "$boundary_arn" = "None" ] || [ -z "$boundary_arn" ]; then
    printf '%s\n' "Permissions boundary: none"
else
    printf 'Permissions boundary: %s\n' "$boundary_arn"
fi

group_names=$(aws iam list-groups-for-user \
    --user-name "$IAM_USER" \
    --query 'Groups[].GroupName' \
    --output text | tr '\t' '\n')

if [ -z "$group_names" ]; then
    printf '%s\n' "Groups: none"
    exit 0
fi

while IFS= read -r group_name; do
    [ -z "$group_name" ] && continue

    printf '\nGroup: %s\n' "$group_name"

    printf '%s\n' "Managed policies:"
    aws iam list-attached-group-policies \
        --group-name "$group_name" \
        --query 'AttachedPolicies[].PolicyName' \
        --output text

    printf '%s\n' "Inline policies:"
    aws iam list-group-policies \
        --group-name "$group_name" \
        --query 'PolicyNames[]' \
        --output text
done <<< "$group_names"

The required argument syntax prevents the script from silently auditing an unintended user. The direct user commands run first, then the script checks the boundary and iterates through every group returned by IAM. Because each group is queried separately, the report includes permission sources that would be missed by inspecting only the user object.

Key Takeaways

  • list-attached-user-policies and list-user-policies reveal managed and inline policies attached directly to a user.
  • list-groups-for-user is essential because group membership can provide additional permissions.
  • Inspect each group with list-attached-group-policies and list-group-policies.
  • Use get-user to check for a permissions boundary, which limits identity-based permissions.
  • An inventory of attached policies is not a complete effective-permissions decision; explicit denies and other AWS policy layers also matter.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top