← Back to Tutorials

AWS IAM

A practical guide to Identity and Access Management — the system that controls who can do what inside your AWS account, and why getting it right matters more than most people realise.

4 Units Price: Free

Unit 1
What Is IAM and Why Does It Exist?

When you first create an AWS account, you log in as the root user. This account has unrestricted access to everything — every service, every setting, every resource, every billing option. There are no guardrails, no limits, and no way to take anything back if something goes wrong. If that root account gets compromised, whoever has it can do anything they want, including deleting all your data, running up a massive bill, or locking you out entirely. This is why you should almost never use the root account for day-to-day work, and why AWS created IAM.

IAM stands for Identity and Access Management. It is the AWS service that lets you create separate identities — users, groups, and roles — and attach specific permissions to each one. Instead of everyone sharing the root login, each person or application gets its own identity with exactly the access it needs and nothing more. A developer working on a project might have access to Lambda and DynamoDB but not to billing or EC2. An automated script might be allowed to read from one specific S3 bucket but nothing else. IAM is the mechanism that makes this kind of precise control possible.

The underlying principle behind IAM is called least privilege, and it is one of the most important ideas in cloud security. The idea is simple: every user, service, and application should have the minimum permissions required to do its job, and not a single permission more. In practice this takes discipline, because it is always easier to give broad access and worry about it later. But broad access is also how most security incidents start — not through sophisticated attacks, but through credentials that had access to things they never needed in the first place.

IAM is a global service in AWS, meaning the users and roles you create exist across all regions. You do not create an IAM user in eu-west-1 and another in us-east-1; there is one set of identities for the entire account. This is worth keeping in mind because it affects how you think about organising permissions as your infrastructure grows.

Unit 2
Users, Groups, Roles, and Policies

IAM has four core concepts that you need to understand before anything else makes sense: users, groups, roles, and policies. They work together, but they serve different purposes and it is easy to mix them up at first.

An IAM user is an identity that represents a person or an application. Users have credentials — either a username and password for logging into the AWS Console, or access keys for programmatic access via the CLI or SDK. Each user is independent, and permissions are attached to them either directly or through groups. In a real project, you would create one IAM user per developer rather than sharing credentials, partly for security and partly because it makes audit logs useful — you can actually see who did what.

Groups are collections of users. Rather than attaching the same permissions to ten different users one by one, you create a group, define what that group can do, and then add users to it. When a new developer joins the team, you add them to the right group and they immediately have the appropriate permissions. When someone leaves, you remove them from the group and their access disappears. Groups do not have credentials themselves — they are just a way to manage permissions in bulk.

Roles are different from users in an important way. A role is not tied to a specific person; it is an identity that can be assumed temporarily by a user, an AWS service, or an external application. The most common use case is giving an AWS service — say, a Lambda function — permission to call other services. You do not create a user for Lambda; you create a role with the right permissions and tell Lambda to assume that role when it runs. Roles are also used for cross-account access and for allowing users from one AWS account to access resources in another.

Policies are the actual documents that define permissions. A policy is a JSON document that specifies what actions are allowed or denied, on which resources, and under what conditions. Policies are attached to users, groups, or roles, and they are what actually controls access. AWS provides a large library of managed policies — pre-written documents for common use cases — but you can also write your own, which is where things get interesting and where the next unit focuses.

AWS IAM documentation: https://docs.aws.amazon.com/IAM/latest/UserGuide/introduction.html
Unit 3
Writing Custom JSON Policies

AWS managed policies cover a lot of ground, but sooner or later you will need to write a custom policy that does exactly what you need and nothing else. IAM policies are written in JSON, and once you understand the structure, they are not as intimidating as they look.

Every policy document has a Version field and a Statement array. The Version is almost always set to "2012-10-17" — this is just the version of the policy language, not a date you need to think about. The Statement is where all the actual permissions live, and it can contain one or more individual statements. Here is the simplest possible policy, one that allows a user to list all S3 buckets in the account:

{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "s3:ListAllMyBuckets", "Resource": "*" } ] }

Each statement has three required fields. Effect is either "Allow" or "Deny" — it tells AWS whether the statement is granting or taking away access. Action is the specific operation being allowed or denied, written as service:operation. Resource is the ARN (Amazon Resource Name) of the specific resource the statement applies to, or a wildcard if it applies to all resources of that type.

Here is a more realistic example — a policy for a Lambda function that needs to read from a specific DynamoDB table and write logs to CloudWatch:

{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:UpdateItem" ], "Resource": "arn:aws:dynamodb:eu-west-1:123456789012:table/my-table" }, { "Effect": "Allow", "Action": [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" ], "Resource": "arn:aws:logs:*:*:*" } ] }

Notice that the DynamoDB statement uses the full ARN of a specific table rather than a wildcard. This means the Lambda function can only touch that one table, not any other DynamoDB table in the account. This is least privilege in practice — the function gets access to exactly what it needs. The CloudWatch statement uses wildcards because log groups are typically created dynamically and you do not always know the name in advance.

You can also add conditions to a statement, which lets you restrict access based on things like the time of day, the IP address the request came from, or whether MFA was used to authenticate. Conditions are optional and most basic policies do not need them, but they become useful when you need finer control over sensitive operations.

One thing that trips people up: Deny always wins over Allow. If you have two statements and one allows an action while the other denies it, the result is a denial. This is intentional and important — it means you can create a broad Allow and then use Deny statements to carve out specific exceptions, and the exceptions will always be respected regardless of what other policies are attached.

Unit 4
Summary and Best Practices

IAM is one of those things that rewards getting right early. The habits you build in the first few weeks of using AWS tend to stick, and sloppy IAM habits have a way of compounding into real problems as a project grows. This final unit pulls together the key ideas and gives you a short list of practices worth following from day one.

Never use the root account for anything except the tasks that specifically require it — things like changing the account email address, managing billing settings, or closing the account. For everything else, create an IAM user with the permissions you actually need and use that instead. Better still, enable MFA on the root account immediately after creating your AWS account and then put the root credentials somewhere safe and largely forget about them.

Use groups to manage permissions rather than attaching policies to individual users. It takes a few extra minutes to set up but saves a significant amount of time and reduces the chance of inconsistency as the team grows. When someone's role changes, you update the group, not ten individual user records.

For applications and services, always use roles rather than embedding access keys in your code. A Lambda function should have an execution role; an EC2 instance that needs to call AWS services should have an instance profile. Access keys that live in code get committed to repositories, copied into chat messages, and generally end up in places they should never be. Roles eliminate this problem because the credentials are temporary and managed by AWS automatically.

When writing custom policies, start narrow and expand as needed. It is much easier to add a permission you realise you need than to understand the full blast radius of a permission you gave too broadly. The IAM Policy Simulator, available in the AWS Console, is a useful tool for testing what a policy actually allows before you attach it to anything.

Finally, turn on CloudTrail if it is not already enabled. CloudTrail logs every API call made in your account — who made it, from where, at what time, and whether it succeeded. It does not prevent anything on its own, but when something goes wrong, and eventually something always does, it is the difference between being able to investigate and being completely in the dark. Combined with thoughtful IAM configuration, it gives you both a strong first line of defence and the visibility to catch problems early.