cmc/cleberg.net

My personal web garden & blog.

clone: git clone https://gitbay.org/cmc/cleberg.net.git

main: content/blog/2026-02-21-auditing-aws-iam.org · raw

  1#+date:        [2026-02-21 Sat 18:58:39]
  2#+title:       Auditing AWS IAM Users
  3#+description: How to audit IAM user assignments in AWS.
  4#+slug: auditing-aws-iam
  5#+filetags:    :audit:
  6
  7If you've ever asked an IT team for a list of who has access to an AWS account,
  8you've probably received a spreadsheet that was already out of date. Or worse,
  9an out-of-context screenshot that doesn't explain anything. Or worse, they don't
 10know either and ignore you, hoping you forget about the request (ask me how I
 11know!).
 12
 13To make it worse, IAM Identity Center doesn't make this easy to extract through
 14the console, and most teams don't have a repeatable process for pulling it.
 15
 16To solve this problem, this post walks through a script that does it for you and
 17explains what to look for once you have the output. This is something I've used
 18in practice and want to share, as cloud auditing knowledge seems scarce.
 19
 20The script is available at [[https://github.com/audit-labs/audit-tools/blob/main/applications/aws/aws_iam_users.sh][audit-labs/audit-tools]].
 21
 22* Background: How AWS Provisions Access
 23
 24Before running anything, it helps to understand the different ways AWS can be
 25architected to grant access. You may encounter any one (or all) of these in
 26practice, sometimes within the same organization. Knowing how they differ tells
 27you where to look during an audit and which questions to ask.
 28
 29** IAM Users
 30
 31This is the original AWS access model. An IAM user is an identity created
 32directly inside a single AWS account (e.g., ~iamuser1~ created inside AWS user
 33~cmc~). This type of account has a username, an optional console password, and
 34optional access keys for programmatic access. Permissions are granted by
 35attaching IAM policies directly to the user, or by adding the user to an IAM
 36group that has policies attached to it.
 37
 38The core problem with IAM users at scale is that they're account-scoped, meaning
 39a user in ~Account A~ has no relationship to a user in ~Account B~, even if they're
 40the same person. Organizations with dozens or hundreds of accounts end up with a
 41fragmented identity landscape that's difficult to review and even harder to
 42deprovision consistently.
 43
 44From an audit perspective, these are key risks to understand during your
 45walkthrough of the environment. Additionally, you should note that IAM users are
 46high-risk when they have active access keys (long-lived credentials that don't
 47expire unless explicitly rotated or revoked) or when policies are attached
 48directly to the user rather than inherited through a group.
 49
 50** IAM Groups
 51
 52IAM groups are a way to organize IAM users within a single account and attach
 53policies to multiple users at once. A user inherits all permissions from every
 54group they belong to. Groups themselves cannot assume roles or make API calls.
 55They exist only as a convenience for managing user permissions in bulk.
 56
 57Groups help with consistency, but they don't solve the cross-account problem. An
 58IAM group in ~Account A~ still has no bearing on access in ~Account B~. You'll see
 59groups used alongside IAM users in the same environments where IAM users are
 60prevalent.
 61
 62** IAM Roles
 63
 64Roles are the most flexible identity construct in AWS. Unlike users, roles
 65aren't tied to a specific person. They're assumed temporarily by whoever (or
 66whatever) is authorized to use them. When an entity assumes a role, AWS issues
 67short-lived credentials that expire automatically, usually after an hour.
 68
 69Roles are used in several contexts:
 70
 71- *Cross-account access*: ~Account A~ can grant a role in ~Account B~ the ability to
 72  be assumed by principals in ~Account A~. This is how many organizations handle
 73  access across accounts without using IAM Identity Center.
 74- *Service roles*: AWS services like Lambda, EC2, and ECS use roles to make API
 75  calls on your behalf. An EC2 instance with an attached instance profile is
 76  assuming a role every time it calls an AWS API.
 77- *Federated access*: External identity providers (Active Directory, Okta, etc.)
 78  can be configured to exchange a user's external credentials for temporary AWS
 79  role credentials via SAML or OIDC. This is an older pattern that predates IAM
 80  Identity Center.
 81- *Break-glass access*: A role with broad permissions that is locked down by
 82  default and only assumed in emergencies. A common method for securing these
 83  accounts is vaulting the credentials in a PAM platform like CyberArk.
 84
 85From an audit perspective, roles require you to look at the trust policy (who is
 86allowed to assume the role) as well as the permission policy (what the role can
 87do once assumed). Both matter. A role with ~AdministratorAccess~ is only as risky
 88as its trust policy is permissive.
 89
 90** IAM Policies
 91
 92Policies are the documents that define permissions. They're not an access
 93pattern on their own, but understanding the three types helps when reviewing
 94output from any access audit:
 95
 961. *AWS managed policies* are created and maintained by AWS. Examples include
 97   ~AdministratorAccess~, ~ReadOnlyAccess~, and job-function policies like ~Billing~.
 98   They're convenient but broad: ~AdministratorAccess~ is a single policy that
 99   grants full access to every AWS service.
1002. *Customer managed policies* are created by the organization. They offer more
101   precise control and can be reused across users, groups, and roles. Better
102   practice than relying entirely on AWS managed policies for anything
103   sensitive.
1043. *Inline policies* are embedded directly into a specific user, group, or role
105   rather than existing as a standalone resource. They can't be reused and are
106   easier to miss during reviews. When you see an inline policy in audit output,
107   treat it as something that warrants a closer look — they're often added ad
108   hoc to solve a specific problem and not always well documented.
109
110** IAM Identity Center
111
112IAM Identity Center (formerly AWS SSO) is the current recommended approach for
113human (end-user) access in multi-account AWS Organizations. It sits above
114individual accounts and provides centralized access management across all of
115them from a single place.
116
117It connects to an identity source, such as AWS's own directory, Active
118Directory, or an external identity provider (IdP) like Okta or Entra ID, and
119uses that as the source of truth for users and groups. Access is configured
120within Identity Center itself, not inside individual accounts.
121
122The key construct is the permission set: a named bundle of IAM policies
123(managed, customer managed, and/or inline) that defines what a principal can do.
124When you assign a permission set to a user or group for a specific account, AWS
125creates a temporary IAM role in that account that the user assumes when they
126access it. The credentials are short-lived and scoped to the session.
127
128The standard architecture: users belong to groups in the identity source, groups
129are assigned permission sets for specific accounts in Identity Center, and users
130inherit access through their group memberships. Provisioning and deprovisioning
131happen at the group membership level, not by touching individual account
132assignments.
133
134Direct user assignments (where a user is assigned a permission set without going
135through a group) are possible but generally a control gap, because they bypass
136the group-based review and provisioning process.
137
138If the organization uses Identity Center, the access population you need to
139review lives there, not inside individual accounts. Looking at IAM users and
140roles within an account will give you an incomplete picture. You'll see the
141temporary roles that Identity Center creates, but not who is assigned to them or
142why.
143
144* What the Script Does
145
146The script works by running commands against a single named AWS account and
147produces a JSON report of every user and group assignment, along with the
148permission set and policies attached to each assignment.
149
150It runs in four steps:
151
1521. Finds the IAM Identity Center instance and looks up the target account ID by
153   name;
1542. Lists all permission sets provisioned to that account;
1553. For each permission set, retrieves all user and group assignments, resolves
156   their names from the Identity Store, and fetches the managed and inline
157   policies attached to the permission set;
1584. Writes everything to a JSON file named ~report_<account_name>.json~.
159
160The script caches permission set details and principal names to avoid redundant
161API calls when the same permission set or user appears in multiple assignments.
162
163At the end of this process, we will have the evidence we need to test controls
164such as administrative access or segregation of duties.
165
166* Prerequisites
167
168You'll need:
169
170- AWS CLI (or AWS CloudShell) installed and configured with credentials that
171  have read access to ~sso-admin~, ~identitystore~, and ~organizations~;
172- ~jq~ installed (used throughout the script for JSON parsing);
173- The name of the AWS account you want to audit (must match exactly as it
174  appears in AWS Organizations).
175
176Set the account name at the top of the script before running:
177
178#+begin_src bash
179ACCOUNT_NAME="your-account-name"
180#+end_src
181
182Then make the script executable and run it:
183
184#+begin_src bash
185chmod +x aws_iam_users.sh
186./aws_iam_users.sh
187#+end_src
188
189#+caption: Script Output
190#+attr_html: :alt Terminal output of aws_iam_users.sh showing discovered assignments for the cmc account.
191[[https://img.cleberg.net/blog/20260221-auditing-aws-iam/script.webp]]
192
193* Reading the Output
194
195The script produces a JSON array where each element represents a single
196principal-to-permission-set assignment. Here's an example:
197
198#+begin_src json
199[
200  {
201    "principal": {
202      "type": "GROUP",
203      "name": "testgroup1"
204    },
205    "permission_set": {
206      "name": "AdministratorAccess",
207      "policies": {
208        "managed_policies": [
209          "arn:aws:iam::aws:policy/AdministratorAccess"
210        ],
211        "inline_policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Sid\":\"Statement2\",\"Effect\":\"Deny\",\"Action\":[\"a4b:*\"],\"Resource\":[\"*\"]}]}"
212      }
213    }
214  },
215  {
216    "principal": {
217      "type": "USER",
218      "name": "iamtestuser1"
219    },
220    "permission_set": {
221      "name": "AdministratorAccess",
222      "policies": {
223        "managed_policies": [
224          "arn:aws:iam::aws:policy/AdministratorAccess"
225        ],
226        "inline_policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Sid\":\"Statement2\",\"Effect\":\"Deny\",\"Action\":[\"a4b:*\"],\"Resource\":[\"*\"]}]}"
227      }
228    }
229  },
230  {
231    "principal": {
232      "type": "USER",
233      "name": "iamtestuser1"
234    },
235    "permission_set": {
236      "name": "Billing",
237      "policies": {
238        "managed_policies": [
239          "arn:aws:iam::aws:policy/job-function/Billing"
240        ],
241        "inline_policy": ""
242      }
243    }
244  }
245]
246#+end_src
247
248#+caption: JSON Report
249#+attr_html: :alt The contents of report_cmc.json showing principal and permission set assignments.
250[[https://img.cleberg.net/blog/20260221-auditing-aws-iam/json.webp]]
251
252A few things to look for when reviewing this output:
253
254- *Direct user assignments*: ~iamtestuser1~ is assigned ~AdministratorAccess~ directly
255  as a USER, not through a group. Access should generally be provisioned through
256  groups so that provisioning and deprovisioning are tied to group membership,
257  not managed case-by-case. A direct user assignment to a high-privilege
258  permission set warrants a conversation with IT about why it exists.
259- *AdministratorAccess*: This is AWS's broadest managed policy, which allows full
260  access to everything in the account. Note who has it and whether that's
261  appropriate. In many environments, even senior engineers shouldn't have
262  standing ~AdministratorAccess~. It should be reserved for break-glass accounts
263  or infrastructure teams with a documented need.
264- *Inline policies*: The ~inline_policy~ field in this example contains a Deny
265  statement for ~a4b:*~. Inline policies attached to permission sets are worth
266  reviewing, as they can add permissions or restrict them, and they're easy to
267  miss if you're only looking at the managed policy name.
268- *Multiple assignments for the same user*: ~iamtestuser1~ appears twice: once with
269  ~AdministratorAccess~ and once with ~Billing~. This isn't automatically a problem,
270  but a user with both full administrative access and explicit billing access is
271  worth confirming with IT.
272
273* Common Exceptions and False Positives
274
275- *Break-glass accounts*: Most organizations maintain at least one emergency
276  account with direct ~AdministratorAccess~ that bypasses normal access controls.
277  A direct USER assignment to ~AdministratorAccess~ may be intentional for this
278  reason. Ask IT whether a break-glass account exists, confirm the username
279  matches, and document it as an accepted exception if appropriate.
280- *Small organizations or early-stage environments*: Some teams haven't
281  implemented group-based access management yet and assign permissions directly
282  to users across the board. This is a control gap, but the finding should
283  reflect the scale and maturity of the environment. A two-person startup with
284  direct assignments is a different risk than an enterprise doing the same
285  thing. This may or may not give rise to a deficiency.
286- *Service or automation accounts*: Occasionally a ~USER~ principal in Identity
287  Center is a service account rather than a human. These may legitimately have
288  specific permission sets assigned directly. Confirm the account's purpose
289  before writing it up.
290
291* How to Write Up the Finding
292
293If you identify a direct high-privilege user assignment that isn't a documented
294exception, here's how to frame it:
295- *Deficiency:* User ~iamtestuser1~ is directly assigned the ~AdministratorAccess~
296  permission set in account ~cmc~, rather than receiving access through a group
297  assignment.
298- *Root Cause:* Due to {{ root cause }}, the user ~iamtestuser1~ was inappropriately
299  provisioned the ~AdministratorAccess~ permission directly rather than through a
300  group assignment.
301- *Risk:* Direct user assignments increase the likelihood of orphaned access
302  following role changes or departures. They are also harder to identify during
303  periodic access reviews, as reviewers may focus on group membership rather
304  than individual assignments.
305- *Evidence:* Attach the relevant portion of ~report_cmc.json~ filtered to show the
306  specific principal, permission set, and policies. The JSON output is
307  self-documenting, as it includes the account name in the filename and contains
308  the full policy details alongside the assignment.
309
310If requested during review, you can filter the output to just the direct user
311assignments:
312
313#+begin_src bash
314jq '[.[] | select(.principal.type == "USER")]' report_cmc.json
315#+end_src
316
317#+caption: Filtered User Assignments
318#+attr_html: :alt Terminal output of the jq filter showing only direct user assignments from report_cmc.json.
319[[https://img.cleberg.net/blog/20260221-auditing-aws-iam/jq.webp]]