Yohan Beschi
Developer, Cloud Architect and DevOps Advocate
Understanding AWS CloudTrail Logs
AWS CloudTrail is an unvaluable service for governance, compliance and auditing. AWS CloudTrail Insights, Amazon GuardDuty and AWS Security Hub backed by AWS Config already help us a great deal in these areas. However, it happens from time to time that we need to dig into AWS CloudTrail logs and it far from an easy task.
With CloudTrail we can:
- monitor API call errors
- track user activity
- enforce the Principle Of Least Privilege
- …
From AWS Console we can explore events history over the past 90 days. To go back further we need to create a Trail and store logs into an AWS S3 Bucket. These logs can then be loaded into Amazon Athena (for more information see Querying AWS CloudTrail Logs and Analyze Security, Compliance, and Operational Activity Using AWS CloudTrail and Amazon Athena).
CloudTrail produces a lot of logs. Therefore, we first need to understand the logs format before exploiting them effectively.
Note from the author:
To write this article billion of AWS CloudTrail logs coming from 15 different AWS Accounts have been analyzed. Unfortunately, not all AWS services being used, we won’t be able to cover all the use cases.
AWS CloudTrail logs in S3
AWS CloudTrail logs can be stored in AWS CloudWatch Logs and AWS S3. To save costs and for easy processing AWS S3 is recommended as CloudTrail produce a lot of logs and a regular project with multiple AWS Accounts can have hundreds of GB of logs within a year.
AWS CloudTrail logs are stored in S3 as JSON text file compressed in gzip with the following name:
1
<bucket_name>/<prefix_name>/AWSLogs/<aws_account_id>/CloudTrail/<region>/YYYY/MM/DD/file_name.json.gz
Furthermore, every hour CloudTrail produce a digest file, containing the names of the log files that were delivered to the S3 bucket with the following name:
1
<bucket_name>/<prefix_name>/AWSLogs/<aws_account_id>/CloudTrail-Digest/<region>/YYYY/MM/DD/file_name.json.gz
For me information see Finding Your CloudTrail Log Files & CloudTrail Digest File Structure
A log file contains one or more records. A record being an event (an action made in an AWS Account - e.g. AssumeRole
, StartInstances
, CreateLogGroup
).
1
2
3
4
5
6
7
{
"Records": [
{
// Event
}
]
}
The structure of a record is partially documented (see CloudTrail Record Contents & CloudTrail userIdentity Element). Unfortunately, with these pages only we won’t be able to do much.
AWS CloudTrail logs format
Let’s go through the most important properties and see how they can help us in our different tasks.
eventSource & eventName
The eventSource
is the AWS Service against which the API call has been made (e.g. route53.amazonaws.com
, ec2.amazonaws.com
).
The eventName
is an Action done through the AWS APIs (e.g. AddTags
, DeleteUser
, CreateAutoScalingGroup
). As we will see later, this is not completely true, some Events don’t have any API call equivalent.
Of course the same Event Name can be used by multiple Event Sources. Here few examples:
"ListApplications" : [ "codedeploy.amazonaws.com", "application-insights.amazonaws.com", "kinesisanalytics.amazonaws.com" ]
"CreateWebACL" : [ "wafv2.amazonaws.com", "waf.amazonaws.com" ]
"DeleteRepository" : [ "ecr.amazonaws.com", "codecommit.amazonaws.com" ]
"ListTags" : [ "cloudtrail.amazonaws.com", "es.amazonaws.com", "backup.amazonaws.com" ]
With the eventSource
and the eventName
we can easily recreate the AWS IAM permission required to do this action. For example, if "eventSource" : "ssm.amazonaws.com"
and "eventName" : "PutParameter"
the IAM permission is ssm:PutParameter
. Some eventName
s are suffixed with a date (e.g. PublishLayerVersion20181031
) or a date and version (e.g. TagResource20170331v2
).
Being able to associate a CloudTrail event with an IAM permission can help us find a missing permission or to gather all the permissions actually used.
Programmatically, a one liner can do the job.
Let’s see an example in Python:
1
2
3
4
5
6
7
8
9
10
import re
eventSource = 'lambda.amazonaws.com'
eventName = 'PublishLayerVersion20181031'
iam_permission = eventSource.split('.')[0] \
+ ':' \
+ re.sub(r'^([a-zA-Z0-9]+)([0-9]{8})(v[0-9])?$', r'\1', eventName)
print(iam_permission)
and another one in Java:
1
2
3
4
5
6
7
8
var eventSource = 'lambda.amazonaws.com';
var eventName = 'PublishLayerVersion20181031';
var iamPermission = eventSource.split(".")[0]
+ ":"
+ eventName.replace("^([a-zA-Z0-9]+)([0-9]{8})(v[0-9])?$", "$1");
System.out.println(iamPermission);
It is important to note that this convention has some exceptions. For example, the eventSource
for Amazon CloudWatch is monitoring.amazonaws.com
while the IAM permission is cloudwatch:*
.
Furthermore, some Event Names do not match the AWS Action name and therefore not the IAM permissions either. As described in the S3 documentation, some translations are required:
SOAP API name | API event name used in CloudTrail log |
---|---|
ListAllMyBuckets | ListBuckets |
CreateBucket | CreateBucket |
DeleteBucket | DeleteBucket |
GetBucketAccessControlPolicy | GetBucketAcl |
SetBucketAccessControlPolicy | PutBucketAcl |
GetBucketLoggingStatus | GetBucketLogging |
SetBucketLoggingStatus | PutBucketLogging |
Finally, the Event Source signin.amazonaws.com
does not have any matching permission in IAM. Therefore, the following permissions does not exist:
signin:ConsoleLogin
signin:CheckMfa
signin:PasswordRecoveryCompleted
signin:PasswordRecoveryRequested
signin:RenewRole
signin:ExitRole
signin:SwitchRole
But some of these Events (e.g. SwitchRole
) are associated with Events that have IAM permissions (e.g. sts:AssumeRole
)
eventType
There are three types of Event:
AwsConsoleSignIn
- Used with the Event Sourcesignin.amazonaws.com
and the Events describes previouslyAwsServiceEvent
- Event generated by a service (RotationSucceeded
,DeleteKey
,ProcessBackupPlanSelection
,BackupJobCompleted
,BackupDeleted
,NewClientConnection
,SharedSnapshotVolumeCreated
, etc.)AwsApiCall
- An API was called.
Author’s note: in my logs I’ve found a fourth Event Type: AwsConsoleAction
associated with the SetIAMAccessPreference
event.
sourceIPAddress
The sourceIPAddress
can have the following values:
- An IP from which the call has been made
- A DNS (e.g.
cloudformation.amazonaws.com
,elasticbeanstalk.amazonaws.com
), the service doing the action on our behalf AWS Internal
userAgent
The userAgent
is like the HTTP User-Agent
header and can have multiple values:
- A Browser Agent
- A DNS (e.g.
cloudformation.amazonaws.com
,elasticbeanstalk.amazonaws.com
), the service doing the action on our behalf - The information of the SDK used (e.g. a
ws-sdk-go/1.28.12 (go1.13.15; linux; amd64)
,[aws-sdk-java/1.11.792 Linux/4.9.217-0.1.ac.205.84.332.metal1.x86_64 OpenJDK_64-Bit_Server_VM/25.252-b09 java/1.8.0_252 vendor/Oracle_Corporation]
) - The information of the SDK used through an AWS Toolkit (e.g.
AWS-Toolkit-For-JetBrains/1.7 IntelliJ-IDEA-Ultimate-Edition/2019.1.2, aws-sdk-java/2.6.5 Linux/4.15.0-108-generic OpenJDK_64-Bit_Server_VM/25.202-b49 Java/1.8.0_202-release kotlin vendor/JetBrains_s.r.o io/sync http/UNKNOWN PAGINATED/2.6.5
) - The information of the AWS CLI used (e.g.
aws-cli/2.0.57 Python/3.7.3 Linux/5.4.0-56-generic exe/x86_64.ubuntu.20 command/cloudformation.deploy
) AWS Internal
errorCode & errorMessage
If an error occurs (included if an action is not permitted - Access Denied) it is logged into CloudTrail.
Most of the time we have the errorCode
and the errorMessage
, but sometimes only one of them.
userIdentity
For user activity tracking or enforcing the Principle Of Least Privilege, the userIdentity
is a very important piece of the puzzle.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"userIdentity" : {
"type" : "AssumedRole",
"principalId" : "<value>:botocore-session-1612455092",
"arn" : "arn:aws:sts::<account_id>:assumed-role/codebuild-deploy-assumable-role/botocore-session-1612455092",
"accountId" : "<account_id>",
"sessionContext" : {
"sessionIssuer" : {
"type" : "Role",
"principalId" : "<value>",
"arn" : "arn:aws:iam::<account_id>:role/codebuild-deploy-assumable-role",
"accountId" : "<account_id>",
"userName" : "codebuild-deploy-assumable-role"
},
"webIdFederationData" : { },
"attributes" : {
"mfaAuthenticated" : "false",
"creationDate" : "2021-02-04T16:11:32Z"
}
},
"invokedBy" : "cloudformation.amazonaws.com"
}
}
Let’s start with the attribute type
, which identify which entity has made the action. It can have the following values:
AssumedRole
AWSAccount
AWSService
IAMUser
Root
FederatedUser
SAMLUser
Unknown
<novalue>
- the attribute is absent
The attribute userName
has a value only for certain types:
- For the types
IAMUser
,FederatedUser
andSAMLUser
the value is the user real name (the one used to login) - For the type
Root
, the value is an AWS Account ID or account alias if defined - For all the other types the
userName
attribute is absent
The arn
attribute has the following value depending on the type
:
Root
:arn:aws:iam::<aws_account_id>:root
IAMUser
:arn:aws:iam::<aws_account_id>:user/<username>
. For the Event TypeAwsConsoleSignIn
thearn
does not always have a value (e.g.CheckMfa
event)AssumedRole
:arn:aws:sts::<aws_account_id>:assumed-role/<role_name>/<session_name>
orarn:aws:sts::<aws_account_id>:assumed-role/<role_name>/<user_name>
- For all the other types, the
arn
attribute is absent
Going a little further, the type AssumedRole
has the attribute sessionContext.sessionIssuer.arn
where the arn
’s value is the actual ARN of the role used to do the action (e.g. arn:aws:iam::<account_id>:role/codebuild-deploy-assumable-role"
)
The accountId
is the ID of the AWS Account in which the action has been made. For the types SAMLUser
, FederatedUser
and AWSService
, the attribute is not set. For the type AWSAccount
the value can either be an AWS account ID or ANONYMOUS_PRINCIPAL
.
AssumeRole Event
In order to gather the activity from each user, we first need to understand the workflow in term of CloudTrail Events.
A user can login through the console, or using an SSO, then either stay in the AWS account or switch role to access another Account (It is why a centralized logging S3 Bucket is mandatory for a project using multiple AWS Accounts. It makes exploiting CloudTrail logs much easier.).
Let’s start with a Login from the AWS Console Event (for clarity, only the useful attributes are shown):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"eventVersion" : "1.08",
"userIdentity" : {
"type" : "IAMUser",
"arn" : "arn:aws:iam::A_0123456789:user/Yohan.Beschi@arhs-spikeseed.com",
"accountId" : "A_0123456789",
"userName" : "Yohan.Beschi@arhs-spikeseed.com"
},
"eventTime" : "2021-02-11T07:56:54Z",
"eventSource" : "signin.amazonaws.com",
"eventName" : "ConsoleLogin",
"responseElements" : {
"ConsoleLogin" : "Success"
},
"recipientAccountId" : "A_0123456789"
}
The user Yohan.Beschi@arhs-spikeseed.com
logged in successfully into the AWS Account with the ID: A_0123456789
. If the login failed, we would have the same information but the reponseElements
would be: "ConsoleLogin" : "Failure"
.
Now the connected user decides to switch from the current AWS Account (A_0123456789
for this example) to another AWS Account (B_9876543210
) using the SwitchRole/AssumeRole feature. Here things start to get a little crazy.
In the account A_0123456789
a SwitchRole
Event with userIdentity.type = IAMUser
is generated:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"eventVersion" : "1.08",
"userIdentity" : {
"type" : "IAMUser",
"arn" : "arn:aws:iam::A_0123456789:user/Yohan.Beschi@arhs-spikeseed.com",
"accountId" : "A_0123456789",
"userName" : "Yohan.Beschi@arhs-spikeseed.com"
},
"eventTime" : "2021-02-11T07:57:32Z",
"eventSource" : "signin.amazonaws.com",
"eventName" : "SwitchRole",
"responseElements" : {
"SwitchRole" : "Success"
},
"additionalEventData" : {
"SwitchTo" : "arn:aws:iam::B_9876543210:role/cloudadmin-console-assumable-role"
},
"recipientAccountId" : "A_0123456789"
}
In the account B_9876543210
a SwitchRole
Event with userIdentity.type = AssumedRole
is generated:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"eventVersion" : "1.08",
"userIdentity" : {
"type" : "AssumedRole",
"arn" : "arn:aws:sts::B_9876543210:assumed-role/cloudadmin-console-assumable-role/Yohan.Beschi@arhs-spikeseed.com",
"accountId" : "B_9876543210"
},
"eventTime" : "2021-02-11T07:57:32Z",
"eventSource" : "signin.amazonaws.com",
"eventName" : "SwitchRole",
"responseElements" : {
"SwitchRole" : "Success"
},
"recipientAccountId" : "B_9876543210"
}
Furthermore, in the account A_0123456789
a AssumeRole
Event with userIdentity.type = IAMUser
is generated:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"eventVersion" : "1.08",
"userIdentity" : {
"type" : "IAMUser",
"arn" : "arn:aws:iam::A_0123456789:user/Yohan.Beschi@arhs-spikeseed.com",
"accountId" : "A_0123456789",
"userName" : "Yohan.Beschi@arhs-spikeseed.com",
},
"eventTime" : "2021-02-11T07:57:32Z",
"eventSource" : "sts.amazonaws.com",
"eventName" : "AssumeRole",
"requestParameters" : {
"roleArn" : "arn:aws:iam::B_9876543210:role/cloudadmin-console-assumable-role",
"roleSessionName" : "Yohan.Beschi@arhs-spikeseed.com"
},
"responseElements" : {
"assumedRoleUser" : {
"arn" : "arn:aws:sts::B_9876543210:assumed-role/cloudadmin-console-assumable-role/Yohan.Beschi@arhs-spikeseed.com"
}
},
"recipientAccountId" : "A_0123456789",
"sharedEventID" : "73bbd83b-9a8f-4a3f-8e54-5c214c65692e"
}
And finally, in the account B_9876543210
a AssumeRole
Event with userIdentity.type = AWSAccount
is generated:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"eventVersion" : "1.08",
"userIdentity" : {
"type" : "AWSAccount",
"accountId" : "A_0123456789"
},
"eventTime" : "2021-02-11T07:57:32Z",
"eventSource" : "sts.amazonaws.com",
"eventName" : "AssumeRole",
"requestParameters" : {
"roleArn" : "arn:aws:iam::B_9876543210:role/cloudadmin-console-assumable-role",
"roleSessionName" : "Yohan.Beschi@arhs-spikeseed.com"
},
"responseElements" : {
"assumedRoleUser" : {
"arn" : "arn:aws:sts::B_9876543210:assumed-role/cloudadmin-console-assumable-role/Yohan.Beschi@arhs-spikeseed.com"
}
},
"recipientAccountId" : "B_9876543210",
"sharedEventID" : "73bbd83b-9a8f-4a3f-8e54-5c214c65692e"
}
While the SwitchRole
Events are pretty useless, the AssumeRole
ones are not.
First, we have a sharedEventID
attribute that links the two events, which have been generated in two different AWS Accounts, together. And second, both events have the same requestParameters
and responseElements.assumedRoleUser.arn
arn:aws:sts::B_9876543210:assumed-role/cloudadmin-console-assumable-role/Yohan.Beschi@arhs-spikeseed.com
Let’s see few other examples with AssumeRole
and other userIdentity.type values
without going into a detailed explanation:
userIdentity.type = SAMLUser
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"userIdentity" : {
"type" : "SAMLUser",
"userName" : "Yohan.Beschi@arhs-spikeseed.com",
},
"eventTime" : "2021-02-09T12:00:43Z",
"eventSource" : "sts.amazonaws.com",
"eventName" : "AssumeRoleWithSAML",
"responseElements" : {
"assumedRoleUser" : {
"arn" : "arn:aws:sts::<account_id>:assumed-role/AWSReservedSSO_AdministratorAccess_<id>/Yohan.Beschi@arhs-spikeseed.com"
}
}
userIdentity.type = AWSAccount
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"eventVersion" : "1.08",
"userIdentity" : {
"type" : "AWSAccount",
"accountId" : "<account_id>"
},
"eventTime" : "2021-02-02T11:31:20Z",
"eventSource" : "sts.amazonaws.com",
"eventName" : "AssumeRole",
"responseElements" : {
"assumedRoleUser" : {
"arn" : "arn:aws:sts::<another_account_id>:assumed-role/cloudadmin-pa-assumable-role/botocore-session-1612265479"
}
}
}
userIdentity.type = AWSService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"eventVersion" : "1.08",
"userIdentity" : {
"type" : "AWSService",
"invokedBy" : "trustedadvisor.amazonaws.com"
},
"eventTime" : "2020-11-24T16:51:08Z",
"eventSource" : "sts.amazonaws.com",
"eventName" : "AssumeRole",
"requestParameters" : {
"roleArn" : "arn:aws:iam::<account_id>:role/aws-service-role/trustedadvisor.amazonaws.com/AWSServiceRoleForTrustedAdvisor",
"roleSessionName" : "TrustedAdvisor_<account_id>_1a6601d4-3a39-4fbe-a820-98d23299927f"
},
"responseElements" : {
"assumedRoleUser" : {
"arn" : "arn:aws:sts::<account_id>:assumed-role/AWSServiceRoleForTrustedAdvisor/TrustedAdvisor_<account_id>_57b9d37f-975b-4787-b0ee-cc7b5d714025"
}
}
}
userIdentity.type = AssumedRole and userIdentity.sessionContext.sessionIssuer.type = Role
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
{
"eventVersion" : "1.08",
"userIdentity" : {
"type" : "AssumedRole",
"arn" : "arn:aws:sts::<account_id_a>:assumed-role/codebuild-deploy-test-app-role/AWSCodeBuild-5923ff10-3773-4c14-a807-3f4185513106",
"accountId" : "<account_id_a>",
"accessKeyId" : "[...]",
"sessionContext" : {
"sessionIssuer" : {
"type" : "Role",
"principalId" : "[...]",
"arn" : "arn:aws:iam::<account_id_a>:role/codebuild-deploy-test-app-role",
"accountId" : "<account_id_a>",
"userName" : "codebuild-deploy-test-app-role"
},
"webIdFederationData" : { },
"attributes" : {
"mfaAuthenticated" : "false",
"creationDate" : "2021-02-04T15:11:52Z"
}
}
},
"eventTime" : "2021-02-04T15:20:52Z",
"eventSource" : "sts.amazonaws.com",
"eventName" : "AssumeRole",
"awsRegion" : "us-east-1",
"sourceIPAddress" : "34.250.63.250",
"userAgent" : "Boto3/1.17.1 Python/3.8.3 Linux/4.14.203-116.332.amzn1.x86_64 exec-env/AWS_ECS_EC2 Botocore/1.20.1",
"requestParameters" : {
"roleArn" : "arn:aws:iam::<account_id_b>:role/codebuild-deploy-assumable-role",
"roleSessionName" : "botocore-session-1612452052"
},
"responseElements" : {
"assumedRoleUser" : {
"arn" : "arn:aws:sts::<account_id_b>:assumed-role/codebuild-deploy-assumable-role/botocore-session-1612452052"
}
}
}
Conclusion
Understanding CloudTrail Logs is not easy. But it is mandatory to be able to exploit them seamlessly when needed.
Furthermore, this will help us for an upcoming article, when we will investigate ways to implement the Principle of Least Privilege on IAM Roles.