From sre-skills
Audits the union of all IAM policies attached to one AWS principal for cross-statement privilege-escalation paths and neutralised apparent escalations. Use when asked to review a role or user for over-broad grants or admin access.
How this skill is triggered — by the user, by Claude, or both
Slash command
/sre-skills:iam-deceptive-escalation-auditorThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Privilege-escalation audit skill for one AWS IAM principal. Takes every permissions policy
FAILURE_MODES.mdfixtures/01-orphaned-passrole-deny/meta.jsonfixtures/01-orphaned-passrole-deny/policy.jsonfixtures/02-action-star-blanket-deny/meta.jsonfixtures/02-action-star-blanket-deny/policy.jsonfixtures/03-assumerole-broken-trust/meta.jsonfixtures/03-assumerole-broken-trust/policy.jsonfixtures/03-assumerole-broken-trust/trust-policy.jsonfixtures/05-iam-mutation-boundary-capped/meta.jsonfixtures/05-iam-mutation-boundary-capped/policy-1.jsonfixtures/05-iam-mutation-boundary-capped/policy-2.jsonfixtures/06-cross-account-assume-condition-gated/meta.jsonfixtures/06-cross-account-assume-condition-gated/policy-1.jsonfixtures/06-cross-account-assume-condition-gated/policy-2.jsonfixtures/06-cross-account-assume-condition-gated/trust-policy.jsonfixtures/07-passrole-sandboxed-role-orphaned/meta.jsonfixtures/07-passrole-sandboxed-role-orphaned/policy-1.jsonfixtures/07-passrole-sandboxed-role-orphaned/policy-2.jsonfixtures/07-passrole-sandboxed-role-orphaned/policy-3.jsonfixtures/08-ml-platform-passrole-launch-needle/meta.jsonPrivilege-escalation audit skill for one AWS IAM principal. Takes every permissions policy attached to a role or user (plus the trust policy and permissions boundary if supplied), resolves the effective permission set across all of them, and answers one question a per-statement read cannot: can this principal escalate to a privilege it was not granted, and is an apparent escalation real or already neutralised. It returns findings with severity and a fix, then names exactly where a single principal's policy documents stop being able to answer the question.
The escalation combinations this skill exists to catch are precisely the ones that span
two statements or two attached policies, so that no single statement looks guilty on its
own. iam:PassRole in one policy and sagemaker:CreateTrainingJob in another are each
routine; together they let the principal launch compute with any role attached and inherit
it. A per-statement read clears every statement and misses the union. The other half of the
skill is the inverse discipline: an explicit Deny, a resource scope, a broken trust, or an
unsatisfiable Condition can neutralise an escalation that still reads as critical, and
the audit must not fabricate a finding the effective permissions do not support.
Action '*')
and the claim "but it's capped / scoped / denied" needs to be confirmed against the
effective permissions, not taken on trust.It reads the static policy documents attached to one principal: every permissions policy,
plus the trust policy (AssumeRolePolicyDocument) and the permissions boundary if supplied.
That is the entire input. The audit is correct and complete for the effective permissions
those documents express, and it is explicit about the rest. Every audit ends by naming the
joins it cannot make:
A clean (neutralised) policy still gets a boundary section, because a capped policy is not a proven-safe principal.
Build the effective permission set across all attached policies. An action is granted
when some Allow statement matches it (by case-insensitive glob on Action, or by
NotAction) and no blanket Deny (on Resource "*") matches it. Deny wins over Allow,
always. The escalation checks then run against this resolved set, not against any single
statement, because the combos are unions and the neutralisations are denies.
Deny handling is a conservative approximation: a Deny on
Resource "*"kills the action; resource-specific denies are behind the boundary (the audit does not enumerate the account's ARNs). This never under-reports a grant on a wildcard resource, which is the case the skill cares about.
Before any judgment, union the statements and apply Deny:
policy*.json for the principal. A principal can have several attached
policies, and the escalation combos are exactly the ones that span them.Effect: Deny as a hard constraint, not noise — it is the
single most common neutraliser in this corpus.Action (* or svc:*) into the concrete sensitive permissions it
grants, so a wildcard is judged by what it contains, not skimmed as "broad."These are the flagship. Each spans statements so no single one looks guilty. Run them against the resolved set:
iam:PassRole + a compute-launch action. Pair PassRole with
ec2:RunInstances, lambda:CreateFunction, ecs:RunTask, sagemaker:CreateTrainingJob,
cloudformation:CreateStack, etc.: launch compute with a more-privileged role attached,
then use that compute's credentials. Critical when PassRole is on Resource "*" (any
role, including admin); high when scoped (the escalation is real only if that scoped
role is more privileged — a boundary question). The launch action must actually bind a
role: Start/Invoke on existing compute take no PassRole argument and do not arm E1.iam:CreatePolicyVersion /
iam:SetDefaultPolicyVersion: mint a new admin version of an attached policy, or flip the
default back to a permissive one. No second action needed; the policy ARN is unchanged.lambda:UpdateFunctionCode:
overwrite an existing function's code to run attacker code with that function's role. No
PassRole required (it reuses an attached role).iam:AttachUserPolicy /
AttachRolePolicy / PutRolePolicy etc.: a single attach call turns a scoped identity
into an administrator.iam:UpdateAssumeRolePolicy (+ sts:AssumeRole = critical): rewrite a privileged role's
trust to trust this principal, then assume it.iam:CreateAccessKey /
CreateLoginProfile / AddUserToGroup etc.: a sideways takeover that never touches the
caller's own policies, so a review of this principal's permissions looks clean.What is NOT an escalation (do not flag these): A standalone sts:AssumeRole grant is
not an in-account privilege escalation on its own. Escalation-via-assume is E5 and
requires iam:UpdateAssumeRolePolicy to rewrite a role's trust so it trusts this principal.
Without that rewrite capability, an sts:AssumeRole grant only does anything if the target
role already trusts this principal back, and even then it is lateral movement to whatever
that role can do, not self-escalation, scored as the boundary question of "is the target more
privileged." A cross-account sts:AssumeRole narrowed by an aws:PrincipalOrgID /
sts:ExternalId condition, with no UpdateAssumeRolePolicy to relax either side, is inert:
report no escalation. Do not debate whether the condition is "satisfiable" or call the path
"live" — that is the wrong frame and produces a false positive. The grant is unused and
removable; the correct recommendation is "no fix needed (optionally remove the inert grant)",
never "harden / pin / monitor it."
Each Allow statement gets at most one wildcard finding (W1 > W3 > W2 > W4 > W5):
Action '*' on Resource '*'. Full administrator by value. Every
privesc combo is a subset of this one grant, so report it as the single headline rather
than enumerating a dozen restatements.Allow + NotAction. This is "allow everything except a short list," not
"allow these few." It reads narrow and is one of the broadest possible shapes. The safe
form is Deny + NotAction.svc:*) on a sensitive service (iam, sts, kms,
secretsmanager, s3, lambda, ec2, ...). Hands over every mutating and credential-bearing
action that service exposes.Resource '*' where the action supports
resource-level scoping. Broader than the workload needs.Resource '*' restricted to the sensitive-data read set:
s3:GetObject/ListBucket, secretsmanager:GetSecretValue, kms:Decrypt,
dynamodb:GetItem/Scan/Query, ssm:GetParameter(s). A data-exfiltration reach whose
impact depends on the data classification (behind the boundary): a flag, not a confirmed
leak. W5 does not fire on benign read APIs — cost-and-usage / billing reads,
Describe* / List* inventory, CloudWatch, tagging reads — on Resource '*'. Broad access
to non-sensitive metadata is not a W5 finding; flagging it is a false positive.Principal "*" with no aws:PrincipalOrgID / aws:SourceAccount / sts:ExternalId
condition lets any AWS principal in any account assume the role. A wildcard principal with
an ExternalId or org condition (the cross-account vendor pattern) is fine and must not be
flagged.This is the half the naive read gets wrong in the other direction. An apparent escalation that the effective permissions neutralise is CLEAN, and the audit must say so instead of flagging a critical that cannot fire. The resolution in step 1 is what proves it. The neutralisers seen in practice, each of which must suppress the finding it looks like:
Deny on iam:PassRole kills the E1 combo even with a scoped Allow and a
launch action present. The PassRole half is dead.Action '*' pinned to one bucket (never Resource '*'), with a Deny on every
escalation-bearing service, expands to nothing useful. Not W1.sts:AssumeRole on an admin-sounding role whose trust policy does not
trust this principal back, and no iam:UpdateAssumeRolePolicy to rewrite it. The path is
inert.Deny over a full mutation kit (E2/E4/E5/E6 primitives) on
Resource '*' collapses the effective set to read-only. The kit is capped.Condition (an sts:ExternalId + aws:PrincipalOrgID),
with the target's trust narrowed by the same condition and no iam:UpdateAssumeRolePolicy to
relax either side, is inert (see "What is NOT an escalation"). Report no escalation;
recommend at most removing the unused grant. Do not call the path live or recommend hardening
it — that is the false positive this fixture baits.Start/Invoke) bind no role. The shape of E1 is there; the gain is not.On a clean policy the audit reports: no real escalation, why the apparent one is neutralised (the Deny / scope / broken trust / sealed condition), and the boundary. It does not headline a neutralised or read-only grant as critical, and does not drown the verdict in nitpicks about correctly-scoped statements.
Order findings by severity (critical, high, medium, low). For each: the statement(s) it is grounded in, what the escalation is, and the fix. Then list the boundary from step "What this skill reads." A clean policy still gets a boundary section.
| Severity | Meaning |
|---|---|
| critical | A path to administrator that the effective permissions support: PassRole-on-* + launch (E1), policy rewrite (E2), function hijack (E3), self-attach (E4), trust-rewrite + assume (E5), full admin (W1). |
| high | A real but bounded escalation or exposure: scoped PassRole + launch, credential minting (E6), service wildcard (W2), Allow+NotAction (W3), open trust (X1). |
| medium | An over-broad mutating grant where scoping is possible (W4). |
| low | A read-reach whose impact needs the data classification behind the boundary (W5). |
The low band is deliberately honest: W5 depends on what data the resources hold, which is not in the policy. It is a flag to verify, not a verdict.
| Code | Rule | Severity | Grounded in |
|---|---|---|---|
| E1 | iam:PassRole + a role-binding compute-launch action | critical / high | resolved Allow set |
| E2 | iam:CreatePolicyVersion / SetDefaultPolicyVersion | critical | resolved Allow set |
| E3 | lambda:UpdateFunctionCode | critical | resolved Allow set |
| E4 | policy-attach / put actions onto a principal | critical | resolved Allow set |
| E5 | iam:UpdateAssumeRolePolicy (+ sts:AssumeRole) | critical / high | resolved Allow set |
| E6 | credential-minting actions for another identity | high | resolved Allow set |
| W1 | Action '*' on Resource '*' (full admin) | critical | one Allow statement |
| W2 | service-level wildcard on a sensitive service | high | one Allow statement |
| W3 | Allow + NotAction | high | one Allow statement |
| W4 | mutating actions on Resource '*' (scopable) | medium | one Allow statement |
| W5 | broad read on Resource '*' | low | one Allow statement |
| X1 | trust policy: wildcard principal, no narrowing condition | high | trust policy |
The matching half of every escalation rule is the clean verdict: the combo present in statements but killed by a Deny / scope / broken trust / sealed condition is not a finding. Reporting it anyway is the dominant failure mode this skill prevents.
The agent's final message in any invocation must include:
Seven end-to-end fixtures are committed under fixtures/, each with a runnable replay test.
The set is deliberately weighted toward the deceptive-clean cases, because over-flagging a
neutralised policy is the cold agent's dominant failure here:
08-ml-platform-passrole-launch-needle:
the needle. iam:PassRole on Resource '*' and sagemaker:CreateTrainingJob sit four
policies apart across ~16 statements; only the union is the critical E1 escalation.01-orphaned-passrole-deny: PassRole +
RunInstances looks like E1, but an explicit Deny on iam:PassRole kills the combo. Clean.02-action-star-blanket-deny: Action '*'
reads as admin but is pinned to one sandbox bucket with a Deny on every dangerous service.
Clean.03-assumerole-broken-trust: sts:AssumeRole on
an admin-sounding role whose trust does not point back, and no rewrite action. Clean.05-iam-mutation-boundary-capped: a full
mutation kit (E2/E4/E5/E6 primitives) capped by a permissions-boundary Deny on
Resource '*'. Clean.06-cross-account-assume-condition-gated:
a cross-account assume sealed by an unsatisfiable ExternalId + org-id condition at both
ends. Clean.07-passrole-sandboxed-role-orphaned:
PassRole + compute verbs, but the verbs bind no role and the one passable role is
read-only. Clean.Every fixture has a replay test in tests/ that runs the methodology (via the deterministic
reference engine tests/_audit.py) against the committed policy JSON, with no external
credentials. Run from the skill directory:
for t in tests/replay_*.py; do python "$t" || exit 1; done
The seven tests cover the needle (E1 from the union) and the six neutralisation mechanisms
(Deny, scope, broken trust, boundary cap, sealed condition, orphaned combo). Tests exit
non-zero if the audit names the wrong escalation or fabricates one on a clean policy. See
tests/README.md for the fixture schema.
This skill is wrong in predictable ways. Read FAILURE_MODES.md before
relying on it. Highlights:
Resource "*". A resource-specific Deny that neutralises
a grant on a concrete ARN is behind the boundary, not modelled.The audit above runs end-to-end against the policy JSON the user already has. No Anyshift dependency.
Every boundary note in this skill is a join: principal to its full set of attached policies, principal to its permissions boundary, account to its org SCPs, this policy to the privileges of the roles it passes or assumes. The Anyshift MCP can act as a context primer by resolving those joins from a versioned resource graph, so an E1 finding ("scoped PassRole, escalation real only if the target role is more privileged") can be closed instead of deferred. A measured "with vs without" delta will be published here once the integration has been exercised against the replay fixtures.
npx claudepluginhub anyshift-io/claude-plugins --plugin sre-skillsDetect AWS IAM privilege escalation paths using boto3 and Cloudsplaining policy analysis to identify overly permissive policies, dangerous permission combinations, and least-privilege violations
Detect AWS IAM privilege escalation paths using boto3 and Cloudsplaining policy analysis to identify overly permissive policies, dangerous permission combinations, and least-privilege violations
Detects AWS IAM privilege escalation paths using boto3 and Cloudsplaining analysis to identify overly permissive policies, dangerous permission combinations, and least-privilege violations.