What does this IAM policy actually allow?
Paste the JSON. This reads it the way an attacker would — what the wildcards really cover, which actions lead from here to administrator, and who can reach your bucket. No signup, no email required.
Every test, explained
Twenty-five checks across five groups, plus what is deliberately left out and why.
What the wildcards actually cover
A star in a policy is not a rounding error. "Action": "*" with "Resource": "*" on an Allow is administrator access to the entire account, and it is usually a placeholder somebody wrote to get a deployment working. It works, so nobody goes back to it.
The tool also reads the narrower wildcards, because those are the ones that look reasonable. s3:* covers DeleteBucket and PutBucketPolicy as well as reading an object. It also covers every action Amazon adds to S3 next year, which nobody reviewed. Where the resource is scoped that matters much less, so it is reported separately and more quietly.
Then there are NotAction and NotResource, which are read backwards more often than anything else in IAM. Under an Allow they do not block what they name. They permit everything else. Under a Deny they are correct and useful, which is exactly why they look familiar in the wrong place — so this only flags them under an Allow.
The actions that lead to administrator
This is the group worth running a tool for. Every one of these looks like ordinary maintenance in a policy review, and each is a short path from a limited identity to full control.
iam:PassRole with "Resource": "*" is the clearest example. It arrives alongside the EC2 or Lambda permissions that genuinely need it, and it means any role can be handed to a service. So the holder starts a small instance carrying the most privileged role in the account, then reads its credentials out of the instance metadata. Nothing about that is exotic.
The rest are the policy-editing set: creating a new version of a managed policy and making it the default, attaching a managed policy such as AdministratorAccess, writing an inline policy straight onto an identity, editing the trust policy on a role, and creating access keys or console passwords for somebody else. Each is one or two API calls from where it starts to where it ends.
Who can reach your bucket
On a resource policy — a bucket policy, most often — the question changes. It is no longer what an identity may do. It is who from outside may reach the thing this is attached to.
One detail matters more than the rest, and the tool gets it right rather than being noisy. A public-read bucket is not automatically a fault. s3:GetObject open to the world is how a static site works. That is reported as information and asks you to confirm it, never as a failure. Public write is the crisis: an anonymous principal that can PutObject or DeleteObject gets found by scanners and used to host somebody else's files under your domain and on your bill.
It also looks for the conditions that only appear to protect something. A bucket restricted by aws:Referer or aws:UserAgent is a public bucket, because the caller chooses both of those values. Alongside that: s3:ListBucket open to anyone, which publishes the name of every object rather than the ones you meant to share; a whole account root as the principal; and no Deny on plain HTTP.
Who can become this role
A trust policy is the shortest document in AWS and one of the easiest to get wrong. It says who may assume the role. Everything the role can then do lives somewhere else entirely, which is part of why trust policies get less attention than they deserve.
A wildcard principal here means any AWS account in the world can assume the role. Anyone can open an account in a few minutes, so that is not a narrow exposure. It usually appears while somebody is trying to make a cross-account integration work, and then stays.
The subtler one is a cross-account trust with no sts:ExternalId. If the trusted account belongs to a vendor, anyone who persuades that vendor to assume a role on their behalf can name yours, and the vendor's own credentials do the rest. That is the confused deputy problem. This tool cannot tell whether the account listed is yours or somebody else's, so it reports the finding and prints the account numbers for you to check.
Version, size, and the quiet ones
A policy with no Version element runs under the 2008 rules, and so does one that names 2008-10-17 outright. Under those rules policy variables are not expanded. ${aws:username} in a resource ARN is compared as literal text.
That failure is completely silent. A policy written to give every user access to their own prefix gives them access to a prefix nobody has, and nothing anywhere raises an error. It just quietly does not work the way it reads.
Two smaller ones round the group out. AWS requires a Sid to be unique within a policy, so a repeated one means the document in front of you does not apply cleanly as it stands — usually because two policies were merged by hand. And a managed policy is capped at 6,144 characters, not counting whitespace, so a document close to that is about to have its next edit rejected.
What this deliberately does not do
It never asks whether a permission is needed. That cannot be answered by reading a policy, because it depends on what the workload actually calls. A tool that guessed would be confidently wrong, and confidently wrong is worse than silent. Answering it properly means reading CloudTrail for the identity over a few weeks.
It makes no AWS API calls and it never wants your credentials. It reads the JSON you paste and nothing else. So it cannot see S3 Block Public Access, which overrides a bucket policy and may well be the reason a public-looking bucket is not actually public. It also cannot see service control policies, permission boundaries or session policies, all of which can cut permissions back before this document is ever consulted.
It has no opinion on formatting, key order or style. There is no service behind that, and a report padded with taste is a report people stop reading.
What usually goes wrong, and how it gets fixed
The same handful of statements turn up again and again. Here they are, with the change that fixes each one.
Replace the star with the calls you actually make
The wildcard almost never needs to be a wildcard. Start from the operations the application performs, not from the ones it might one day want.
Before, and this is the single most common statement in a real account:
{
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}
After, for an application that uploads and serves files: {"Effect": "Allow", "Action": ["s3:GetObject", "s3:PutObject"], "Resource": "arn:aws:s3:::app-uploads/*"}. Note the /* on the end — object actions apply to objects, and a bucket ARN without it grants nothing. If you do not know which calls the application makes, CloudTrail does. Filter on the role for a fortnight and the list writes itself.
Scope iam:PassRole to the roles that may be passed
PassRole is not really an IAM permission. It is the permission to hand a set of credentials to a service, so it should always name which credentials.
List the role ARNs. There are usually one or two, and they are the roles this workload creates instances or functions with:
{
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": "arn:aws:iam::123456789012:role/app-task-role",
"Condition": {
"StringEquals": {
"iam:PassedToService": "ecs-tasks.amazonaws.com"
}
}
}
The condition is optional and worth adding. It pins the role to the service that is meant to receive it, so the same permission cannot be used to launch something else carrying that role.
Turn NotAction under an Allow into an Allow
This one is worth reading twice, because the fix is not to delete a line. The statement means the opposite of what most people read.
Before — and what this grants is every action in AWS except IAM, which is nearly everything:
{
"Effect": "Allow",
"NotAction": "iam:*",
"Resource": "*"
}
If the intent was "everything except IAM", it belongs in a Deny, where NotAction behaves the way it reads — or better, in a service control policy or a permission boundary, which is where account-wide exclusions belong. If the intent was "what this application needs", write it as an Allow that lists those actions.
Publish a bucket properly, if publishing is the point
Plenty of buckets are meant to be public. A static site, a downloads directory, an image host. The fix is not to lock it — it is to grant reading and nothing else.
Two things to get right: the resource ends in /* so it covers objects rather than the bucket, and s3:ListBucket is left out so nobody can enumerate what is in there.
{
"Sid": "PublicReadObjectsOnly",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-static-site/*"
}
For anything larger, serving the bucket through CloudFront with Origin Access Control is better again: the bucket itself stops being public, the distribution is what the world reaches, and you get caching and logs with it.
Stop protecting a bucket with a Referer check
This pattern circulates in old hotlink-protection advice and it still turns up in copied bucket policies. It does not restrict anything.
aws:Referer and aws:UserAgent are request headers, and the caller writes their own headers. One extra flag on a curl command satisfies the condition. If the content genuinely needs protecting, use a condition on something the caller does not control:
"Condition": {
"IpAddress": {
"aws:SourceIp": ["203.0.113.0/24"]
}
}
Depending on what you are protecting, aws:SourceVpce, aws:PrincipalOrgID or a CloudFront signed URL will each fit better than an address range. What they have in common is that the caller cannot simply assert them.
Refuse plain HTTP in one statement
S3 answers on both HTTP and HTTPS unless a policy says otherwise, and a request made over HTTP carries its authorisation header in clear text.
This is the cheapest statement in this whole page. It costs nothing, breaks nothing that was already using HTTPS, and it is the first thing an auditor looks for:
{
"Sid": "DenyInsecureTransport",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
],
"Condition": {
"Bool": { "aws:SecureTransport": "false" }
}
}
Both ARNs are needed. The first covers bucket-level calls and the second covers the objects, and a Deny that names only one of them leaves the other reachable over HTTP.
Give a third party an ExternalId
If you have granted a vendor access with a role, this is the line that keeps that role yours.
The ExternalId is not a secret and it is not authentication. It is a value that you and the vendor both know, which their platform must send when it assumes the role. Vendors that do this properly generate one for you and show it in their console:
{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::123456789012:root" },
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "the-value-your-vendor-gave-you"
}
}
}
Where the principal is an AWS service rather than an account, the equivalent is a condition on aws:SourceArn or aws:SourceAccount, which pins the role to the specific resource it exists for.
Permissions that fit the job, and nothing more.
I review and rebuild IAM in AWS accounts for small teams — the roles, the trust relationships, the bucket policies, and the CloudTrail work that shows which permissions anything actually uses. Send me a policy and I will tell you what I would change.
Prefer to talk? Book a free call ↗ · Or hire me on Upwork ↗ · Typical reply within one business day.