Profile
Back to NewsBack
Dev.to 4 min
Reader Mode
Your AWS account has more admins than the IAM console shows

Your AWS account has more admins than the IAM console shows

15 hours ago

Open IAM, filter by the AdministratorAccess policy, and you get a list. Three
users, maybe a role or two. That is the list that gets reviewed in access
reviews, pasted into compliance evidence, and shown to auditors.

It is almost always wrong, and not by a little.

Admin is not a policy you have. It is a state you can reach. Plenty of
principals that are nowhere near that filtered list can reach it in one API
call.

Permissions that grant permissions

Most policy review looks for wide permissions: s3:*, ec2:*, a Resource of
*. Those are worth finding. But there is a second category that gets almost no
attention, and it is the one that matters more: permissions that let you change
permissions.

Here are the ones I keep running into.

iam:CreatePolicyVersion. If you can create a new version of a policy and
set it as the default, you can rewrite any policy you are attached to. Your
tightly scoped policy becomes * on * in a single call. The policy document
that was reviewed last quarter is not the policy document in effect.

iam:AttachUserPolicy, iam:AttachRolePolicy, iam:AttachGroupPolicy.
The direct version. If you can attach a policy, you can attach
AdministratorAccess. To yourself.

iam:PutUserPolicy and iam:PutRolePolicy. The same thing with inline
policies, and these are worse because inline policies do not show up where
people look for attached managed policies.

iam:PassRole combined with a compute service. This one is the most common
and the least understood. PassRole on its own does nothing. Combined with
lambda:CreateFunction and lambda:InvokeFunction, or with
ec2:RunInstances, it means you can start something that runs as a role more
privileged than you, and then use it. The permission you needed was not admin.
It was the ability to hand a role to a service.

iam:UpdateAssumeRolePolicy. You cannot assume that role today. You can
edit its trust policy to say that you can, and then you can.

iam:CreateAccessKey on another user. Mint a key for someone more
privileged and use it. No exploit, no escalation, just an API call that is
working as designed.

iam:CreateLoginProfile and iam:UpdateLoginProfile. Set a console
password on a user who does not have one, or change one that does.

None of these say admin. Every one of them is a path to it.

Why linters miss this

Policy linters are built to answer "is this permission too broad". They flag
wildcards, they flag Resource: *, they score things against least privilege.

iam:CreatePolicyVersion on one specific policy ARN is not broad. It is narrow,
specific, and looks like exactly what a well-scoped policy should look like. It
will pass every check you run and it is a full escalation to whatever that
policy can be rewritten to grant.

The question a linter asks is about the shape of a permission. The question that
matters is about what the permission leads to. Those are different questions and
the second one is a graph problem.

Going and looking

Start by listing what actually exists rather than what you remember granting:

# every inline policy on every role, where the escalation paths usually hide
aws iam list-roles --query 'Roles[].RoleName' --output text \
  | tr '\t' '\n' \
  | while read -r r; do
      aws iam list-role-policies --role-name "$r" \
        --query "PolicyNames[] | [?length(@)>\`0\`]" --output text \
        | grep -q . && echo "$r"
    done

Then search your policy documents for the specific actions above. Not for
wildcards. For iam:PassRole, iam:CreatePolicyVersion, iam:AttachRolePolicy
and the rest, by name. Anything that holds one of them is effectively an admin
with extra steps, and should be reviewed as an admin.

Two things that make this harder than it sounds. Inline policies are invisible
in most tooling and most dashboards, so they are where this accumulates.
And iam:PassRole is so routine in CI setups that it is usually granted without
anyone asking which roles can be passed, which is the only part that matters.

The number that is actually useful

Stop counting principals with AdministratorAccess. Start counting principals
that can become an administrator, in any number of steps, using only calls they
are already allowed to make.

That number is bigger. On every account I have looked at, it has been bigger.
And unlike the first number, it is the one that describes your actual blast
radius.

If you want to see the trust half of the same picture, who can get into the
account from outside rather than what they can become once in, I maintain a
read-only CLI for it called frontdoor.
It is Apache 2.0, needs no account, and covers AWS, GCP and Azure.


If you review IAM differently, particularly if you have found a clean way to
handle PassRole scoping in CI, I would like to hear it.

Chat with me