IAM stands for Identity and Access Management. Most explanations throw four terms at you at once — users, roles, policies, permissions — and expect it to click. It doesn't, because the terms describe different layers of the same question: who is asking, and what are they allowed to do? Separate those two questions first, and the rest falls into place.
Identity: who is asking
An IAM User is a permanent identity, usually mapped to one human or one application, with long-lived credentials (a password for console login, or access keys for programmatic/API access). An IAM Role is different in a way that trips people up: a role has no permanent credentials at all. It's an identity that something else assumes temporarily — an EC2 instance, a Lambda function, or another AWS account — getting short-lived, automatically-rotating credentials for the duration it's assumed.
Continue reading
Unlock the full breakdown of policies, the dual-check evaluation model, and the least-privilege mistakes that actually matter.
Why roles exist instead of just using users everywhere
If an EC2 instance needed to read from S3, the naive approach is to generate an IAM User's access keys and hardcode them into the application running on that instance. This is a real security liability: those keys are permanent until manually rotated, and if the instance (or its code, or a misconfigured log) leaks them, the credentials are valid until someone notices and revokes them.
A role attached to that same EC2 instance instead hands the instance temporary security credentials through the instance metadata service, automatically rotated roughly every few hours by AWS, with no key ever written to disk or hardcoded anywhere. This is why "use roles, not user access keys, for anything running inside AWS" is treated as a near-absolute rule in production environments, not just a suggestion.
Permission: what they're allowed to do
A Policy is a JSON document that states permissions explicitly — which actions, on which resources, are Allowed or Denied. Policies attach to users, groups, or roles. A minimal policy statement looks like this:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-bucket/*"
}
]
}
This single statement says: allow the s3:GetObject action (reading an object) on any object inside my-bucket, and nothing else. No write access, no delete access, no access to any other bucket. That specificity — naming the exact action and the exact resource ARN — is what "least privilege" means in practice, not as an abstract principle.
The dual-check model: how AWS actually evaluates a request
This is the part almost nobody explains clearly, and it's the single most useful mental model for debugging "access denied" errors. Every API request in AWS is evaluated against two separate policy types, and both have to allow the action, or it's denied:
- Identity-based policies — attached to the user or role making the request. "Does this identity have permission to do this?"
- Resource-based policies — attached to the resource being accessed (an S3 bucket policy, for example). "Does this resource allow this identity to do this to it?"
If either side says no — or simply doesn't say yes — the request is denied. An identity with full S3 permissions can still be blocked from reading a specific bucket if that bucket's own policy explicitly denies that identity or doesn't grant it access. This is exactly why cross-account access to an S3 bucket requires changes on both sides: the requesting account's IAM policy must allow the action, and the bucket's resource policy must also allow that specific account or role.
Explicit deny always wins
One more rule sits on top of the dual-check model: if any applicable policy contains an explicit Deny for an action, that Deny overrides every Allow, from any source, anywhere in the evaluation. There is no policy that can override an explicit Deny — this is intentional, and it's how AWS lets you build hard guardrails (like Service Control Policies in AWS Organizations) that no individual account's permissions can bypass.
Groups: the practical shortcut
An IAM Group is just a named collection of users that a policy can attach to once, instead of attaching the same policy to every user individually. Groups have no credentials of their own and can't be "assumed" the way a role can — they exist purely to make managing permissions for many similar users less repetitive.
The mistake that shows up most often
Attaching the AWS-managed AdministratorAccess policy to a user or role "temporarily, just to get something working" and then never revisiting it. This grants Action: "*" on Resource: "*" — every action, on every resource, with no boundary at all. It's the opposite of least privilege, and because it works for literally everything, there's rarely a forcing function that makes someone go back and scope it down later. The fix is mechanical, not clever: start with zero permissions, add specific Allow statements only for the actions an STS identity-verification check or a CloudTrail log shows the identity actually using, and revisit it as the workload's real needs become clear.
Where this connects to real boto3 work
Calling sts.get_caller_identity() is the fastest way to confirm exactly which identity (user or assumed role) a script or instance is currently running as before debugging a permissions issue — it removes the guesswork of "which credentials is this even using right now," which is usually step one when an "access denied" error doesn't make sense at first glance.
IAM access denied errors are almost never about one missing permission. They're about which side of the dual-check — identity policy, resource policy, or an explicit deny sitting somewhere in between — never said yes.