Original sample written by Security Flair to demonstrate our work. Not client work.
// TECHNICAL BLOG
iam:PassRole is the scariest permission in your AWS account (and nobody's looking at it)
Picture a pretty normal mid-sized SaaS company's AWS setup. Nothing exotic. A few accounts, Terraform for most things, a CI pipeline that deploys Lambda functions. The team is proud of its least-privilege work, and honestly, most of it is good.

Then you get to the CI role. It can create Lambda functions, invoke them, and pass any role in the account. Any role. Including OrganizationAccountAccessRole.

This one's a made-up example, but if you've done a few AWS reviews you've probably met this exact role. Nobody did anything wrong, exactly. Someone needed the pipeline to deploy a new function in a hurry, iam:PassRole on * made the error go away, and the ticket got closed. Two years later it's still there, sitting one leaked GitHub Actions secret away from full admin.

So let's talk about PassRole, because it's the permission I'd check first in almost any AWS account.
Wait, what does PassRole actually do?
When you create a Lambda function, you give it an execution role. That role is what the function runs as. AWS needs to check that you're allowed to hand that role over, and that check is iam:PassRole.

Here's the bit people miss. PassRole doesn't care whether you have the permissions inside the role. You can be a fairly boring identity and still pass a role that can do everything. Combine that with lambda:CreateFunction and lambda:InvokeFunction and you've basically got a way to run code as whatever role you like.

Lambda isn't special here, by the way. EC2 instance profiles, ECS tasks, Glue jobs, CloudFormation and SageMaker all take a role. If your review only looks at Lambda, you've checked one door in a long hallway.
How to find it in your own account
You don't need a fancy tool to start. The IAM policy simulator will tell you whether a given principal can do the scary combo:
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::111122223333:role/ci-deployer \
--action-names iam:PassRole lambda:CreateFunction lambda:InvokeFunction \
--resource-arns "*"
If all three come back allowed, that principal can very likely escalate. Run it against your CI roles first. Then your developer SSO permission sets. Then the role attached to that one EC2 box everyone forgot about.

For anything bigger than a handful of roles, open-source tools like PMapper or Cloudsplaining will map these paths across a whole account, which beats doing it by hand.
The cousin: CreatePolicyVersion
While you're in there, look for iam:CreatePolicyVersion too. Managed policies keep up to five versions, and one of them is the default. If an identity can create a new version of a policy that's attached to itself, it can write "Action": "*" into that new version, set it as default in the same API call, and walk off with admin.

What makes this one sneaky is that nothing looks different in an attachment review. Same policy, same identity. Only the contents changed.
What it looks like in CloudTrail
This is where a lot of detection rules quietly fail. PassRole is a permission check, not an API call, so there's no PassRole event in CloudTrail. If you wrote an alert that looks for one, it's never going to fire.

What you actually want to watch:
  • Lambda CreateFunction events (CloudTrail logs these with a version suffix, like CreateFunction20150331) where the role in the request parameters is a high-privilege role
  • EC2 RunInstances or AssociateIamInstanceProfile with a privileged instance profile
  • CreatePolicyVersion with setAsDefault set to true, and SetDefaultPolicyVersion
Quick way to eyeball recent policy version changes:
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=CreatePolicyVersion \
--max-results 50
A deploy pipeline creating functions all day is normal. A developer's long-lived access key creating a function with an admin role at 2am on a Sunday is not. The interesting signal is almost always who plus which role, not the event on its own.
Fixing it without starting a fight with engineering
The goal isn't to rip PassRole out. Pipelines genuinely need it. The goal is to make it boring.

Scope it to the specific roles that identity should be passing, and pin it to the service that should receive them:
{
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": "arn:aws:iam::111122223333:role/lambda-app-*",
"Condition": {
"StringEquals": {
"iam:PassedToService": "lambda.amazonaws.com"
}
}
}
A few other things that help a lot:
  • Treat IAM write actions (CreatePolicyVersion, PutRolePolicy, AttachRolePolicy and friends) as admin permissions, because they are
  • If developers need to create roles, give them a permissions boundary so nothing they create can outgrow it
  • Put an SCP at the org level that blocks IAM changes outside your approved path
  • Get rid of long-lived access keys wherever you can. Most of these stories start with one
Go run that simulator command against your CI role. I'd genuinely like to know how many of you get three "allowed"s back.

Let’s talk about your cybersecurity product

/ /⠀GET IN TOUCH
Leave your details and we’ll follow up to discuss your priorities and how Security Flair can help.
We'll use your details to respond to your inquiry. See our Privacy Policy