AWS IAM Permissions Explained

IAM should be the first AWS service you've consciously used: When you create an AWS account, the first thing you do is create an IAM user, and stop using the root user. That's nothing new to you, I hope! But what do you do after that?

Usually (and you have to be honest with yourself here), you'll create IAM users for everyone and their dogs, attach the AdministratorAccess policy, create access keys, and forget about it until 3 months after someone left the company and you finally got around to deleting their IAM user (and a service breaks).

Surely you can do better, but that would require understanding what I consider one of the most complex services in the whole of Amazon Web Services: AWS Identity and Access Management (yes, IAM is hard!). So, let's dive into a totally-not-fun-but-necessary journey through permissions and access control in AWS.

Understanding AWS IAM

AWS Identity and Access Management (IAM) is how you control who can do what in your AWS account. It allows you to manage permissions for users, groups, and roles, granting or denying access to specific actions and resources. Obviously, the idea is to allow everything you and your people need, and deny everything else, and everything from everyone else. Let's start with the basics and build from there.

Basics of AWS IAM

IAM, short for Identity and Access Management, is a fundamental service within AWS that enables access management for all other services. You create and manage users and roles, and use JSON policies to grant them permissions to perform specific actions on specific resources. These policies consist of statements that specify actions, resources, and conditions, allowing you to control who can do what in your AWS account.

It's really easy to grant access: just attach the AdministratorAccess policy to a user and they can do nearly everything. The hard thing is to only allow your users to perform the actions they need in your AWS account, and not more. You might trust anyone in your company to not delete the production database, but granting them permissions they don't need only serves to increase the risk of a malicious actor being able to perform those actions. That's why we're talking about AWS IAM.

Deny, Allow, Deny

IAM Policies can either Allow or Deny actions. When a principal attempts an action, IAM first checks for explicit Deny statements in attached policies, and if it finds any, the action is denied. After that, it checks for explicit Allow statements, in which case the action is allowed. If no explicit Allow statements are found across all policies involved, the action is denied, in what we usually call a default deny or implicit deny. Every action is implicitly denied, unless there's an explicit Allow statement. And even if there's an explicit Allow statement, an explicit Deny statement means the action is denied. The easy way to remember it is explicit Deny > explicit Allow > implicit Deny. Also known as Deny, Allow, Deny.

IAM Principals

IAM Principals are the actors in AWS who perform access operations. A principal can be an AWS account (which includes its root user), an IAM role, a Role session, an IAM user, a Federated user session or an AWS service.

Users and Roles are the easy ones to understand. A role session is the session used by a role in a particular moment. Same for Federated user sessions. And AWS services only perform actions directly when it's the service itself making the action, not a resource you own. For example, the Application Auto Scaling service makes the scaling calls directly, not using a Role you provided. In contrast, code in an EC2 instance doesn't make calls from the EC2 service, but using the IAM Role that you associate with the instance. By the way, did you notice that Groups aren't principals? Groups are just a way to, you know, group users.

IAM Users

IAM Users represent individual users (persons) within an AWS account. Each person must have their own user. This sounds obvious, but I can't tell you how many companies I've seen where they share users.

An IAM User can have a password, which they use to log in to the console. Additionally, or alternatively, an IAM User can have access keys, which they use for the AWS CLI or for SDKs. That said, Access Keys aren't recommended. If you're using the AWS SDK from inside an AWS resource, such as an EC2 instance of a Lambda function, don't use credentials. Instead, assign an IAM Role to the instance or function. For your own use, it's preferable to use CloudShell or Cloud9 when possible, or if you have IAM Identity Center users (which I'll explain further below) you can use temporary access keys.

IAM Users can belong to zero to many Groups. If IAM Policies are attached to groups to which an IAM User belongs to, those policies act as if they were attached directly to the user.

IAM Roles

IAM Roles don't have a password or access keys. They are temporarily assumed by an IAM User, an AWS Resource, an AWS Service or an external entity, and this lets them perform actions with that role's permissions. Note that assuming a role can lead to a decrease in permissions, if the role has less permissions than the user that's assuming it.

Roles have policies attached to them, which define the permissions that whomever assumes the role will have. Additionally, roles have a Trust Policy, which determines who can assume that role.

When a role (or rather, an entity that has assumed a role) performs a request to AWS, first a request is made to the Security Token Service (STS) service, which returns temporary credentials that are used to sign the request to AWS.

I already mentioned this above, but it's worth repeating. Don't use access keys in your code. Instead, assign a role to your EC2 instances, Fargate containers, Lambda functions, etc, and let AWS use those temporary credentials instead of your long-lived ones. You don't even have to do anything, just assign a role and instantiate the AWS SDK with no credentials, and the virtualization layer by AWS will capture the request as it leaves your instance/function/etc and sign it with the credentials of the role that you assigned.

I think I've only ever worked with 2 clients that didn't make this mistake, so I'll repeat it once again. Don't ever, ever, EVER use long-lived credentials in AWS resources. And please don't commit credentials to a git repo.

Trust Relationships in IAM

Trust relationships define who can assume a role or access specific resources. Trust relationships can be established between AWS accounts, enabling cross-account access. Additionally, they can also be formed with identity providers like Active Directory. Trust relationships are defined in trust policies, which govern access permissions.

Deep Dive into IAM Policies and Permissions

I've been sort of skirting around the edges of IAM policies and permissions. Now it's time to dive into them. IAM policies are the key to defining permissions, determining what actions and resources are allowed or denied. They're defined in JSON, and they can range from not-totally-simple to rather-complex.

Types of IAM Policies

There's three types of IAM Policies:

  • First, we have identity-based policies, which control access permissions for IAM users, groups, or roles. These are the ones that we usually talk about when we say "IAM Policies".

  • Then, there are resource-based policies that govern access to specific resources like S3 buckets or EC2 instances. These are the ones you set in an S3 bucket to make it publicly accessible, for example.

  • Lastly, we have permissions boundaries, which limit the maximum permissions an identity-based policy can grant. I'll explain permission boundaries a bit further down.

Associating IAM Policies with Users, Roles and Groups

You can directly associate policies with individual users and roles, and this will grant them permissions. Additionally, you can associate policies with groups and make users part of those groups. This makes it easier to manage permissions, since you can, for example, create a group called Developers, assign several IAM policies relevant for devs to that group, and then make all IAM users that belong to developers a part of that group. You can also create another group, such as DevOps, and make other users a part of that group. If a person changes roles, you can just move them from one group to another. And if they have both roles, you can make them a part of both groups!

IAM Policy Elements

Let's analyze an IAM Policy, to understand how they're written. The language is JSON, and it doesn't support comments, but I've added them with # at the end of every line anyways.

{
    "Version": "2012-10-17", # The policy language version; always include this with the latest version
    "Statement": [ # A policy has one or more statements
        {
            "Sid": "ListAllBuckets", # An optional identifier for the statement
            "Effect": "Allow", # This could be "Allow" or "Deny" to explicitly allow or deny the permissions
            "Action": [ 
                "s3:ListAllMyBuckets", # This action allows listing all S3 buckets
                "s3:GetBucketLocation" # This action allows getting the location of the S3 bucket
            ],
            "Resource": "*" # A wildcard (*) indicating that this action applies to all resources
        },
        {
            "Sid": "ReadBucket", 
            "Effect": "Allow",
            "Action": [ 
                "s3:GetObject", # This action allows reading objects in S3
                "s3:ListBucket" # This action allows listing the contents of a bucket
            ],
            "Resource": [
                "arn:aws:s3:::example-bucket", # The ARN of the bucket to which the actions apply
                "arn:aws:s3:::example-bucket/*" # The ARN pattern for all objects within the bucket
            ]
        }
    ]
}

IAM Policy Example: Providing Read-only Access to an S3 Bucket

Let's say you want to give someone access to read but not modify an S3 bucket. You can create an IAM policy that allows only "s3:GetObject" permissions. Like this:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "ReadBucket", 
            "Effect": "Allow",
            "Action": [ 
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::example-bucket",
                "arn:aws:s3:::example-bucket/*"
            ]
        }
    ]
}

If you try this policy, it will... fail! Unfortunately, permissions aren't that simple, and what looks like an atomic operation to us may actually involve several actions behind the scenes. This is the correct policy to be able to view and access objects in an S3 bucket from the S3 console:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "ReadBucket", 
            "Effect": "Allow",
            "Action": [ 
                "s3:GetObject",
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:::example-bucket",
                "arn:aws:s3:::example-bucket/*"
            ]
        }
    ]
}

But what happens if you want to grant everyone read access to the bucket? In that case, instead of an identity-based IAM policy, you need a resource policy on the bucket itself. This is the policy:

{
    "Version":"2012-10-17",
    "Statement":[
        {
            "Sid":"MakeBucketPublic",
            "Effect":"Allow",
            "Principal": "*",
            "Action":["s3:GetObject"],
            "Resource":["arn:aws:s3:::example-bucket/*"]
        }
    ]
}

You can set it on an S3 bucket (change the ARN inside Resource), and anyone will be able to read objects from it, without even needing to have access to an IAM Principal, and without signing the requests to identify themselves. This is what you'd use when hosting public files or a static website on S3.

Permission Boundaries

Permission boundaries are sort of like IAM policies, but instead of actually granting permissions, they define the maximum permissions that a user or role can have. The effective Deny statements will be the union of the Deny statements in permission boundaries and identity-based policies. The effective Allow statements will be the intersection of the Allow statements in permission boundaries and identity-based policies. That means, an explicit Deny in either a permission boundary or identity-based policy results in the action being denied. If there's no explicit Deny, then both the permission boundary and any of the identity-based policies need to have an Allow statement. An Allow statement in just the permission boundary or just in an identity-based policy is not enough, you need it in both.

Note that if a user or role has no permission boundary associated with it, you can safely ignore this section. However, if you associate an empty permission boundary, then that effectively denies all actions, since an empty permission boundary has no Allow statements. A permission boundary that has no effect would be one that allows all actions. In that case, permissions will be entirely controlled by identity-based policies.

Service Control Policies

Service Control Policies (SCPs) define maximum permissions for member accounts within an AWS Organization. They can restrict access to specific AWS services, actions, resources, or even regions. They're a part of AWS Organizations, which I'll explain in a second.

SCPs are like permission boundaries, but at the account level. For a principal to have an action allowed, it must be allowed both in an SCP and in an identity-based policy, it's not enough to have an allow in just one of them and not the other. Also, explicit denies in either will result in the action being denied.

Service Control Policies actually do grant permissions, but to the root user of the AWS account. In AWS, when a principal receives permissions, they're actually inheriting those permissions from the root user. That's why, when granting cross-account permissions, the account that shares the resource grants permissions to the other account (to the root user), and that other account needs to create a policy for its own users or roles to inherit those permissions.

When you don't have any Service Control Policies (for example, if your AWS account doesn't belong to an Organization), the root user can do anything and you just ignore this section. But when there are SCPs involved, anything not explicitly allowed by an SCP is implicitly denied, both to the root user and to any principals which inherit permissions from that account. For that reason, an SCP that has no effect is one which allows all actions.

AWS Organizations

AWS Organizations lets you group AWS accounts into a hierarchical structure, where you can use some cool features like Service Control Policies, consolidated billing (meaning you enter your credit card in just one account), and really easy cross-account access via IAM Identity Center. I could write 3k words about Organizations, but I already have, so go read AWS Organizations: How to Manage Multiple AWS Accounts.

Creating Service Control Policies

You create Service Control Policies from the root account of the Organization. They're associated with accounts, or with Organizational Units, and inherited by any AWS accounts that are under that OU. Seriously, go read AWS Organizations: How to Manage Multiple AWS Accounts, I even gave you some great SCPs to enhance your security.

SCP Example: Restricting Access to Specific AWS Regions

Have you ever accidentally deployed a resource in the wrong region, then forgot about it, and ended up paying a couple hundred dollars more than you should have? I've got the solution for you: don't deploy resources in the wrong region. No, I'm not kidding. This is a good example of what you can do with SCPs: deny resource creation unless it's in the 1, 2 or however many regions that you actually use (it's usually 1 or 2). Here's the policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Deny",
      "NotAction": "*",
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "aws:RequestedRegion": "us-east-1"
        }
      }
    },
    {
      "Effect": "Allow",
      "Action": "*",
      "Resource": "*"
    }
  ]
}

You'll notice that I'm Allowing every action (otherwise you couldn't do anything), and I'm using an explicit Deny when the requested region is not us-east-1.

This SCP will actually block all global services, such as Route 53 or CloudFront. To get it properly working, you need to add a "NotAction" statement with the actions for global services, such as route53:*. The entire policy is a bit long, but send me a DM on LinkedIn if you need it and I'll send it to you.

Resource-Based Policies

Resource-based policies are attached to resources, and control who can access them. If a resource-based policy has an Allow statement, you don't need another Allow statement from an identity-based policy. Heck, you don't even need an identity! Resource-based policies can be used to grant access to a resource to people who don't have an identity, such as the entire internet accessing a static website hosted on an S3 bucket.

Permissions Evaluation in AWS

As you noticed, there's a lot to evaluate, not just a single IAM policy with a single statement. Every time a request is made to the AWS API (which is what happens when you run a CLI command, send a request through the SDK, click somewhere on the Console, etc), the IAM service finds every policy related to your identity and to the resource, and analyzes all of them to determine whether the request is Allowed or Denied. Here's the entire process.

How Are Permissions Evaluated in AWS?

Here's a decision tree to show you how permissions are evaluated.

Decision tree for evaluating permissions in AWS

Example: SCPs, IAM Policies and Permission Boundaries

Let's take an example. Suppose you have the following SCP in place:

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

And the following IAM Policy associated with your IAM User:

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

And the following Permission Boundary:

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

When you open the S3 console, you see a list of the S3 buckets in your AWS account. The action is s3:ListAllMyBuckets. If you do it with the above policies in place, do you think it's allowed or denied?

Take a minute to think about it.

Think.

Still thinking.

Think think think.

Got it?

It's Denied, because the Permission Boundary doesn't allow the s3:ListAllMyBuckets action. Remember that permission boundaries don't grant permissions, but if something isn't allowed in the permission boundary, IAM policies can't grant that permission.

AWS IAM Best Practices

Alright, you learned a ton about how things work under the hood. Now what do you do with that knowledge? That should be obvious. You take an exam, get a hexagonal badge, and get paid more for doing the exact same work, just with more hexagons. But if you want to actually do a better job, here are some best practices you can implement.

Associate Policies to Groups

Define groups by logical sets of responsibilities, like "Service1Access" or "DevAccess". Associate policies to those groups, and make users part of them. On one hand, this helps you keep permissions uniform: If a dev needs access to X, all devs need access to X. On the other hand, this helps you avoid unnecessary permissions: If someone is a dev, why should they have more access than the other devs? There may be a legitimate reason, such as that person being a tech lead. In that case, create another group called "TLAccess" and add the tech lead to that group.

See where I'm going? It's about keeping permissions consistent with the roles that people have in your company. This helps avoid someone having more permissions than they actually need, creating a security issue.

Principle of Least Privilege

Kicking off from the last sentence of the previous section, every person and system should only have the permissions that they need, and not more. Think of it like this: If your devs only really use EC2, then letting them access S3 where you keep some important files doesn't help them at all. Suppose you 100% trust your devs, you'd argue it can't hurt, right? Well, if an attacker compromises the credentials of any of your devs, the attacker can delete your important files. You're increasing the risk, for no benefit at all.

You should take this as far as you can afford to. "Access to EC2" would probably translate to ec2:* permissions. Do your devs really need the ability to delete instances? Sure, maybe they do. Every single instance, including the production one? Seems we found an area of improvement there. Add conditions, check the specific actions you need to grant permissions for, the specific resources if possible, and in general do your best not to use in any IAM policy. You'll end up using it in several places, and maybe it's alright. But do your best to avoid it and only use it when you actually mean to grant permissions on everything, instead of making permissions the default.

Always use Multi-Factor Authentication

Strong passwords are difficult to compromise, so use strong passwords. But you know what's even more difficult to compromise? A strong password + another thing. That's what Multi-Factor Authentication means: To log in, someone needs to know your password and have access to your other authentication factor. The most common second factor is a physical device that generates 6-digit codes that are valid for 30 seconds, known as Temporary One-Time Passwords (TOTP). You can buy one that connects via USB, or install an app on your phone following this old article I wrote: 7 Must-Do Security Best Practices for your AWS Account.

Permissions in Multi-Account Setups

When we say multi-account, we always mean AWS Organizations. The general best practice is to always have an Organization (it's free), and create as many accounts as needed (they're free). Have each account handle a single app or environment, or support responsibility. For example, for a startup with just one product you'd have a Dev account, a Prod account, the root account of the Organization, and probably a Security account and a Log account.

IAM Identity Center

IAM Identity Center is where you manage users when using AWS Organizations. It lets you create users and groups just like with IAM, and assign IAM Policies to them. Moreover, you can use an external identity provider, such as Microsoft Active Directory, Okta, etc. When you want to log in to an AWS account, you log in to IAM Identity Center, and you're presented with all the AWS accounts that you have access to, and the roles in each account. With just 2 clicks you can assume a role in an account and access the AWS Console, or copy short-lived credentials to use in the CLI.

Screenshot of IAM Identity Center

How can you troubleshoot AWS IAM permission issues?

To troubleshoot AWS IAM permission issues, you can use the AWS Identity and Access Management (IAM) policy simulator. This tool allows you to test permissions for different IAM policies and resources, helping you identify any missing or incorrect permissions that may be causing the issue.

Conclusion

Now that you've read about IAM, do you agree that it's one of the most complex services in AWS? Security is really hard to do well, and identity and access management is only one part of security. I hope this article helps with that.

Overall, the most important takeaway from this should be Least Privilege. Every IAM policy, SCP, boundary, role, etc is a tool to make it possible and easier to grant someone exactly the permissions they need, and not more. That's our goal here. A very difficult one, but one you should always strive for.

Did you like this issue?

Login or Subscribe to participate in polls.

Join the conversation

or to participate.