Easily run CLI commands as an AWS role with AWSudo

AWSudo is a simple tool to transparently assume a role and run a command

Author's image
Tamás Sallai
9 mins

Run commands as an AWS role with AWSudo

AWSudo is an easy-to-use CLI utility to run a command as an AWS role. It works similar to the traditional sudo used widely in most Linux-based systems, which provides an easy way to run something as the root user. When an apt install does not work because of permissions, use sudo apt install and it's on its way to install the package. AWSudo does the same, but instead of switching to the local root user, it switches AWS entities by assuming a role.

Of course, this functionality is nothing novel. If you regularly use roles you might already have profiles in your .aws/config file which does pretty much the same. What is different with AWSudo is that you don't need to hardcode permanent role ARNs to use it. They can come from an external program too.

Such as Terraform.

The way I found AWSudo is when I wanted to limit my admin user's privileges to what is absolutely necessary. For this, a Terraform module creates a role with the minimal set of permissions and outputs its ARN.

data "aws_caller_identity" "current" {}

data "aws_iam_policy_document" "trust_current_account" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "AWS"
      identifiers = [data.aws_caller_identity.current.account_id]
    }
  }
}

resource "aws_iam_role" "role" {
  assume_role_policy = data.aws_iam_policy_document.trust_current_account.json
}

resource "aws_iam_role_policy" "add_permissions" {
  role = aws_iam_role.role.id

  policy = # policy.json
}

output "role" {
  value = aws_iam_role.role.arn
}

With this in place, I could use npx awsudo $(terraform output role) ... to run something with the role. No need to modify the global AWS CLI config.

Why use roles in AWS

Roles are temporary credentials that can be assumed by another entity, entity being another role, an IAM user, or a service, which has permissions on their own. As its name implies, they are especially suited to grant a small set of permissions just for a specific task. And unlike AWS users, they depend on another entity to provide the authorization via the trust policy.

For example, a security team can have access to an account via a role. The role specifies who can assume it via the trust policy and what it can do via permissions. This way there is no need to create users for individual people and, more importantly, they don't need to keep a new pair of username-password safe.

There are two use-cases for roles.

Gain permissions with roles

The first is to gain permissions you don't have. For example, your user might not have access to an S3 bucket containing sensitive information, but you can assume an auditor role that has. This is also the case with cross-account roles as you have no access to a different account but you can assume a role that has permissions to access resources inside.

Limit permissions with roles

The second use-case is to limit your permissions to what is absolutely required. Let's say you have an admin account but you know you'll only need access to a specific resource, such as to upload a document to a specific S3 bucket. In this case, you might want to limit the access range not to accidentally upload to the wrong bucket.

$ aws s3 cp secrets.txt s3://prod-bucket/secrets.txt
Ctrl-C
Ctrl-C

$ aws s3 rm s3://prod-bucket/secrets.txt

$ aws s3 cp secrets.txt s3://dev-bucket/secrets.txt

If you have no access to the wrong bucket, you can not upload to the wrong bucket.

It also applies to running a program that you don't completely trust. Would you give free rein to something you've just found on the Internet? If it's a security assessment tool, it might not need write permissions at all. If it's a bucket analyzer, you can allow only specific buckets for it to analyze.

If a tool has no access to a resource it can not do harm regarding that resource.

Usage

AWSudo is in the npm registry and offers a CLI, which ticks off all the requirements for running it with npx. This means you don't need to install anything and it has zero system dependencies besides a recent npm.

To run a command as a given role, use:

npx awsudo <roleARN> <command>

Just like with sudo, it is a prefix before the command and you can run just about anything.

For example, all AWS CLI commands can be prefixed. To upload a file, use:

npx awsudo <roleARN> aws s3 cp secrets.txt s3://dev-bucket/secrets.txt

But it's not limited to the AWS CLI. To run a javascript code with the role's credentials, use npx awsudo <roleARN> node index.js. Similarly, it is equally easy to run something more complex, like a dev workflow that watches the source directory and restarts a script:

npx awsudo <roleARN> npx nodemon -w src --exec "node index.js"

In this example, AWSudo assumes a role and runs Nodemon, which in turn runs node index.js and restarts it when anything is changed inside src.

What is a roleARN

The roleARN is the only required argument for AWSudo, and that identifies the role to assume. In the Terraform example above, the aws_iam_role.role.arn is the created role's ARN, which is outputted as the role output. To assume this role with AWSudo, use npx awsudo $(terraform output role) <command>.

This works by first running terraform output role which prints the role's ARN then it is used as the first argument of AWSudo.

To find out a role's ARN outside Terraform, the aws iam list-roles gives back all the roles in the account. Inside each object there is a property called Arn which is what you need here. To find out a role's ARN by its name, you can use jq:

$ aws iam list-roles | jq -r '.Roles[] | select(.RoleName == "<name>") | .Arn'
arn:aws:iam::123456789012:role/<name>

And this can be easily inserted to AWSudo similarly as the Terraform output.

But finding out the role's ARN heavily depends on your situation and where that role comes from. It's different when it's created by a resource manager, like CloudFormation or Terraform, or when it's already created for you.

Running multiple commands

While running one-off commands is easy with AWSudo, each run makes a call to AWS STS to assume the role and get the credentials. I don't know of about any limits regarding this, but if you know you'll need multiple commands it's even easier to run a subshell.

$ npx awsudo -d 3600 <roleARN> bash

With this, you'll have a shell with the role assumed. Inside this shell, you can use all the usual commands without any changes.

As an example, I needed to remove some objects from a bucket managed by Terraform because I accidentally uploaded them with the wrong configuration. Since this was a one-off act, I needed a playground-style way for exploring and fixing things.

$ npx awsudo -d 3600 $(terraform output role) bash
$ aws s3 ls s3://$(terraform output bucket)
...

$ aws s3 rm s3://$(terraform output bucket) --recursive
$ exit

Of course, any other shell can be used. If you use zsh, use that instead of bash.

Duration

The default duration is 15 minutes when using AWSudo, which is plenty for one-off commands but will be too short when running things in a subshell. To increase it, use -d <seconds> argument before the role ARN. One hour has 3600 seconds, so specifying -d 3600 gives you a longer time.

Roles can also limit their time. This is called the MaxSessionDuration and it can be a range between 1 and 12 hours with 1 hour being the default. When you try to use a longer time than the allowed maximum, the operation will fail.

Fortunately, it is apparent when you try to assume the role with a longer requested duration than the role's maximum:

Error \[ValidationError]: The requested DurationSeconds exceeds the MaxSessionDuration set for this role.

If you get this error, use a lower value.

How AWSudo works under the hood

The inner mechanics are quite simple and easy-to-understand. When an AWS role is assumed, it returns 3 important parameters: The AccessKeyId, the SecretAccessKey, and the SessionToken.

$ aws sts assume-role --role-arn ... --role-session-name test
{
	"Credentials": {
		"AccessKeyId": "....",
		"SecretAccessKey": "....",
		"SessionToken": "....",
		"Expiration": "...."
	},
	"AssumedRoleUser": {
		"AssumedRoleId": "....",
		"Arn": "...."
	}
}

Then if you set these parameters as environment variables, the AWS CLI will pick them up and use the role.

$ eval `aws sts assume-role --role-arn ... --role-session-name test \
	| jq -r '"export AWS_ACCESS_KEY_ID=" + .Credentials.AccessKeyId,
		"export AWS_SECRET_ACCESS_KEY="+.Credentials.SecretAccessKey,
		"export AWS_SESSION_TOKEN="+.Credentials.SessionToken
	'`

$ aws s3 ls # run as the role

The AWSudo library simply hides these details and provides an easier-to-remember construct.

Dangers

While AWSudo is open source, running it imposes risks, especially since it's a security-related tool. If the maintainers got hacked and a malicious new version gets published on npm then it can steal your access keys easily. And since npx uses the latest version by default, it's an automatic process on your end.

To mitigate this risk, you can pin down the version you are using to some trusted one: npx awsudo@1.3.2 .... This protects against a published malicious version but the command is harder to remember.

Conclusion

AWSudo is a tool that makes certain operations with roles a lot easier. Since it does not require any installation and has a command structure that is easy to memorize, it's a great help for anybody working on AWS.

It helps with using roles in the CLI without any permanent config to your AWS CLI. This is especially suited to roles created by Terraform or CloudFormation and one-off tasks that require more exploration than planning.

April 21, 2020