AWS IAM vs API vs CloudTrail


RSS feed

For the most part, the names of the API calls in AWS match the names of the IAM privileges and the log message that is recorded in CloudTrail. For the most part, you can control what actions a user can take with IAM. For the most part, the actions a user takes will show up in CloudTrail. This post will discuss exceptions to these rules.

Knowing about these oddities can be helpful in a number of situations. For example, one area is when trying to apply a least privilege strategy to give IAM users and roles only the minimal IAM privileges they need to perform their tasks. As such, many of these were discovered while working on CloudTracker, an open-source tool for helping with implementing AWS least privileges.

How these work together

As a simple example, let's look at the CLI command aws iam create-user Bob. In order for this to work successfully, the calling user needs IAM privileges like this:

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

This will generate a CloudTrail log message that looks like this:

{"Records": [{
    "eventSource": "",
    "eventName": "CreateUser",
    "requestParameters": {"userName": "Bob"},

When the original CLI command was run, it converted the kebab-cased string create-user to the camel-cased API name CreateUser and made a call to the IAM service that is hosted at

For the purpose of this discussion, what we care about is that the API name (CreateUser) and the sub-domain (iam) matches the IAM privilege Action name (iam:CreateUser) and that this matches the CloudTrail log event's eventSource and eventName.

Data sources

IAM privilege data

The best source of data for IAM privileges comes from the AWS Policy Generator at This was released in 2011, but is updated regularly. We can download the data that backs this app by grabbing it from

Remove the initial string app.PolicyEditorConfig= in order to leave just a json blob. This can then be parsed as follows:

jq -r '.serviceMap[]|.StringPrefix + ":" +.Actions[]' policies.js | sort | uniq > privileges.txt

This currently results in 4059 privilege names:


API name data

In order to get the API names, we can parse the data out of the botocore project with:

git clone --depth 1 -b master
find botocore/botocore/data -name *.json | xargs cat | jq -r 'select(.operations != null) as $parent | .operations | keys | .[] | $parent.metadata.endpointPrefix +":"+.' | sort | uniq > api_names.txt

This results in a similar list of 4362 items.

Naming differences

Service name differences

By running a diff of the two data source files we can start finding a lot of the differences. For example, for the service names, the IAM privilege service name ses correlates to the API end-point email. Similarly, the cloudwatch privileges use the end-point monitoring. These match with the CLI command names (ses and cloudwatch). In the case of the IAM service name tag, the API end-point is tagging, and the CLI command is resourcegroupstaggingapi.

Some of the IAM privilege service names are lumped into API end-points that serve more general purposes. For example, there is an IAM privilege service named trustedadvisor, but when that API is called, you use the support end-point. Similarly, for AWS's new graph database named Neptune, they created a new IAM privilege named neptune-db, but the API call uses the rds end-point.

In contrast to that, some of the IAM privilege service names are more generalized than the API end-point names. For example, there is a single IAM privilege named lex, but depending on the action, the end-point models.lex or runtime.lex will be used.

So sometimes the IAM privilege service name is more fine-grained, and sometimes the API end-point is.

Action name differences

Within the services the actions can also be named differently. For example, the CLI command aws s3 ls will list the S3 buckets in an account. To do this, the IAM privilege name is ListAllMyBuckets whereas the API action name is ListBuckets. Similarly, when you pass that command a bucket name, such as aws s3 ls s3://mybucket it will list the objects in the bucket, by using the IAM privilege ListBucket and the API ListObjects. Here's a table to show this:

CLI commandIAM privilegeAPI action
aws s3 lsListAllMyBucketsListBuckets
aws s3 ls s3://mybucketListBucketListObjects

Actions that can't be stopped

When you first create an IAM user, without assigning them to any groups or applying any policies to them, they have no privileges by default... according to Amazon's documentation. That isn't entirely true. You can even apply an IAM policy to the user with {"Action": "*", "Effect": "Deny", "Resource": "*"} to try to stop all actions, but there are still some that are always allowed.

An IAM user with a password can still log into the AWS console via their web browser. This will show up in the CloudTrail logs as having happened through the signin end-point, for which there is no IAM service or API equivalent. These will show up in CloudTrail as ConsoleLogin and CheckMfa events. Once logged in, a user won't be able to see or do anything, but this is worth knowing about.

Similarly, if an IAM actor has an access key, they can always call the sts APIs GetCallerIdentity and GetSessionToken. Note that getting a session token only works if this is not already a session key. Oddly, there is an IAM privilege named sts:GetCallerIdentity, but denying it does not do anything.

If the IAM user has the ability to assume roles (IAM privilege iam:AssumeRole), then from the web console they may perform actions that show up in the CloudTrail logs as signin events SwitchRole, RenewRole, and ExitRole. If they use an access key to change roles, this shows up as sts:AssumeRole.

They key point to take away from this is if you find an AWS access key, you can use it to call aws sts get-caller-identity and this will always work if it is active. This will tell you the account ID and the actor's ARN (which tells you the username or role name).

Privileges without actions

Although AWS likes to state they are API first, there are some IAM privileges that have no API equivalent. For example, in order to get a copy of AWS's SOC 2, you need IAM privileges for the artifact service, but there is no API call to download these documents.

There are also some APIs that have been deprecated, so there are still IAM privileges for it, but the latest botocore code no longer contains a way to access it. For example, the Athena call CancelQueryExecution has been deprecated as described here. (Researcher note: Some of these might be interesting).

API versioning

As opposed to the IAM privileges versus API names, the API names much more closely match what is recorded in CloudTrail. However, some of the APIs have changed versions over the years, so CloudTrail records which version of an API was called.

For example, the CloudFront service has changed 15 times since it was released. You can see the different versions in botocore by looking at the directory botocore/data/cloudfront. When the latest version of the API ListDistributions is called, it will show up in CloudTrail as the event ListDistributions as you would expect, but if an older version is called, it will show up with the version date appended, for example ListDistributions2017_03_25.

When you call AWS APIs you make an HTTP request, and the URL can describe the version of the API to be called. For example, to call ListDistributions what happens behind the scenes of the boto library is you make an HTTP GET request to where 2017-10-30 is the version of the API you are making the call to. This is the latest version of the API, so this would be recorded in CloudTrail as ListDistributions, but if you used an older version of the boto library that resulted in a call to this would be recorded as ListDistributions2017_03_25.

The format of the event name can vary slightly, for example, for Lambda calls, there are no underscores in the version, resulting in event names such as GetFunctionConfiguration20150331, and for that call there were actually two versions released on the same day, so you may have events named GetFunctionConfiguration20150331v2.

Researcher note: You cannot restrict IAM actors from using older versions of APIs, so there are potentially interesting issues that may only arise when calling older versions of APIs. For example, Google Cloud has an issue where you can call older versions of their metadata service without host headers (see the exploitation in this HackerOne bug bounty report by André Baptista).

Actions without logs

Not all API calls shows up in CloudTrail. Data level events are not recorded by default, which includes S3 object level calls such as GetObject, and also Lambda function calls as a result of Invoke. You can enable those data level events, but there are still many more that are not recorded, such as the CloudWatch PutMetricData call, and there is no way to cause these to be recorded.


I hope this post has brought to light some of the difficulties in correlating between AWS IAM privileges, API calls, and CloudTrail log events. If you'd like Amazon to document these things better, please +1 the Github issues I've filed against their CloudTrail user guide here and here, or better yet, pester your TAMs. :)

This lack of documentation makes it difficult to implement a least privilege strategy effectively or perform incident response, and makes it especially difficult to write tooling to assist with those things.

If you're looking for help with your AWS security, reach out to me!