Introduction
AWS IAM can look simple when you first learn it: create a user, attach a policy, and start using AWS. That approach, however, does not scale well for production environments.
Modern AWS environments rely heavily on IAM roles and temporary credentials instead of distributing long-lived IAM access keys. A role allows an identity or workload to obtain temporary security credentials with a defined set of permissions. AWS Security Token Service (STS) provides those temporary credentials through operations such as AssumeRole.
In this AWS IAM tutorial, we will build that concept around a practical scenario. A developer or EC2 workload needs permission to perform administrative EC2 operations, but those operations should only be allowed when the request comes from a trusted public IP address.
The goal is not simply to create an IAM role. The goal is to understand how trust policies, permissions policies, STS AssumeRole, conditions, and IAM policy evaluation work together.
What is AWS IAM?
AWS Identity and Access Management (IAM) controls who can access AWS resources and what they are allowed to do. A policy defines permissions using elements such as:
EffectActionResourceCondition
For example:
{
"Effect": "Allow",
"Action": "ec2:*",
"Resource": "*"
}
This statement allows EC2 actions against resources covered by the statement. However, permissions are not the only part of IAM. When you work with IAM roles, you also need to understand the trust policy.
A trust policy answers: Who is allowed to assume this role?
A permissions policy answers: What can this role do after it has been assumed?
This distinction is one of the most important IAM concepts to understand. AWS evaluates the permissions attached to the assumed role when temporary credentials are used.
IAM Roles vs IAM Users
IAM users traditionally represent individual identities, while IAM roles are designed for temporary access and delegation.
For example, instead of putting an AWS access key and secret key on a developer’s laptop or inside an application, the developer can assume a role and receive temporary credentials.
The same principle applies to AWS workloads.
An EC2 instance can use an IAM role. Lambda functions can use execution roles. Other AWS compute and orchestration services can also operate with IAM roles.
This gives you a cleaner architecture:
Human / Workload
|
| AssumeRole
v
IAM Role
|
v
Temporary Credentials
|
v
AWS Services
The permissions of the temporary credentials come from the role being assumed, subject to any additional session restrictions.
What is AWS STS AssumeRole?
AWS Security Token Service, commonly called AWS STS, provides temporary security credentials. The AssumeRole operation allows a trusted principal to obtain temporary credentials for an IAM role.
A typical CLI command looks like this:
aws sts assume-role --role-arn "arn:aws:iam::YOUR_ACCOUNT_ID:role/DevOps-Operator-Role" --role-session-name ClassroomDemoSession"
AWS returns temporary credentials containing:
{
"Credentials": {
"AccessKeyId": "ASIA...",
"SecretAccessKey": "...",
"SessionToken": "..."
}
}
The default AssumeRole session duration is one hour. IAM roles can have a maximum session duration of up to 12 hours, depending on configuration. However, role chaining is limited to a maximum of one hour. AWS Documentation
The important idea is that these are temporary credentials rather than permanent credentials that remain valid indefinitely.
Real Production Scenario
Consider a development environment where a DevOps engineer needs to manage EC2 instances.
They may need to:
- Start an EC2 instance
- Stop an EC2 instance
- Reboot an EC2 instance
- Perform other EC2 administrative operations
However, the company has a security requirement:
EC2 administrative operations should only be performed from a trusted network.
This is where IAM roles, STS, conditions, and explicit Deny become useful.
Our architecture is:
Developer / EC2
|
| sts:AssumeRole
v
DevOps-Operator-Role
|
| EC2 permissions
v
Amazon EC2
The role contains two important policy statements.
The first allows EC2 operations:
{
"Sid": "AllowEC2Operations",
"Effect": "Allow",
"Action": "ec2:*",
"Resource": "*"
}
The second statement applies an explicit Deny when the request does not originate from the trusted public IP address:
{
"Sid": "ExplicitDenyIfNotOnCorporateNetwork",
"Effect": "Deny",
"Action": "ec2:*",
"Resource": "*",
"Condition": {
"NotIpAddress": {
"aws:SourceIp": [
"YOUR_PUBLIC_IP/32"
]
}
}
}
The aws:SourceIp condition key evaluates the public IP address from which the request originates. AWS documents that aws:SourceIp is intended for public IP ranges; private VPC source IP controls use different mechanisms such as aws:VpcSourceIp.
Why Use Explicit Deny?
This is where IAM policy evaluation becomes important.
At a high level, you can think about IAM authorization like this:
Implicit Deny
|
v
Default = denied
Explicit Allow
|
v
Request can be allowed
Explicit Deny
|
v
Overrides an applicable Allow
AWS states that an applicable explicit Deny overrides an Allow. An implicit deny occurs when no applicable Allow exists and there is no explicit Deny. AWS Documentation
In our example, the role has:
Allow → ec2:*
But it also has:
Deny → ec2:*
Condition → Source IP is NOT trusted
Therefore, when the request originates from the trusted IP:
Allow EC2
+
Deny condition does not match
=
ACCESS ALLOWED
From an untrusted IP:
Allow EC2
+
Explicit Deny matches
=
ACCESS DENIED
This is a useful production pattern because the Deny statement acts as a guardrail around the broader permission.
Creating the IAM Role
The role is called:
DevOps-Operator-Role
Its trust policy allows principals from the specified AWS account to assume it:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::YOUR_ACCOUNT_ID:root"
},
"Action": "sts:AssumeRole"
}
]
}
The important thing to understand is that this is the trust policy, not the permission policy.
It establishes who is trusted to assume the role.
After the role is successfully assumed, its attached permissions determine what the resulting temporary credentials can do.
Trust Policy vs Permissions Policy
This distinction becomes much easier when you think of the two policies as answering different questions.
Trust Policy
WHO can assume this role?
Example:
AWS account
IAM user
IAM role
AWS service
Federated identity
Permissions Policy
WHAT can this role do?
Example:
ec2:*
s3:GetObject
dynamodb:GetItem
ssm:SendCommand
So the overall flow is:
Trust Policy
|
WHO can enter?
|
v
IAM Role
|
Permissions
|
WHAT can it do?
|
v
AWS APIs
A role therefore needs both a trust relationship and permissions appropriate to its workload or purpose.
Live Demo: Proving the Identity
Before assuming the role, the current identity can be checked with:
aws sts get-caller-identity
For example:
arn:aws:iam::YOUR_ACCOUNT_ID:role/...
After assuming the role and configuring the returned temporary credentials, run:
aws sts get-caller-identity
The result should show an assumed-role identity similar to:
arn:aws:sts::YOUR_ACCOUNT_ID:assumed-role/DevOps-Operator-Role/ClassroomDemoSession
This proves that the CLI is now operating under the temporary role session.
AWS role permissions are not cumulative with the permissions of the identity that assumed the role. When you assume a role, the active role session operates using the permissions assigned to that role, subject to the applicable policy evaluation rules.
Test 1: Implicit Deny
After assuming DevOps-Operator-Role, try:
aws s3 ls
The role does not contain an S3 permission such as s3:ListAllMyBuckets.
Therefore AWS returns an access-denied response.
This demonstrates implicit deny:
DevOps-Operator-Role
|
| s3:ListAllMyBuckets
v
No applicable Allow
|
v
Implicit Deny
AWS describes this as the default IAM behavior: when there is no applicable Allow and no explicit Deny, the request is implicitly denied.
Test 2: EC2 From the Trusted IP
Now perform an EC2 operation while connected through the trusted public IP.
The EC2 Allow statement matches, and the Deny condition does not.
Result:
EC2 request
|
v
Allow matches
|
v
Deny condition = false
|
v
ACCESS ALLOWED
Test 3: EC2 From an Untrusted IP
Now change the network so the request comes from an IP outside the configured trusted range.
The Allow statement still exists.
However, the explicit Deny now matches:
EC2 request
|
v
Allow matches
|
v
Source IP is NOT trusted
|
v
Explicit Deny matches
|
v
ACCESS DENIED
This is the key lesson.
Having an Allow does not guarantee access when an applicable explicit Deny also applies. AWS IAM gives explicit Deny precedence over Allow.
Why This Matters in Production
Production IAM design should not rely on one permission statement alone.
A role may need a relatively broad permission set to perform its job, but additional conditions and guardrails can reduce where and how those permissions may be used.
For example:
IAM Role
|
+-- What can it do?
| |
| +-- EC2 permissions
|
+-- Who can assume it?
| |
| +-- Trust Policy
|
+-- Where can it be used?
|
+-- Condition Keys
+-- Network restrictions
+-- Other guardrails
This approach moves IAM design beyond simply asking:
“Does this user have permission?”
Instead, production security asks:
“Who is making the request, what are they trying to do, which resource are they accessing, and under what conditions?”
That is where IAM becomes a real authorization system rather than just a collection of policies.
Key Takeaways
AWS IAM roles provide a better foundation for temporary access than distributing long-lived credentials. STS AssumeRole provides temporary security credentials, while the role’s policies determine what those credentials can do.
The most important concepts from this demo are:
Trust Policy
→ Who can assume the role?
Permissions Policy
→ What can the role do?
STS AssumeRole
→ How do we obtain temporary credentials?
Implicit Deny
→ Nothing is allowed unless an applicable Allow exists.
Explicit Deny
→ Overrides an applicable Allow.
Condition Keys
→ Add additional controls such as network-based restrictions.
The practical goal is simple: give identities only the permissions they need, use temporary credentials whenever possible, and add conditions or explicit Denies when a permission needs an additional security boundary.


Leave a Reply