About Expertise Work Projects
Hosted Monitoring & Dashboards Self-Hosted Observability Stack Bulk Document Data Extraction
Free Tools
Website Health Check Email Domain Health Check DNS Health Check SSL Certificate Checker Redirect Chain Checker Robots.txt Checker XML Sitemap Validator Docker Compose Checker WordPress Security Check AWS IAM / S3 Policy Checker Downtime Cost Calculator
Blog Certifications Hire Me

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.

Nothing you paste is stored, logged or cached. It is read in memory and thrown away.

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.

Questions

Is my policy stored, logged or sent anywhere?
No. The policy is read in memory and thrown away when the page finishes rendering. It is never written to disk, never cached and never sent anywhere. This tool submits by POST rather than in a query string precisely so the document does not end up in the web server access log, in a Referer header, or in analytics — which is what would happen if it travelled in the URL.
Do you need my AWS credentials?
No, and you should not give them to any web page that asks. This makes no AWS API calls at all. It reads the JSON you paste, and that is the entire interaction.
Is a public S3 bucket always a problem?
No, and any tool that says so is wasting your time. Public read access is how a static site, a downloads bucket or a public asset bucket is meant to work, so this reports it as information and asks you to confirm it is intended. Public write is the one that matters. An anonymous principal allowed to PutObject or DeleteObject is reported as critical, because that bucket will be found and used by somebody else.
Can it tell me which permissions I actually need?
Not from the document, and it does not pretend to. Whether a permission is needed depends on the calls your workload makes, which a policy does not record. The honest answer to that question comes from CloudTrail: filter on the role or user for a couple of weeks and you have the real list. That is the work behind a proper least-privilege rewrite, and it is why the rewrite is a conversation rather than a button on this page.
Does it account for Block Public Access, SCPs or permission boundaries?
No. It reads one document with no access to your account, so it cannot see any of them. That cuts both ways. S3 Block Public Access may already be stopping a public-looking bucket policy from doing anything, and a service control policy or permission boundary may be cutting an over-broad policy back before it is ever consulted. Equally, a tight policy sitting beside a loose one on the same identity is a loose policy, because permissions in AWS are the sum of everything attached.
What kinds of policy can I paste?
Identity policies, resource policies such as S3 bucket policies, and role trust policies. It works out which one it is reading and says so at the top of the report, because the rules genuinely differ — a Principal element is required on one and not allowed on another, and judging a bucket policy by identity-policy rules would be wrong in both directions. Paste one document at a time.
Does this replace IAM Access Analyzer?
No. Access Analyzer is inside your account, so it can do things this cannot — it sees what is actually attached, and it can tell you which permissions have gone unused. If you are in AWS, turn it on. This is for the moment before that: a policy in a pull request, one a vendor has sent you, or one you found in a repository and want read before it is applied to anything.
What is an ExternalId, in plain terms?
It is a value that you and a third party both know, which their systems must send when they assume a role in your account. It is not a password and it does not need to be secret. What it does is stop somebody else asking that same vendor to assume your role on their behalf, which the vendor would otherwise have no way to refuse. That is the confused deputy problem, and the ExternalId is the standard answer to it.
How often can I run this?
Thirty times an hour from one address, which is deliberately generous because nothing here touches anyone else's server. The expected way to use it is to narrow a policy, paste it again and check that it cleared, and a tight limit would make that unpleasant. Documents up to 64 KB are accepted, which is around ten times the largest managed policy AWS will take.