Project Overview
Every AWS account has a question it needs to answer before anything else: who gets access to what?
In this project, I'll use AWS Identity and Access Management (IAM) to control exactly that; setting up users, groups, and policies that define what actions are allowed and on which resources. IAM sits at the foundation of cloud security, and getting it right is what separates a well-architected environment from one that's one misconfigured permission away from a serious incident.
It's a skill companies care about deeply enough that entire roles like IAM Engineers and Cloud Security Engineers exist around it. Today, I'm building that foundation from the ground up.
Tools and Concepts
Services I used:
Amazon EC2 to provision virtual server instances, AWS IAM to control access through users, groups, and policies, and the IAM Policy Simulator to validate permissions without touching live infrastructure.
Key concepts I learned:
Least privilege : granting users only the access they need, nothing more.
Tag-based access control : using Env: development and Env: production tags as policy conditions to enforce hard environment boundaries.
IAM policy structure : how Effect, Action, Resource, and Condition work together in JSON, and why explicit Deny always overrides Allow.
Group-based permission management : attaching policies to groups rather than individual users for scalable, consistent access control.
Policy simulation : validating IAM logic safely before it affects real resources.
Project Reflection
This project took me approximately 2 hours to complete, including the secret mission.
The most challenging part was understanding the IAM policy JSON rather than just copying and running it. Breaking down each statement; why the Condition block uses tag values to gate access, and why the explicit Deny on DeleteTags and CreateTags exists as a separate guardrail. It took deliberate effort to fully grasp. Getting that detail right required thinking like an attacker, not just a builder.
It was most rewarding to see the policy work end-to-end: from a JSON document I dissected and understood, to a real authorization decision in the AWS console, to a confirmed simulation result in the IAM Policy Simulator. That full loop from comprehension to validation is exactly what cloud security work looks like in practice.
Step 1: Tags
What I did in this step
To simulate a realistic cloud environment, I launched two EC2 instances; one tagged as a production server and one as a development server. This environment tag distinction is central to the IAM project: later, I configured policies that control which users can access which instance based on these tags, reflecting how access control works in real engineering teams where developers shouldn't have unrestricted access to production infrastructure.

EC2 launch screen showing Env tags
Understanding tags
Tags are key-value labels you can attach to AWS resources for organisation and management. A tag has two parts: a Key (the category) and a Value (the specific label). For example: Env : production.
They're useful for three things:
Organisation : filter and identify resources quickly across a large account without scrolling through everything manually.
Cost allocation : track spending by team, project, or environment using AWS Cost Explorer.
Policy enforcement : IAM policies can condition access based on tag values, so you can allow actions on development resources while blocking the same actions on production.
That third use is the focus of this project.
My tag configuration
The tag key I've used on both EC2 instances is Env, short for Environment. The values I've assigned are production for the first instance and development for the second, distinguishing the two environments that my IAM policies will later control access to.
Step 2: IAM Policies
What I did in this step
Next, I created an IAM policy that grants access to the development EC2 instance only.
The reasoning is straightforward: engineers testing new changes should never have unrestricted access to production infrastructure. A misconfigured script, an accidental stop command, or a wrong setting pushed during testing could take down a live environment. By scoping permissions to the development instance through an IAM policy, we enforce a hard boundary; testers can do their work freely without the risk of touching production.
This is the principle of least privilege in practice: give users exactly the access they need, and nothing more.
Understanding IAM policies
An IAM policy is a document that defines what actions are allowed or denied on which AWS resources, and under what conditions. Every permission in AWS flows through a policy; nothing is granted by default.
In plain terms: policies answer the question "can this user do this thing to this resource?" They can be attached to users, groups, or roles, and they evaluate every API call made in your account against a set of Allow and Deny rules.
In this project, my policy specifically allows actions on EC2 instances where the Env tag equals development, meaning the same user would be blocked from performing those same actions on the production instance.
The policy I set up
I created the policy using the JSON editor rather than the visual editor. JSON (JavaScript Object Notation) is the native format AWS uses to store and evaluate IAM policies. While the visual editor is a helpful starting point for beginners, writing directly in JSON gives you precise control over the policy structure, and more importantly, it's how policies are managed in real engineering workflows, through infrastructure-as-code tools like Terraform or AWS CloudFormation.
Policy effect
This policy has three statements that work together:
1. Allow all EC2 actions on development resources: the ec2:* wildcard grants full EC2 permissions, but only where the resource carries the tag Env: development. Any action attempted on the production instance is implicitly denied.
2. Allow read-only visibility across all instances: ec2:Describe* lets the user see all EC2 instances regardless of environment. This is necessary so they can actually view the AWS console without getting a blank screen; but seeing is not the same as touching.
3. Deny tag modification on all resources: ec2:CreateTags and ec2:DeleteTags are explicitly denied across the board. This is a critical guardrail: without it, a user could simply retag the production instance as development and bypass the first rule entirely.
Together, these three statements enforce a clean boundary; full working access to development, zero access to production, and no way to game the tagging system to escalate privileges.
Understanding Effect, Action, and Resource
The Effect, Action, and Resource attributes of a JSON policy define the three core elements of every permission statement.
Effect determines whether access is granted or blocked; it can only be Allow or Deny. When both apply to the same action, Deny always takes precedence.
Action specifies which AWS API calls the statement covers; for example, ec2:* for all EC2 actions, or ec2:Describe* for read-only operations only.
Resource defines which AWS resources the statement applies to. A * targets all resources, but this can be scoped to a specific resource using its ARN (Amazon Resource Name). In this project, resource-level targeting is handled through the Condition block instead, using the Env tag to distinguish development from production.
Think of a policy statement as a sentence: "[Allow/Deny] this [Action] on this [Resource]." The Condition block is what adds the "but only if..." clause, making policies precise rather than broad.
My JSON Policy
json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "ec2:*",
"Resource": "*",
"Condition": {
"StringEquals": {
"ec2:ResourceTag/Env": "development"
}
}
},
{
"Effect": "Allow",
"Action": "ec2:Describe*",
"Resource": "*"
},
{
"Effect": "Deny",
"Action": [
"ec2:DeleteTags",
"ec2:CreateTags"
],
"Resource": "*"
}
]
}
Step 3: Account Alias
What I did in this step
Next, I created an account alias for my AWS account.
By default, the AWS IAM sign-in URL contains your account's numeric ID; a 12-digit number that's difficult to share and easy to mistype. An account alias replaces that with a human-readable name, making the sign-in URL clean and recognisable.
This matters in practice: when onboarding a new team member or sharing console access with an intern, you want to hand them a URL like https://intern-alias-fafa.signin.aws.amazon.com/console rather than a string of numbers. It's a small change with a real impact on how professionally access is managed.
Understanding account aliases
An account alias is a custom, human-readable name assigned to your AWS account that replaces the default 12-digit account ID in the IAM sign-in URL.
Instead of signing in at:
https://123456789012.signin.aws.amazon.com/console
You get:
https://intern-alias-fafa.signin.aws.amazon.com/console
Beyond readability, an alias signals professionalism. It's what you'd configure in a real company environment before sharing console access with a new team member or contractor. It also reduces the risk of someone accidentally signing into the wrong account when managing multiple AWS environments.
Setting up my account alias
Setting up the account alias took just a few minutes; one of the quickest wins in this project. My AWS console sign-in URL is now https://intern-alias-fafa.signin.aws.amazon.com/console. Clean, readable, and ready to share.

IAM dashboard showing alias confirmed
Step 4: IAM Users and User Groups
What I did in this step
Next, I created a dedicated IAM user group for interns and added a new IAM user to it.
Groups are how permissions scale cleanly in AWS. Rather than attaching policies to individual users, which becomes unmanageable as a team grows, you attach the policy once to the group, and every user in that group inherits it automatically. Onboard a new intern? Add them to the group. Remove someone's access? Remove them from the group. No policy editing required.
The IAM user I created represents our new intern: a separate identity with its own login credentials, scoped entirely to the permissions defined by the group policy; nothing more.
Understanding user groups
An IAM user group is a collection of IAM users who share the same permissions. Rather than attaching a policy to each user individually, you attach it once to the group and every member inherits those permissions automatically.
This is what makes access management scalable. In a real engineering team, you might have groups like developers, interns, or ops-team, each with a policy tailored to their level of access. Adding or removing a user from a group is all it takes to grant or revoke their permissions; no policy changes needed.
In this project, my interns group is attached to the IAM policy I created earlier, which scopes all access to the development EC2 instance only.
Attaching policies to user groups
I attached the policy I created earlier to the interns user group, which means every user in this group automatically inherits the same permissions; full access to the development EC2 instance, read-only visibility across all instances, and a hard block on modifying any resource tags.
This is the power of group-based access management: the policy is defined once and applied consistently across every member. If I onboard five more interns tomorrow, I simply add them to the group; no need to touch the policy or configure permissions user by user.
Understanding IAM users
IAM users are unique identities created within an AWS account, each representing a specific person or application that needs access to AWS resources. Every IAM user has their own login credentials. Sharing the root account credentials is a serious security risk, so each user gets their own login through the account alias URL: https://intern-alias-fafa.signin.aws.amazon.com/console
In this project, I created an IAM user named fafa-dev-InternFafa to represent our new intern. Their permissions are inherited directly from the interns group, meaning their access is already scoped to the development environment before they log in for the first time.
A key security detail: AWS displays the generated password only once at the point of user creation; a deliberate design choice that enforces secure credential handoff rather than leaving passwords retrievable indefinitely.

User created / retrieve password screen
Step 5: Logging in as an IAM User
Sharing sign-in details
There are two ways to share a new user's sign-in details:
Download as a .csv file: directly from the AWS console at the point of user creation. This file contains the sign-in URL, username, and generated password; everything the new user needs to log in.
Email the sign-in instructions: using the "Email sign-in instructions" option in the console. AWS generates a pre-formatted email containing the account sign-in URL and username, which you can send straight to the new user.
Both options are only available immediately after user creation. Once you navigate away, the password is no longer retrievable; which is why it's critical to share credentials securely at the point of setup, not after the fact.
Observations from the IAM user dashboard
Once I logged in as fafa-dev-InternFafa, the dashboard immediately showed the policy in action. The Applications widget displayed an Access denied to servicecatalog:ListApplications error, and the Cost and usage widget showed Access denied across both current month and cost breakdown; services the intern has no business accessing.

Dashboard with access denied errors
This is exactly what we want to see. The policy attached to the interns group doesn't grant broad AWS console access; it scopes permissions specifically to EC2 actions on the development instance. Any service outside that scope hits an implicit deny: IAM blocks everything that isn't explicitly allowed.
Those red access denied banners aren't errors. They're the policy working as designed.
Step 6: Testing IAM Policies
What I did in this step
I logged into the AWS console as the intern IAM user and tested access against both EC2 instances; attempting actions on the production instance and the development instance separately.
This is an important validation step. Creating a policy and attaching it to a group is only half the work; you need to verify that the permissions behave exactly as intended before handing credentials to a real user. A misconfigured policy that accidentally grants production access would undermine everything we've set up so far.
By testing from the intern's perspective, I can confirm that the principle of least privilege is actually enforced, not just configured.
Testing policy actions
I tested the policy by attempting to stop both EC2 instances as fafa-dev-InternFafa. The results confirmed the policy is working exactly as intended.
Two instances, two different tags, two completely different outcomes; from the same user, under the same policy. That's tag-based access control working in practice.
Stopping the production instance
When I tried to stop Prod-server-Fafa (i-0c2e3f0f509ccdf5e), the action failed immediately with an authorisation error: You are not authorized to perform this operation.
This was because the IAM policy attached to the interns group only allows EC2 actions on resources tagged Env: development. The production instance carries the tag Env: production, so the policy's condition check fails; and AWS blocks the action entirely before it can execute.
The error message itself is worth noting: it references fafa-dev-InternFafa by name and explicitly states that no identity-based policy allows the ec2:StopInstances action on that resource. AWS isn't just blocking the action; it's telling you exactly why, which is what makes IAM errors useful for debugging in real engineering environments.

Red authorisation error screen
Stopping the development instance
Next, when I tried to stop Dev-server-Fafa (i-057461c1ed516a0e4), it worked perfectly. The console confirmed Successfully initiated stopping of i-057461c1ed516a0e4 and the instance state immediately shifted to Stopping.
This was because the development instance carries the tag Env: development, which satisfies the condition in the IAM policy. AWS evaluated the request, matched the tag value, and allowed the ec2:StopInstances action to execute without restriction.
This is the exact outcome the policy was designed to produce; full working access to development, zero access to production, enforced automatically through tags rather than manual permission checks.

Green success banner + Stopping state
Step 7: IAM Policy Simulator (Secret Mission 🔐)
To extend this project, I used the IAM Policy Simulator to validate the intern's permissions without affecting any live AWS resources.
Directly testing permissions on real infrastructure, as I did in the previous step, isn't sustainable in a production environment. Every unnecessary instance stop risks disrupting running workloads and other engineers. The Policy Simulator lets me verify exactly what a user can and cannot do against any action or resource, without executing a single real API call. It's a faster, safer, and more professional approach to permission validation.
Understanding the IAM Policy Simulator
The IAM Policy Simulator is a tool that lets you test and validate IAM policies against specific actions and resources without executing anything in your actual AWS environment.
It's useful for verifying that a policy behaves exactly as intended before it affects real infrastructure. In a real team environment where policies are updated frequently; onboarding new roles, adjusting permissions, responding to a security incident; the Policy Simulator is the difference between testing safely and finding out something is misconfigured in production.
How I used the simulator
I set up a simulation for FafaDevEnvironmentPolicy, testing two EC2 actions; DeleteTags and StopInstances with the ec2:resourcetag/env context key set to development.
StopInstances came back allowed, confirming the policy permits actions on development-tagged resources. DeleteTags came back denied; the explicit Deny statement holds regardless of environment, blocking any attempt to retag resources and bypass access controls.
I had to adjust the simulation context manually by adding the ec2:resourcetag/env: development key-value pair under the StopInstances action. Without that context, the simulator has no tag information to evaluate the condition against and would return a denied result even for actions that should be allowed. This is an important nuance: the Policy Simulator only knows what you tell it about the resource context.
One simulation, two actions, zero infrastructure touched; and complete confidence that the policy is enforcing the right boundaries.

Policy Simulator results screen
⚠️ Don't Forget to Clean Up
If you followed along with this project, make sure to delete your AWS resources when you're done to avoid unexpected charges:
- Terminate both EC2 instances : go to EC2 > Instances > select both > Instance State > Terminate
- Delete the IAM user : go to IAM > Users > select fafa-dev-InternFafa > Delete
- Delete the IAM user group : go to IAM > User Groups > select interns > Delete
- Delete the IAM policy : go to IAM > Policies > filter by Customer managed > select FafaDevEnvironmentPolicy > Delete
- Remove the account alias — go to IAM > Dashboard > Delete next to your alias

_Deletion/Termination of instances _
AWS's free tier covers most of what we used here, but EC2 instances left running will accumulate charges. When in doubt, clean up.
Why I Did This Project
I chose to do this project today because cloud security is a non-negotiable foundation for the DevOps engineering path I'm building toward. IAM sits at the centre of every AWS environment; understanding how to design and validate access control policies isn't optional, it's foundational.
This project gave me hands-on experience with the exact skills that show up in cloud and DevSecOps job descriptions: least privilege, policy design, tag-based access control, and permission validation.
This project met my goals. I came in wanting to understand IAM beyond the theory, and I'm leaving with a working policy, a tested environment, and documentation I can publish and point to.
Something that would make learning with NextWork even better is more projects that chain together; where the IAM setup you build here feeds directly into a subsequent project on CloudTrail logging or AWS Config compliance rules. The security story feels most powerful when you see how the pieces connect.



Top comments (0)