Finding Who Deleted an EC2 Instance Using CloudTrail Event History
An EC2 instance disappears without warning — no ticket, no change request, no explanation. Before you can prevent it from happening again, you need to answer one question: who did it? AWS CloudTrail is your forensic audit log, and knowing how to query it efficiently is a non-negotiable skill for any cloud engineer or security responder.
TL;DR
| Step | Action | Where |
|---|---|---|
| 1 | Open CloudTrail Event History | AWS Console → CloudTrail → Event History |
| 2 | Filter by Event Name: TerminateInstances |
Event History filter bar |
| 3 | Narrow by time range and Resource ID | Date picker + Resource ID filter |
| 4 | Inspect the event record for userIdentity |
Event detail JSON |
| 5 | (Optional) Query via AWS CLI or Athena for scale | Terminal / Athena Console |
How CloudTrail Captures EC2 Termination Events
Every AWS API call — including ec2:TerminateInstances — is recorded by CloudTrail as a structured JSON event. CloudTrail Event History retains the last 90 days of management events (control-plane API calls) at no additional charge, per region, per account. For longer retention or cross-account aggregation, you need a Trail writing to S3.
Analogy: Think of CloudTrail as the keycard access log in a secure building. Every door opened (API call made), by every badge holder (IAM principal), at every timestamp, is recorded. When a server goes missing from the server room, you don't guess — you pull the log.
The Data Flow: From API Call to Audit Record
(User / Role / Service)"] -->|"ec2:TerminateInstances API Call"| B["EC2 API Endpoint"] B -->|"Executes Termination"| C["EC2 Instance
(Shutting Down)"] B -->|"Management Event Recorded"| D["CloudTrail"] D -->|"Queryable for 90 days"| E["Event History
(Console / CLI)"] D -->|"If Trail configured"| F["S3 Bucket
(Long-term Retention)"] F -->|"SQL Query"| G["Amazon Athena"] D -->|"If Trail + EventBridge rule"| H["EventBridge → SNS
(Real-time Alert)"]
- IAM Principal (user, role, or service) calls the EC2
TerminateInstancesAPI — via Console, CLI, SDK, or automation. - EC2 API Endpoint receives and authorizes the request, then executes the termination.
- CloudTrail intercepts the management event and writes a structured JSON record containing the caller identity, source IP, timestamp, request parameters, and response.
- Event History makes the last 90 days of events queryable in the console without any Trail configuration.
- If a Trail is configured, the same event is also delivered to an S3 bucket (and optionally CloudWatch Logs or EventBridge) for long-term retention and alerting.
Step-by-Step: Console Investigation
Step 1 — Navigate to CloudTrail Event History
Go to AWS Console → CloudTrail → Event History. Ensure you are in the correct AWS region where the EC2 instance existed. CloudTrail Event History is region-scoped for regional services.
Step 2 — Filter by Event Name
In the filter dropdown, select Event name and type TerminateInstances. This is the exact EC2 API action name recorded by CloudTrail. Set your time range to bracket the suspected deletion window.
Step 3 — Identify the Specific Instance
If multiple terminations appear, add a second filter: select Resource ID and enter the instance ID (e.g., i-0abc1234def567890). This narrows results to events that reference that specific resource.
Step 4 — Inspect the Event Record
Click the matching event row to expand it, then click View event to see the full JSON. The critical fields are:
(IAMUser / AssumedRole / Root)"] UI --> UA["arn
(Full IAM ARN of caller)"] UI --> USC["sessionContext
(For AssumedRole: reveals base role)"] RP --> IS["instancesSet.items
(List of targeted instance IDs)"] RE --> CS["currentState
(shutting-down / terminated)"] RE --> PS["previousState
(running / stopped)"]
- userIdentity.type — Identifies the principal type:
IAMUser,AssumedRole,Root,AWSService, etc. - userIdentity.arn — The full ARN of the caller (e.g.,
arn:aws:iam::123456789012:user/john.doeorarn:aws:sts::123456789012:assumed-role/DevOpsRole/session-name). - userIdentity.principalId — A unique identifier for the principal; useful when the ARN alone is ambiguous for assumed roles.
- sourceIPAddress — The IP address from which the call originated. For calls made by AWS services on your behalf, this will be an AWS service DNS name.
- eventTime — The exact UTC timestamp of the API call.
- requestParameters.instancesSet — Lists the instance IDs that were targeted by the termination request.
🔽 Example CloudTrail Event JSON for TerminateInstances
{
"eventVersion": "1.08",
"userIdentity": {
"type": "AssumedRole",
"principalId": "AROAEXAMPLEID:session-name",
"arn": "arn:aws:sts::123456789012:assumed-role/DevOpsRole/session-name",
"accountId": "123456789012",
"sessionContext": {
"sessionIssuer": {
"type": "Role",
"principalId": "AROAEXAMPLEID",
"arn": "arn:aws:iam::123456789012:role/DevOpsRole",
"accountId": "123456789012",
"userName": "DevOpsRole"
},
"attributes": {
"creationDate": "2024-01-15T10:00:00Z",
"mfaAuthenticated": "false"
}
}
},
"eventTime": "2024-01-15T14:32:11Z",
"eventSource": "ec2.amazonaws.com",
"eventName": "TerminateInstances",
"awsRegion": "us-east-1",
"sourceIPAddress": "203.0.113.42",
"userAgent": "aws-cli/2.13.0",
"requestParameters": {
"instancesSet": {
"items": [
{ "instanceId": "i-0abc1234def567890" }
]
}
},
"responseElements": {
"instancesSet": {
"items": [
{
"instanceId": "i-0abc1234def567890",
"currentState": { "code": 32, "name": "shutting-down" },
"previousState": { "code": 16, "name": "running" }
}
]
}
},
"eventID": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"readOnly": false,
"eventType": "AwsApiCall",
"managementEvent": true,
"recipientAccountId": "123456789012"
}
Step-by-Step: AWS CLI Investigation
The console is useful for quick lookups, but the AWS CLI gives you scriptable, repeatable forensics. Use aws cloudtrail lookup-events to query Event History programmatically.
🔽 AWS CLI: Look up TerminateInstances events
# Look up all TerminateInstances events in the last 90 days (region-scoped)
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=TerminateInstances \
--start-time "2024-01-15T00:00:00Z" \
--end-time "2024-01-16T00:00:00Z" \
--region us-east-1 \
--query 'Events[*].{Time:EventTime,User:Username,EventId:EventId}' \
--output table
# Get the full JSON of a specific event by EventId
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventId,AttributeValue=a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
--region us-east-1 \
--query 'Events[0].CloudTrailEvent' \
--output text | python3 -m json.tool
# Filter by Resource (instance ID) — note: lookup-events supports one attribute at a time
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=ResourceName,AttributeValue=i-0abc1234def567890 \
--region us-east-1 \
--output json
Important constraint: lookup-events only supports a single --lookup-attributes filter per call. To combine filters (e.g., EventName AND ResourceName), retrieve results by one attribute and post-filter with jq or Python.
🔽 Post-filter with jq: Find termination of a specific instance
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=TerminateInstances \
--region us-east-1 \
--output json | \
jq '.Events[] |
select(
(.CloudTrailEvent | fromjson | .requestParameters.instancesSet.items[].instanceId)
== "i-0abc1234def567890"
) | {
eventTime: .EventTime,
user: .Username,
sourceIP: (.CloudTrailEvent | fromjson | .sourceIPAddress),
userArn: (.CloudTrailEvent | fromjson | .userIdentity.arn)
}'
Beyond 90 Days: Querying a Trail with Athena
If the deletion occurred more than 90 days ago, or you need to query across multiple accounts/regions, you need a CloudTrail Trail delivering logs to S3, queried via Amazon Athena.
🔽 Athena SQL: Find TerminateInstances for a specific instance
-- Assumes you have created an Athena table over your CloudTrail S3 bucket
-- using the AWS-provided CloudTrail table DDL or AWS Glue crawler
SELECT
eventtime,
useridentity.arn AS caller_arn,
useridentity.type AS caller_type,
sourceipaddress,
useragent,
json_extract_scalar(
requestparameters,
'$.instancesSet.items[0].instanceId'
) AS instance_id
FROM cloudtrail_logs
WHERE
eventsource = 'ec2.amazonaws.com'
AND eventname = 'TerminateInstances'
AND requestparameters LIKE '%i-0abc1234def567890%'
AND eventtime BETWEEN '2024-01-01T00:00:00Z' AND '2024-01-31T23:59:59Z'
ORDER BY eventtime DESC;
IAM Permissions Required for This Investigation
Apply least privilege — the investigator only needs read access to CloudTrail, not write or administrative permissions.
🔽 Minimal IAM Policy for CloudTrail Forensics
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CloudTrailReadOnly",
"Effect": "Allow",
"Action": [
"cloudtrail:LookupEvents",
"cloudtrail:GetTrailStatus",
"cloudtrail:DescribeTrails",
"cloudtrail:GetEventSelectors"
],
"Resource": "*"
},
{
"Sid": "S3ReadForTrailLogs",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::your-cloudtrail-bucket",
"arn:aws:s3:::your-cloudtrail-bucket/*"
]
},
{
"Sid": "AthenaQueryForTrailLogs",
"Effect": "Allow",
"Action": [
"athena:StartQueryExecution",
"athena:GetQueryExecution",
"athena:GetQueryResults"
],
"Resource": "arn:aws:athena:us-east-1:123456789012:workgroup/primary"
}
]
}
Preventive Controls: Stop the Next Deletion
Forensics tells you what happened. These controls prevent recurrence:
- EC2 Termination Protection: Enable
DisableApiTerminationon critical instances. This requires an explicit API call to disable protection before termination can succeed — creating an additional, auditable step. - SCP (Service Control Policy): In AWS Organizations, use an SCP to deny
ec2:TerminateInstancesfor production accounts except via a specific approved role. - EventBridge Rule + SNS Alert: Create an EventBridge rule that matches
TerminateInstancesCloudTrail events and sends an immediate SNS notification to your on-call channel. - IAM Permission Boundaries: Restrict which roles can call
ec2:TerminateInstancesusing permission boundaries, limiting blast radius from compromised credentials.
Glossary
| Term | Definition |
|---|---|
| CloudTrail Event History | A 90-day, region-scoped, no-configuration-required log of management (control-plane) API calls in your AWS account. |
| Management Event | An API call that creates, modifies, or deletes AWS resources (e.g., TerminateInstances, DeleteBucket). Distinct from data events (e.g., S3 GetObject). |
| userIdentity | The CloudTrail JSON field that identifies the IAM principal (user, role, service) that made the API call, including their ARN and session context. |
| AssumedRole | A userIdentity.type value indicating the caller authenticated via sts:AssumeRole. The sessionContext.sessionIssuer field reveals the underlying IAM role. |
| CloudTrail Trail | A configuration that continuously delivers CloudTrail events to an S3 bucket (and optionally CloudWatch Logs), enabling retention beyond 90 days and cross-region/cross-account aggregation. |
Next Steps
You now have a repeatable forensic playbook for tracing any destructive API action in AWS. As immediate next steps: (1) verify your account has at least one multi-region Trail writing to S3 for retention beyond 90 days, (2) enable CloudTrail log file integrity validation to ensure logs haven't been tampered with, and (3) consider enabling AWS Config alongside CloudTrail — Config records resource state changes while CloudTrail records API calls, giving you complementary forensic views.
Comments
Post a Comment