Skip to content
Ashish.
All posts
Diagram showing the structure of a CloudTrail event with UserIdentity and SessionContext fields highlighted.
6 min readSecurityCloud Engineers, Security AnalystsFeatured#aws#cloudtrail#security#athena#cloud#investigation#iam

Reading CloudTrail Events: Identity & Athena

A practical guide to reading CloudTrail events, focusing on UserIdentity, SessionContext, and Athena for AWS security investigation.

By Ashish KumarPart 5 of AWS Security Observability

Reading a CloudTrail Event

CloudTrail is often misunderstood as a simple ledger of "who did what." In reality, it is a structured data stream that captures the state of the AWS security boundary at the moment of interaction. For security engineers, the value lies not in the existence of the log, but in the precise interpretation of its nested fields. Specifically, the userIdentity and sessionContext objects contain the mechanistic details required to trace privilege escalation, lateral movement, and unauthorized access.

This guide breaks down the internal structure of a CloudTrail event and demonstrates how to query it using Amazon Athena for effective security investigation. This is Part 2 of the AWS Security Observability series.

The Anatomy of an Event

Every CloudTrail event is a JSON document. While the full schema is extensive, four top-level keys form the foundation of any forensic analysis:

  1. eventVersion: The schema version (e.g., "1.08").
  2. eventTime: The timestamp in ISO 8601 format, representing when AWS processed the request.
  3. eventSource: The AWS service involved (e.g., ec2.amazonaws.com).
  4. eventName: The specific API call made (e.g., DescribeInstances).

However, the investigative power resides in the userIdentity block. This object describes the principal that initiated the request. It is here that we distinguish between a direct IAM user login and a delegated session.

Consider this simplified excerpt from a CloudTrail event:

{
  "userIdentity": {
    "type": "AssumedRole",
    "principalId": "AROAEXAMPLEID:SessionName",
    "arn": "arn:aws:sts::123456789012:assumed-role/AdminRole/SessionName",
    "accountId": "123456789012",
    "sessionContext": {
      "sessionIssuer": {
        "type": "Role",
        "principalId": "AROAEXAMPLEID",
        "arn": "arn:aws:iam::123456789012:role/AdminRole",
        "accountId": "123456789012"
      },
      "attributes": {
        "mfaAuthenticated": "false",
        "creationDate": "2023-10-27T10:00:00Z"
      }
    }
  }
}
Technical architecture diagram of a CloudTrail event JSON structure. Highlight userIdentity and sessionContext objects with glowing borders. Clean, minimalist style, dark mode background, blue and purple accent colors. Show arrows connecting userIdentity.type to sessionContext…

The Identity Chain: Deconstructing Trust

The critical mechanism in CloudTrail is the relationship between userIdentity and sessionContext.sessionIssuer. This structure allows you to reconstruct the chain of trust from the immediate actor back to the root principal. For detailed field definitions, refer to the AWS CloudTrail User Guide.

1. The Immediate Actor (userIdentity)

The userIdentity.type field tells you how the request was authenticated.

  • IAMUser: The request was made directly by an IAM user using long-term credentials or an MFA-protected console session.
  • AssumedRole: The request was made by a temporary security credential. This is the most common source of lateral movement in cloud environments. When an attacker compromises an IAM role, the userIdentity.type will be AssumedRole.
  • Root: The account root user. This is rare and highly privileged.

In the example above, type is AssumedRole. This immediately signals that the actor is not a human logging in directly, but a service or process acting on behalf of a role.

2. The Delegating Principal (sessionContext.sessionIssuer)

When a role is assumed, AWS generates temporary credentials. The sessionContext.sessionIssuer object contains the ARN of the role that was assumed. This is the key to tracing delegation.

  • sessionIssuer.arn: Identifies the role that granted the temporary credentials. In our example, this is arn:aws:iam::123456789012:role/AdminRole.
  • sessionIssuer.principalId: The unique ID of the role.

By linking userIdentity.arn (the assumed role session) to sessionContext.sessionIssuer.arn (the role definition), you can determine if a role was assumed by another role, a user, or an external identity provider.

Note: When querying in Athena, be aware that sessionContext.sessionIssuer.accountId may be null if the actor is an IAM user rather than an assumed role. Ensure your queries handle potential NULL values to prevent errors in mixed-user/role environments.

3. Authentication Context (sessionContext.attributes)

The sessionContext.attributes object provides additional context about the session's origin.

  • mfaAuthenticated: A string "true" or "false". If an admin action is performed via an assumed role and mfaAuthenticated is "false", it may indicate that MFA was not enforced during the initial assumption, which is a security risk.
  • creationDate: When the session was created. This helps correlate events across multiple logs.

Investigative Workflow with Athena

Querying CloudTrail logs directly from Amazon S3 using Amazon Athena is efficient because it avoids the cost and latency of forwarding all logs to a SIEM or Elasticsearch cluster for initial triage.

Prerequisites

  1. CloudTrail trails configured to deliver logs to an S3 bucket.
  2. An Athena database and table created for the CloudTrail logs.

Querying for Suspicious Assumed Roles

A common security scenario is detecting when an IAM role is assumed from an unexpected source or without MFA.

Example 1: Finding All Assumed Role Sessions Without MFA

This query identifies all API calls made by assumed roles where MFA was not authenticated during the session creation.

SELECT
    eventTime,
    eventName,
    userIdentity.arn AS actor_arn,
    sessionContext.sessionIssuer.arn AS issuer_arn,
    sessionContext.attributes.mfaAuthenticated
FROM
    cloudtrail_logs
WHERE
    userIdentity.type = 'AssumedRole'
    AND sessionContext.attributes.mfaAuthenticated = 'false'
LIMIT 100;

Mechanism Explanation:

  • userIdentity.type = 'AssumedRole': Filters for temporary credentials.
  • sessionContext.attributes.mfaAuthenticated = 'false': Checks the session attributes. Note that this field reflects whether MFA was used to assume the role, not necessarily for every subsequent API call. If a role is assumed without MFA, all subsequent actions under that session inherit the lack of MFA verification.

Case Study: Detecting Lateral Movement in a Multi-Account Setup

During a recent incident response engagement, we noticed a spike in AssumeRole events originating from a development account assuming roles in a production account. By running a targeted Athena query, we identified that a CI/CD pipeline role in the dev account had overly permissive trust policies, allowing any IAM user in the dev account to assume it. This role then assumed an admin role in prod.

The query below helped us pinpoint the specific IAM users in the dev account triggering these assumptions:

SELECT
    eventTime,
    eventName,
    userIdentity.arn AS actor_arn,
    sessionContext.sessionIssuer.arn AS issuer_arn,
    sourceIPAddress
FROM
    cloudtrail_logs
WHERE
    userIdentity.type = 'AssumedRole'
    AND sourceIPAddress LIKE '%dev-ip-range%'
LIMIT 50;

This allowed us to immediately revoke the compromised developer credentials and tighten the trust policy, stopping the lateral movement within minutes.

Example 2: Tracing Cross-Account Access

Security teams often need to identify when roles from one AWS account assume roles in another. This is a common pattern in multi-account architectures but can also indicate misconfiguration or compromise.

SELECT
    eventTime,
    eventName,
    userIdentity.accountId AS calling_account,
    recipientAccountId AS resource_account,
    userIdentity.arn AS actor_arn
FROM
    cloudtrail_logs
WHERE
    userIdentity.type = 'AssumedRole'
    AND userIdentity.accountId != recipientAccountId
LIMIT 100;

Mechanism Explanation:

  • userIdentity.accountId: The account that owns the role used to make the call.
  • recipientAccountId: The account that received the API call, typically the account that owns the resource being accessed.
  • If these values differ, it indicates cross-account resource access.
Screenshot-style illustration of an AWS Athena SQL query editor. Display the provided SQL code clearly. Background shows a simplified S3 bucket icon and CloudTrail log file icon connecting to the query. Modern UI, dark theme, clean typography.

Best Practices for Athena Queries

  1. Use Partition Pruning: Ensure your Athena table is partitioned by year, month, and day. This significantly reduces the amount of data scanned.
  2. Filter Early: Always include eventTime ranges in your WHERE clause to limit the scan window.
  3. Handle Nested Fields: Athena requires dot notation for nested JSON fields (e.g., userIdentity.arn). Ensure your table schema correctly maps these nested structures.

Common Pitfalls

  1. Ignoring Partition Pruning Costs: If your Athena table is not partitioned by date, queries can scan terabytes of data, leading to unexpectedly high costs. Always verify your table schema includes dt partitions.
  2. Misinterpreting mfaAuthenticated Scope: As noted earlier, mfaAuthenticated indicates whether MFA was used to assume the role, not whether it was used for the specific API call. An attacker who assumes a role without MFA can perform subsequent actions without MFA, even if the role has MFA requirements. Do not assume MFA protection for every action just because the role enforces it.
  3. Null Values in sessionIssuer: When querying for IAM users, sessionContext.sessionIssuer will be null. Queries that do not account for this may return incomplete results or throw errors depending on how the data is joined or filtered.

Practical Takeaways

  • Focus on the Chain: Always trace from userIdentity back to sessionContext.sessionIssuer to understand the full delegation path.
  • Verify MFA Context: Check mfaAuthenticated in the context of role assumption, not just individual API calls.
  • Use Athena for Triage: Leverage Athena for initial investigation due to its cost-effectiveness and flexibility, but ensure proper partitioning to manage costs.
  • Monitor Cross-Account Assumptions: Set up alerts for unexpected cross-account role assumptions, especially from non-production accounts.

FAQ

Q: How can I tell if an IAM user assumed a role vs. a role assuming another role? A: Check the userIdentity.type field. If it is AssumedRole, look at the principalId in the userIdentity block. If the principalId contains a colon (e.g., AROA...:SessionName), it is a session name. To see what assumed it, you would need to look at the preceding event where that session was created, typically an AssumeRole or AssumeRoleWithSAML event.

Q: Why is my Athena query returning no results for mfaAuthenticated = 'false'? A: Ensure you are filtering for userIdentity.type = 'AssumedRole'. IAM users do not have a sessionContext with mfaAuthenticated in the same way; their authentication context is stored differently. Also, remember that if MFA was used, the value will be "true", not true (boolean).

Q: Can I use Athena to detect brute force attacks? A: Yes, by filtering for eventName = 'ConsoleLogin' and userIdentity.type = 'IAMUser' with responseElements.ConsoleLogin values like Failure, you can track failed login attempts. Grouping by sourceIPAddress and userIdentity.userName can help identify brute force patterns.

Conclusion

Reading CloudTrail events is not about memorizing API names; it is about understanding the identity chain. By focusing on userIdentity.type and sessionContext.sessionIssuer, you can reconstruct the path of trust from the immediate actor back to the root principal. This mechanistic understanding, combined with Athena queries, enables precise, low-latency security investigations directly on your log data.

Remember: every AssumedRole event is a delegation. Your job is to verify that the delegation was authorized, authenticated, and appropriate for the action taken.

Related posts