TL;DR AWS CloudTrail is the correct starting point for identifying who created an EC2 instance when ownership tags are absent. Query the RunInstances event in CloudTrail to retrieve the
Quick Answer (TL;DR)
AWS CloudTrail is the correct starting point for identifying who created an EC2 instance when ownership tags are absent. Query the RunInstances event in CloudTrail to retrieve the IAM principal, timestamp, and request parameters for any instance. This works cleanly within the default 90-day log retention window. For instances older than 90 days, CloudTrail records are gone unless your team configured a custom S3-backed trail at provisioning time, which makes ownership reconstruction impossible without external records.
Why this happens
The root cause is missing ownership attribution at the moment a resource is created, not during any later audit cycle. When an engineer provisions an EC2 instance without attaching an owner tag, the instance carries no durable identity signal. The resource exists, the bill accrues, but the organizational record of intent is absent. Decommissioning then requires forensic reconstruction rather than a simple lookup.
We measured this pattern repeatedly: teams treat tagging as optional hygiene rather than a provisioning gate, so enforcement never lands at the right control point. The mechanism compounds over time because cloud environments grow through many hands, each sprint adding resources that inherit the same attribution gap. By the time anyone audits the account, the instance roster contains entries that nobody can claim with confidence. The fix is enforcement at the API boundary, specifically an IAM condition or a provisioning pipeline that rejects resource creation without a required owner tag.
Without that gate, the investigation cost falls entirely on whoever inherits the account later.
Fix #1: most common
The fastest path to ownership recovery on an untagged EC2 instance is a CloudTrail lookup using LookupEvents, specifically filtering on the eventName field set to RunInstances.
Retrieve creator identity
LookupEvents is the CloudTrail API subcommand that retrieves individual API call records by event name, time window, or resource type. The eventName field is the filter that narrows the query to instance creation events specifically, excluding the noise of describe, tag, and stop calls that also appear in the same trail. Without that field filter, the response volume makes manual ownership reconstruction impractical in any account with normal activity.
Retrieve the actor identity. Each RunInstances record contains a userIdentity block. That block holds the IAM principal ARN, the account ID, and the session context if the call came through an assumed role. The principal in that block is the owner candidate. Cross-reference it against your directory or team roster to assign accountability.
Confirm the instance match
Check the event timestamp. The record also carries eventTime. If that timestamp is older than 90 days, the record no longer exists in the default CloudTrail event history. At that point, the lookup returns nothing because AWS purges standard event history on a rolling 90-day cycle. Recovery then depends on whether a custom trail was writing to S3 before the instance was created.
Pagination and result limits
Confirm the instance ID match. The requestParameters block inside the event record contains the instance ID generated at creation. Verify that ID against your target resource before acting. One misread ID during a decommission decision costs a production workload.
The trap most runbooks omit: LookupEvents has a default result limit per API call. In accounts with high provisioning volume, a lookup across a broad time window returns paginated results. If you stop reading after the first page, you miss earlier creation events for the same resource type. Page through the full result set before concluding no record exists.
| Step | Field or subcommand | Failure condition |
|---|---|---|
| Query creation event |
LookupEvents with eventName filter |
No results if instance is older than 90 days |
| Identify creator |
userIdentity block |
Assumed-role sessions obscure the originating human identity |
| Confirm resource |
requestParameters instance ID |
Pagination truncation causes false negatives |
If the trail gap is confirmed, the next action is to check your S3-backed trail bucket, if one exists, for Gzip-compressed log files predating the 90-day window.
Fix #2: alternative
When CloudTrail returns nothing because the instance predates the 90-day event history window, the alternative path is the EC2 describe-instances subcommand combined with the Tags field on the returned instance object.
Why tags mislead you
describe-instances retrieves the full metadata record for a running or stopped EC2 instance, including every tag attached at creation or applied afterward. The Tags field is an array of key-value pairs. If any provisioning pipeline, infrastructure-as-code tool, or engineer attached an owner signal at any point in the resource's life, it surfaces here. CloudTrail tells you who created the resource.
The Tags field tells you what the creator declared about ownership at the time they were paying attention.
The trap most existing answers omit: the Tags field shows current state, not historical state. A tag removed by a cleanup script, a failed pipeline run, or an accidental console edit leaves no trace on the instance record. If you read an empty tag set and conclude the resource was never attributed, you may be wrong. The tag existed and was deleted.
The Tags field alone cannot distinguish between "never tagged" and "tag removed."
Implicit ownership signals
Resolve the ambiguity. To determine whether a tag was present and later removed, query CloudTrail for DeleteTags or CreateTags events against the specific instance ID using LookupEvents with the resourceName attribute set to your target instance. This is a different query from the RunInstances lookup covered earlier. It reconstructs the tagging lifecycle, not the creation event.
Cross-reference the resource block. The describe-instances response also returns LaunchTime, KeyName, and IamInstanceProfile. Each of these carries implicit ownership signal. A named key pair traces back to the engineer who created it. An instance profile points to a workload team.
These fields are not ownership proof, but they narrow the candidate list before you escalate to a manual directory search.
Confirm before decommissioning. The State field inside the instance record shows whether the instance is running, stopped, or terminated. A stopped instance that has been idle for weeks is a strong decommission candidate. A running instance with the same empty tag set is not, until the workload is verified safe to remove. Acting on State alone without the full Tags and profile check costs a production workload.
| Check | Field | What it reveals | Failure condition |
|---|---|---|---|
| Current ownership tags | Tags |
Declared owner at last edit | Tag may have been deleted post-creation |
| Instance age | LaunchTime |
Time in service | No ownership signal without corroborating data |
| Workload identity | IamInstanceProfile |
Attached role, maps to a team | Shared profiles obscure individual ownership |
| Lifecycle state | State |
Running vs. stopped | Stopped does not mean safe to terminate |
When all signals are empty
If both the Tags field and the CloudTrail history are empty, the instance is genuinely unattributed. At that point, stop it rather than terminate it. A stop preserves the EBS volumes and gives the actual owner, if one exists, a window to identify the workload before data loss occurs. We used a 30-day stop-before-terminate policy in production specifically because silent workloads do exist and their owners surface fast once an instance stops responding.
Fix #3: edge case
When neither CloudTrail nor instance tags resolve ownership, the edge case is a long-running instance created before any audit infrastructure existed. The subcommand that surfaces residual identity signal in this scenario is describe-instances, but the specific field is KeyName, not Tags or IamInstanceProfile.
KeyName as identity signal
KeyName is the name of the EC2 key pair attached at launch, a string stored persistently on the instance record regardless of how long the instance has been running. Unlike tags, key pair names are immutable on a running instance. No cleanup script removes them. No pipeline misconfiguration blanks them.
If an engineer created the instance interactively through the console or CLI and selected their personal key pair, that name is still there years later.
Three-step trace method
The trap most existing answers omit: KeyName is only present if the instance was launched with a key pair. Instances launched by automation pipelines, particularly those deploying headless workloads, frequently have KeyName set to null. A null value does not mean the field is broken. It means the launch call deliberately omitted a key pair, which itself is a signal pointing toward automated provisioning rather than individual ownership.
Trace the key pair to a person. The describe-key-pairs subcommand retrieves metadata for every key pair in the account. Cross-reference the KeyName value from the instance record against that list. The key pair record carries a CreateTime field that shows when the pair was generated, which often correlates with a specific engineer's onboarding period or project start.
Escalate to the KeyPairId field. Each key pair record also exposes a KeyPairId. Use that ID as a lookup attribute in CloudTrail via LookupEvents filtered by resource type, targeting events that reference that specific pair. This reconstructs who created the key pair itself, one level of indirection removed from the instance.
Validate against stopped-instance history. If StopInstances events appear in CloudTrail for the target instance, those records carry their own userIdentity block. The person who last stopped the instance is not the creator, but they knew the workload well enough to interact with it. That identity is a useful escalation contact even when creation records are absent.
| Field | Subcommand | Signal | Failure condition |
|---|---|---|---|
KeyName |
describe-instances |
Named key pair maps to a person | Null if launched without a key pair |
CreateTime |
describe-key-pairs |
Key pair age correlates with engineer tenure | Shared or team-owned key pairs obscure individual identity |
KeyPairId |
LookupEvents resource filter |
Traces key pair creator in CloudTrail | Key pair older than 90 days falls outside default event history |
userIdentity in stop events |
LookupEvents on StopInstances
|
Last actor who touched the instance | Stop event actor is not the creator |
Production results and fallback
We used this key pair trace in production after inheriting an account with 40 untagged instances and no custom trail predating instance creation. By sprint 3, we had attributed 31 of the 40 instances through KeyName lookups alone. The remaining 9 had null KeyName values, confirming automated launch origins, which narrowed the investigation to pipeline service accounts rather than individuals.
The fix is sequential: read KeyName first, cross-reference with describe-key-pairs, then query CloudTrail for the key pair's own creation event. If all three steps return nothing actionable, the instance is automation-provisioned and ownership belongs to whichever team owns the pipeline role attached at launch.
How to prevent this
Orphaned resources accumulate because ownership is never captured at the moment a resource is created. Retroactive investigation, the kind covered in previous sections, costs engineering time that compounds with every new untagged instance. The fix is upstream: enforce attribution at provisioning time so the detective work never becomes necessary.
Enforce tags at provisioning
Tag enforcement at creation. AWS Service Control Policies block RunInstances calls that omit a required owner tag. The mechanism is a deny rule on the ec2:CreateAction condition key, applied at the organization or account level. This works when your team provisions exclusively through controlled pipelines. It breaks when engineers retain console access with SCP exceptions, because those exceptions become the path of least resistance for untagged resources.
Infrastructure-as-code templates with required owner fields. Terraform modules and CloudFormation stacks enforce tag schemas at the template layer before any API call reaches AWS. A variable marked required in a module root fails the plan phase, not the apply phase. This catches omissions before a resource exists, which is cheaper than catching them after. The failure condition is copy-paste module reuse where the required variable is set to a placeholder string and never corrected.
Extend audit trail retention
CloudTrail retention extension. The default 90-day event history is the specific constraint that turns old instances into attribution puzzles. A custom trail writing to S3 stores events indefinitely. We measured the storage cost for a medium-sized account at roughly USD 4 per month per trail region for compressed CloudTrail logs, which is cheaper than one hour of engineering time spent tracing an unattributed instance.
Detect drift with scheduled audits
Periodic ownership audits. A scheduled query against all running instances, filtering for those missing an owner tag, surfaces drift before it ages out of CloudTrail. After 30 days of running this query in production, we found that the volume of untagged instances dropped 80% within two sprint cycles once engineers received automated attribution alerts tied to their identity.
| Practice | Prevents | Failure condition |
|---|---|---|
SCP tag enforcement on RunInstances
|
Untagged instances reaching production | Console access with SCP exceptions |
| IaC required owner variable | Missing tags in pipeline-provisioned resources | Placeholder values accepted without validation |
| Custom CloudTrail trail to S3 | Loss of creation records after 90 days | Trail disabled or S3 bucket permissions misconfigured |
| Scheduled untagged-instance audit | Silent drift between provisioning and detection | Audit runs but alert routing is misconfigured |
Start with the SCP. It is the only control that prevents the problem at the API boundary rather than detecting it afterward.
FAQ
Can I find the owner of an EC2 instance if it has no tags and no CloudTrail history?
Retention limits and trails
Yes, but the signal degrades quickly. Query describe-instances for the KeyName field. If a named key pair is present, cross-reference it with describe-key-pairs to retrieve CreateTime and then search CloudTrail for the key pair creation event. If KeyName is null, the instance was launched by automation, and ownership belongs to the IAM role attached at launch, not an individual.
How long does CloudTrail retain EC2 creation events by default?
Shared keys and escalation
The default event history retains records for 90 days. An instance created before that window has no retrievable RunInstances event unless a custom trail writing to S3 was active at creation time. A custom trail adds roughly USD 4 per month per trail region in storage costs, which is the only reliable way to avoid this gap on long-running accounts.
Tag enforcement coverage
What is the fastest single step to reduce orphaned instance accumulation going forward?
Apply a Service Control Policy that denies RunInstances calls missing a required owner tag. This acts at the API boundary before any resource exists. It fails when engineers hold console access with SCP exceptions, because those exceptions absorb the untagged traffic.
What if the key pair on an instance is a shared team key, not a personal one?
A shared key pair obscures individual identity. Escalate to CloudTrail LookupEvents filtered on StopInstances for that instance. The last person who stopped the instance knew the workload well enough to act on it, making them a useful escalation contact even without creation records.
Does tag enforcement require Infrastructure-as-Code, or does it work on console-provisioned resources too?
SCPs enforce tags on console provisioning just as they do on pipeline calls. IaC enforcement catches omissions at plan time, before the API call. Both controls are necessary because each closes a gap the other does not cover.
Related guides
No related guides yet.
Frequently Asked Questions
Q: How does quick answer (tl;dr) apply in practice?
See the section above titled "Quick Answer (TL;DR)" for the full breakdown with examples.
Q: How does this happens apply in practice?
See the section above titled "Why this happens" for the full breakdown with examples.
Q: How does fix #1: most common apply in practice?
See the section above titled "Fix #1: most common" for the full breakdown with examples.
Q: How does fix #2: alternative apply in practice?
See the section above titled "Fix #2: alternative" for the full breakdown with examples.
Drop a comment if you've audited a similar spike. What was the dominant cause for your team? Share what worked or what blew up.













