Updated recently
Last updated:
Quick Answer: The AWS IAM roles policies distinction, in one line: an IAM role is an identity with no permanent credentials — a trust policy defines who can assume it, and permission policies define what it can do once assumed. Almost every serious AWS breach traces back to one of a handful of errors: wildcard permissions, long-lived access keys left in code, missing conditions on cross-account trust, and nobody reviewing who can assume what. Fixing those four removes most of the risk.
Roles, Users and Policies: What Is Actually the Difference?
Interviewers ask this in some form in almost every cloud security round, and the answer reveals whether someone has thought about identity or just read a checklist.
| Concept | What It Is | When to Use It |
|---|---|---|
| IAM user | A permanent identity with credentials (password, access keys) | Barely at all in 2026 — legacy and a small number of exception cases |
| IAM role | A assumable identity with temporary credentials | Everything: humans, services, cross-account access, workloads |
| Trust policy | An identity-based policy deciding who may assume the role | Every role has exactly one |
| Permission policy | Defines what the role can do | Attached to the role, or passed as a session policy |
| Permission boundary | A maximum ceiling on a role’s permissions | When you delegate role creation to others |
| Service control policy (SCP) | Organisation-wide ceiling on accounts | Guardrails at the AWS Organizations level |
The shift from users to roles is complete for well-run accounts. Long-lived credentials are the problem: access keys do not expire, they leak through git history, laptop backups and CI logs, and nobody notices them for months. Temporary credentials obtained by assuming a role expire in an hour by default. If your account still has human users with access keys, that is your first fix, not your last.
How AWS Actually Evaluates Permissions
The evaluation logic is simpler than people fear and more important than most engineers know. In order:
- An explicit deny anywhere — identity policy, resource policy, SCP, session policy or permissions boundary — wins. Always. There is no override.
- If there is no explicit deny, an SCP or permissions boundary must allow the action. These only restrict; they never grant.
- Then the relevant identity or resource policy must allow it. For cross-account access, both the calling identity’s policy and the resource’s policy must permit it.
That second point is where designs break. Attaching an SCP full of "Effect": "Allow" statements does nothing on its own — you are writing restrictions, not permissions. The flip side is that "NotAction" and wildcard statements in an SCP can silently permit far more than intended once the identity policy allows something.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowReadOnlyAccountInventory",
"Effect": "Allow",
"Action": [
"iam:GetCredentialReport",
"iam:ListRoles",
"iam:ListUsers",
"iam:ListPolicies",
"iam:GetPolicyVersion"
],
"Resource": "*"
},
{
"Sid": "DenyDisablingCloudTrailOrgWide",
"Effect": "Deny",
"Action": [
"cloudtrail:StopLogging",
"cloudtrail:DeleteTrail"
],
"Resource": "*"
}
]
}
The read-only statement above is a legitimate use of "Resource": "*" — many IAM actions do not support resource-level scoping, so a wildcard there is unavoidable. The skill is knowing which actions support resource ARNs and scoping those, rather than slapping * on services where scoping is possible. An S3 policy that could be bucket-scoped and is not is a finding in every audit.
Writing a Trust Policy That Is Actually Safe
The trust policy is the part people paste from a tutorial and never revisit, which is exactly how cross-account roles become open doors. The dangerous version trusts an account ID with no conditions — anyone with credentials in that account who can create a principal can attempt the assumption, and if the external account is compromised, so is your role.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::111122223333:root" },
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "client-acme-2026",
"aws:PrincipalTag/team": "data-platform"
},
"Bool": { "aws:MultiFactorAuthPresent": "true" }
}
}
]
}
Three conditions doing real work there. ExternalId is mandatory for any third-party or cross-account integration — it stops the confused-deputy problem where an attacker tricks your role into granting access to someone else’s resource. PrincipalTag restricts which principals inside the trusted account may assume it. MultiFactorAuthPresent ensures human assumptions happen behind MFA. A trust policy without conditions is not a policy; it is a formality.
The Mistakes That Show Up in Real Breaches
| Mistake | How It Happens | The Fix |
|---|---|---|
Action wildcards (s3:*) |
Copied from a tutorial to “get it working” | Enumerate actions; use Access Analyzer findings |
| Access keys in git | Local testing, committed once, never rotated | Rotate on commit detection, use roles in CI |
| Cross-account role without conditions | Account ID alone treated as security | ExternalId plus principal tags or org condition |
iam:PassRole with wildcard |
Needed for a service, scoped too widely | Restrict to specific role ARNs and services |
| Root account used for daily tasks | Convenience during setup, never stopped | Alarm on root usage, MFA and hardware keys |
| No permission boundaries on delegated roles | Teams create their own roles unchecked | Boundary limiting maximum permissions |
| Unused roles and stale keys | Contractors, leavers, forgotten CI users | Credential report review every month |
| No alerting on IAM changes | CloudTrail on, nobody reading it | EventBridge rule on ConsoleLogin and policy changes |
| Public S3 with broad bucket policy | Quick share, permanent exposure | Block Public Access at the account level |
The PassRole row deserves expansion because it is the mechanism behind multiple headline incidents. A principal that can pass an overly permissive role to a service — Lambda, Glue, SageMaker — effectively inherits that role’s permissions without ever being granted them. Scoping PassRole to specific role ARNs, and to specific services via iam:PassedToService, closes one of the most common privilege-escalation paths in AWS.
The Incident That Started With One Access Key
A mid-size analytics firm found out in 2025 what the table looks like end to end. A developer had created an access key in 2022 for a side script, pasted it into a config file, and pushed the repository to GitHub as public “for portfolio purposes”. The key sat there for two and a half years — no alert existed to notice it.
An automated scanner found it within days of publication. The account had no SCPs, the user’s policy was a copy of AdministratorAccess “just until the setup is finished”, and the user had never been removed. Within hours the key was spinning up GPU instances in another region, and the only signal was an unexpected cost notification two days later — because nothing was logging to a place anyone watched.
Recovery took a week: deactivate the key, review CloudTrail for every action taken, terminate instances, rotate everything else, then rebuild the account’s identity model properly — roles with boundaries, an SCP preventing region surprises and disabling CloudTrail, no long-lived human keys. The bill for the compute was five figures in dollars. The reputation cost with the client whose data had been touched was the part nobody could quantify. The uncomfortable detail: the exploit required no skill at all. It was a scan of public repositories and a policy nobody had reviewed since 2022. Related defensive salary context is in AWS Cloud Security Engineer Salary India 2026, where IAM depth is consistently the highest-paying security skill.
The Checks You Should Be Running Monthly
# Credential report: every user, key age, last used
aws iam generate-credential-report
aws iam get-credential-report --query 'Content' --output text | base64 -d
# Would this policy allow what I think it allows?
aws iam simulate-custom-policy
--policy-source-arn arn:aws:iam::123456789012:role/app-deploy
--action-names s3:DeleteBucket iam:CreateUser
--resource-arns "*"
# Who can actually assume this role?
aws iam list-roles --query "Roles[?AssumeRolePolicyDocument!=null].[RoleName]" --output table
Three commands that answer the questions an auditor will ask: who has credentials that have never been used, does this policy do what I believe, and which roles are assumable by whom. Pair them with IAM Access Analyzer’s unused-access findings, which will tell you which permissions have not been exercised in 45 days — the fastest route to genuinely least-privilege policies without breaking anything.
Implementing Least Privilege Without Slowing Everyone Down
The reason wildcard policies persist is that strict least privilege on day one blocks development, so teams give up. The workable sequence is gradual. Start with sensible service-level policies plus a permissions boundary on every role someone else creates. Turn on Access Analyzer and let it accumulate 60 days of real usage data. Then tighten, action by action, starting with destructive and identity permissions — IAM, CloudTrail, org actions — where over-permissioning is most dangerous. Revisit quarterly rather than never.
Encode the resulting policies in your infrastructure pipeline so a manual console edit becomes visible as drift, the same discipline discussed in Terraform vs AWS CDK vs CloudFormation. Policies reviewed in pull requests get scrutiny; policies clicked into existence at 11 PM do not.
Training Resources
Identity is the highest-value security domain to go deep on. SkilBrill AWS Cloud Security Training covers IAM evaluation, trust policy design, SCPs and incident response hands-on. For the multi-account structures where SCPs live, see AWS Cloud Architecture Training.
FAQ
What is the difference between an IAM role and an IAM policy?
A role is an identity that can be assumed to gain temporary credentials; it has no permanent password or access keys. A policy is a JSON document of permissions that can be attached to a role, a user or a resource. Roles use policies — they are not alternatives.
What does a trust policy control in AWS?
It controls who may assume the role — accounts, users, services or federated identities, optionally with conditions such as ExternalId, MFA or principal tags. It never grants permissions itself; the permission policies attached to the role do that.
Why do most AWS breaches involve IAM?
Because IAM determines blast radius. A single over-permissive policy or leaked long-lived key gives an attacker everything that identity can reach, often across the whole account. Wildcards, stale access keys and unconditioned cross-account trust are the recurring root causes.
Should I use IAM users or roles?
Roles everywhere, including for humans via federation or IAM Identity Center. Long-lived IAM user credentials are the single most common source of leaks, and temporary role credentials expire automatically and are auditable per session.
How do I find over-permissive policies in my account?
Use IAM Access Analyzer policy generation and unused-access findings, plus the credential report for stale keys. The iam simulate-custom-policy command lets you test whether a policy allows an action before you attach it anywhere.
