7 min read

What are AWS Roles?

If you haven’t read it yet, Intro to AWS is great for building a basic understanding of how users, groups, and policies work.

This post explains what AWS roles are and when you might use them. For a technical walkthrough of how to assume a role, see Assuming AWS roles.

What is a role?

Roles are similar to IAM users: they are AWS entities with certain permissions granted to them. However, they are not associated with any individual. A user can assume a role in order to temporarily gain access to the permissions that the role has.

When a user attempts to assume a role, they are asking to receive a set of credentials that they can use in place of their own. Authenticating to AWS with these credentials will allow them to perform actions on resources in accordance with the role’s permissions. These credentials are temporary and will need to be requested again when they expire.

When should I use a role?

If you want to grant permissions on a resource that you own to an external entity, like a contractor or a client with their own AWS account, the cleanest way is to create a role.

You are able to manage the permissions that they’ll have without the need to register them as an IAM user within your AWS account. You essentially create a pseudo-user for them to access, which will temporarily grant them access to the resources you specify.

More from the AWS docs.

How are roles defined?

A role resembles a policy since it uses JSON-formatted specifications. The declaration contains a trust policy and a permissions policy. The trust policy defines which AWS accounts can assume the role. The permissions policy defines what privileges are granted through the role.

For example, the trust policy may include a contractor’s root account ID, validating them as a valid accessor of the role. Agnostic of the trust policy, the permissions policy specifies that anyone assuming the role can view the contents of a specific S3 bucket. Additionally, because the contractor’s root account is trusted, the contractor can delegate that permission to its IAM users.

Here are some examples from AWS’s guide on what those two policies might look like:

A sample trust policy:

{
  "Version": "2012-10-17",
  "Statement": {
    "Effect": "Allow",
    "Principal": { "AWS": "arn:aws:iam::123456789012:root" },
    "Action": "sts:AssumeRole"
  }
}

A sample permissions policy:

{
  "Version": "2012-10-17",
  "Statement": {
    "Effect": "Allow",
    "Action": "s3:ListBucket",
    "Resource": "arn:aws:s3:::example_bucket"
  }
}

Why use a role?

Can’t you simply assign the permissions directly to a user or group?

Let’s go further with the example of an external entity needing permissions. A contractor needs to read the contents of an S3 bucket in your account. But, you can’t assign permissions to another AWS account, only to your own IAM users and groups.

So, you create a role with the following specifications:

The contractor’s account can access the role and can delegate permission to use it to its developers. For any of the IAM users that it allows to assume the role, those users can request credentials from AWS that will allow them to read from the S3 bucket.

Note that the contractor’s IAM users can only read from the bucket by using the temporary credentials from the role. The IAM users themselves still technically don’t have permission to view the S3 bucket, but they do have permission to receive temporary role credentials that allow it on their behalf.

Assuming the role

Make sure you’re authenticated in the CLI as an IAM user that has permission to assume the role. The command to assume a role looks like this:

aws sts assume-role --role-arn "arn:aws:iam::<ACCOUNT_ID>:role/..." --role-session-name "FirstSession" --external-id "<external-id>" 

Let’s break that down.

External IDs

AWS provides a solid explanation of the purpose of external IDs, summarized here.

Say a contractor has two clients, Client A and Client B.

Each client creates a role with permissions to perform actions on their resources, and they send the contractor the ARNs of the roles. In order to ensure that only the contractor can assume these roles, they each ask for the contractor’s account ID and add it to their role’s trust policy.

{
  "Version": "2012-10-17",
  "Statement": {
    "Effect": "Allow",
    "Action": "sts:AssumeRole",
    "Principal": {"AWS": "arn:aws:iam:<Contractor's AWS Account ID>:root"},
  }
}

Unfortunately, this system is exploitable. Imagine that Client B wants to take action on Client A’s resource.

The first step would be figuring out the ARN of Client A’s role, which is possible since role ARNs can be formulaic and guessable.

Even if Client B guesses or learns the ARN for Client A’s role, they still can’t use their account to assume the role because their account ID isn’t approved in the trust policy that Client A defined, so AWS will reject the request. But, they can trick the contractor into performing an action on Client A’s resources. They simply need to reach out to the contractor and request an action to be performed, but provide the ARN for Client A’s role rather than their own. The contractor may not realize they have been provided the wrong ARN, and proceed with the requested action. The contractor’s account is approved to assume the role, and AWS doesn’t know that the contractor has been duped.

One way to mitigate this is if the contractor indicates which client made the request when it tries to assume a role. Client A can configure the role’s trust policy to demand that the contractor states that it is consciously doing this work on behalf of Client A.

If Client B provided the contractor with Client A’s role ARN, the contractor will still indicate that this request came from Client B, so there will be a mismatch and authentication will fail.

To accomplish this, the contractor generates an ID for each client, an ID that it will always include in any requests to assume a role. The contractor sends each client the ID that has been assigned to them, and each client updates their role’s trust policy with a condition for an external ID. Here’s what Client A’s might look like:

{
  "Version": "2012-10-17",
  "Statement": {
    "Effect": "Allow",
    "Action": "sts:AssumeRole",
    "Principal": {"AWS": "<Contractor's AWS Account ID>"},
    "Condition": {"StringEquals": {"sts:ExternalId": "client-A-12345"}}
  }
}

Client A’s new trust policy demands two things:

If the account ID does not match, then authentication will fail because someone other than the contractor is trying to assume the role.

If the external ID does not match, then authentication will fail because the contractor is claiming that a different client requested this work.

Receiving credentials

Having run the assume-role command, you’ll receive a response from AWS that contains credentials, including an access key ID, a secret access key, a session token, and an expiration timestamp.

The easiest way to use these is to add a new profile to your ~/.aws/credentials with these values. You can append a new profile after the default one, making sure to include the session key. Note that once these credentials expire, you’ll need to repeat this process by requesting new credentials and updating the profile.

[default]
aws_access_key_id = <DEFAULT_ACCESS_KEY_ID>
aws_secret_access_key = <DEFAULT_SECRET_ACCESS_KEY>

# Add a new profile
[new-profile]
aws_access_key_id = <ACCESS_KEY_ID>
aws_secret_access_key = <SECRET_ACCESS_KEY>
aws_session_token = <SESSION_TOKEN>

You can then run any valid AWS command that role allows you to, as long as you specify the new profile.

aws s3 ls s3://path/to/bucket/ --profile new-profile

References:

https://docs.aws.amazon.com/AmazonS3/latest/dev/example-walkthroughs-managing-access-example2.html#access-policies-walkthrough-cross-account-permissions-acctB-tasks

https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html

https://docs.aws.amazon.com/AmazonS3/latest/dev/policy-eval-walkthrough-download-awscli.html