Commit 7bdc5cd488

7bdc5cd488bfe0de771155264c119a1d76e31bfc

parent: c200513119

Unregistered key

cmc <hello@cleberg.net> · 2026-02-22 02:12 UTC
committer: <noreply@github.com>

publish new post: auditing-aws-iam (#7)

* fix typo

* publish new post: auditing-aws-iam

Layout: unified · split

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