Wer hat die EC2-Instanz gelöscht? Mit CloudTrail den Verursacher finden

Eine EC2-Instanz ist verschwunden — kein Alarm, kein Ticket, kein Kontext. Genau in diesem Moment wird CloudTrail vom netten Logging-Feature zur einzigen Quelle der Wahrheit. Wer den TerminateInstances-Aufruf abgesetzt hat, steht im Event-Verlauf — wenn man weiß, wo man suchen muss.

TL;DR: CloudTrail-Ereignisverlauf für gelöschte EC2-Instanzen

SchrittAktionZiel
1CloudTrail Event History öffnenEreignis eingrenzen
2Nach 'TerminateInstances' filternRelevantes API-Event finden
3Event-Detail prüfenIAM-Identität und Zeitstempel lesen
4CLI-Abfrage für vollständige DetailsMaschinenlesbare Auswertung
5CloudTrail Lake oder S3-Logs (optional)Langzeitanalyse über 90 Tage hinaus

Wie CloudTrail 'TerminateInstances' aufzeichnet

CloudTrail erfasst Management Events — also Kontrolleben-API-Aufrufe — standardmäßig für alle Regionen, sofern ein Trail oder der Event-Verlauf aktiv ist. Der Event-Verlauf in der Konsole speichert die letzten 90 Tage ohne zusätzliche Konfiguration. Jeder Eintrag enthält die aufrufende Identität (userIdentity), den Zeitstempel, die Quell-IP, den API-Aktionsnamen und die betroffenen Ressourcen.

Beim Beenden einer Instanz ruft der Verursacher — ob Mensch, Rolle oder Service — die EC2-API-Aktion TerminateInstances auf. CloudTrail schreibt dieses Ereignis mit dem Ereignisnamen TerminateInstances und der Quelle ec2.amazonaws.com. Die Instanz-ID taucht im Feld requestParameters auf, die Identität im Feld userIdentity.

graph LR Prinzipal["IAM-Prinzipal
(Benutzer / Rolle / Service)"] --> EC2API["EC2 API
TerminateInstances"] EC2API --> Instanz["EC2-Instanz
wird beendet"] EC2API --> CT["CloudTrail
Management Event"] CT --> EV["Event-Verlauf
(90 Tage)"] CT --> S3["S3 Trail-Bucket
(optional, konfigurierbar)"] CT --> Lake["CloudTrail Lake
(optional, konfigurierbar)"] Operator["Operator"] --> EV Operator --> S3 Operator --> Lake
  1. IAM-Prinzipal (Benutzer, Rolle, Service) ruft TerminateInstances über CLI, Konsole oder SDK auf.
  2. EC2 API Endpoint verarbeitet den Aufruf und beendet die Instanz.
  3. CloudTrail empfängt das Management Event und schreibt es in den Event-Verlauf (90 Tage) sowie optional in den konfigurierten S3-Trail.
  4. Der Operator fragt den Event-Verlauf über Konsole oder CLI ab und liest die userIdentity-Felder.

Schritt 1: Event-Verlauf in der AWS-Konsole öffnen und filtern

Der schnellste Weg bei einem frischen Vorfall: CloudTrail-Konsole → Event-Verlauf → Filter auf Ereignisname setzen → TerminateInstances eingeben. Die Ergebnisliste zeigt alle Terminierungsereignisse der letzten 90 Tage in der aktuellen Region.

Wichtig: Der Event-Verlauf ist regionsspezifisch. Wenn die Instanz in eu-central-1 lief, muss die Konsole auch auf diese Region eingestellt sein. Wer in der falschen Region sucht, findet nichts — das ist einer der häufigsten Fehler bei dieser Art von Untersuchung.

Zusätzlich kann nach der Instanz-ID gefiltert werden, wenn sie bekannt ist: Filter Ressourcenname → Instanz-ID eintragen. Das schränkt die Ergebnisse auf genau diese Ressource ein.

Schritt 2: TerminateInstances-Ereignis per AWS CLI abfragen

Die CLI-Abfrage ist präziser und skriptfähig. Sie liefert das vollständige JSON-Ereignis inklusive aller Identitätsfelder — besonders nützlich, wenn mehrere Instanzen betroffen sind oder die Konsole zu viele Treffer zurückgibt.

aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=TerminateInstances \
  --region eu-central-1 \
  --query 'Events[*].{Zeit:EventTime,Benutzer:Username,Ereignis:EventName,ID:EventId}' \
  --output table

Um gezielt nach einer bestimmten Instanz-ID zu suchen, kann der Filter auf die Ressourcen-ID gesetzt werden:

aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=ResourceName,AttributeValue=i-0abc1234def567890 \
  --region eu-central-1 \
  --output json

Das zurückgegebene JSON enthält im Feld CloudTrailEvent das vollständige Ereignis als escaped String. Dieses muss für die Auswertung geparst werden:

🔽 Vollständiges Ereignis-JSON parsen (Beispielausgabe)
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=ResourceName,AttributeValue=i-0abc1234def567890 \
  --region eu-central-1 \
  --query 'Events[0].CloudTrailEvent' \
  --output text | python3 -m json.tool

Schritt 3: Die userIdentity-Felder lesen — wer steckt dahinter?

Das userIdentity-Objekt im CloudTrail-Ereignis ist die eigentliche Antwort auf die Frage. Es hat unterschiedliche Strukturen je nach Aufrufertyp:

Aufrufertyptype-WertRelevante Felder
IAM-BenutzerIAMUseruserName, arn
Angenommene Rolle (AssumeRole)AssumedRolearn (enthält Rollenname + Session), sessionContext.sessionIssuer
AWS-Service (z.B. Auto Scaling)AWSServiceinvokedBy
Root-BenutzerRootarn

Bei angenommenen Rollen ist sessionContext.sessionIssuer.arn die Rolle selbst, während userIdentity.arn die Session-ARN mit dem angenommenen Rollennamen und der Session-ID enthält. Wenn ein CI/CD-System oder eine Lambda-Funktion die Instanz beendet hat, steht hier die Ausführungsrolle — nicht der menschliche Benutzer dahinter.

Eine angenommene Rolle ist wie ein Leihausweis: Der Ausweis gehört der Rolle, aber wer ihn benutzt hat, steht in der Session-ID. Beide Felder zusammen ergeben das vollständige Bild.

Wenn invokedBy auf autoscaling.amazonaws.com zeigt, hat Auto Scaling die Instanz im Rahmen eines Scale-In-Vorgangs beendet — kein menschlicher Fehler, sondern konfiguriertes Verhalten.

graph TD UID["userIdentity.type"] --> IAMUser["IAMUser
userName direkt lesbar"] UID --> AssumedRole["AssumedRole
sessionContext prüfen"] UID --> AWSService["AWSService
invokedBy: z.B. autoscaling"] UID --> Root["Root
Root-Account-Benutzer"] AssumedRole --> Session["sessionContext.sessionIssuer.arn
= ursprüngliche Rolle"] AssumedRole --> SessionARN["userIdentity.arn
= Rolle + Session-ID"]
  1. IAMUser: Direkter IAM-Benutzer — userName ist sofort lesbar.
  2. AssumedRole: Eine Rolle wurde per sts:AssumeRole übernommen. Die Session-ARN enthält Rollenname und Session-Bezeichner. sessionContext.sessionIssuer zeigt die ursprüngliche Rolle.
  3. AWSService: Ein AWS-Service hat im eigenen Namen gehandelt. invokedBy nennt den Service.
  4. Root: Der Root-Account-Benutzer hat direkt gehandelt — ein Alarmsignal in Produktionsumgebungen.

Schritt 4: Vollständige Identität aus dem Ereignis extrahieren

Für eine saubere Auswertung empfiehlt sich eine direkte JMESPath-Abfrage auf die relevanten Felder. Das vermeidet manuelles Parsen des escaped JSON-Strings:

aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=ResourceName,AttributeValue=i-0abc1234def567890 \
  --region eu-central-1 \
  --query 'Events[*].{Zeit:EventTime,Benutzer:Username,EventId:EventId}' \
  --output json

Das Feld Username im CloudTrail-Lookup-Ergebnis entspricht dem userIdentity.userName bei IAM-Benutzern oder dem Session-Namen bei angenommenen Rollen. Für das vollständige userIdentity-Objekt muss das CloudTrailEvent-Feld geparst werden.

🔽 Bash-Snippet: userIdentity aus CloudTrailEvent extrahieren
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=ResourceName,AttributeValue=i-0abc1234def567890 \
  --region eu-central-1 \
  --output json \
  | python3 -c "
import json, sys
events = json.load(sys.stdin)['Events']
for e in events:
    detail = json.loads(e['CloudTrailEvent'])
    uid = detail.get('userIdentity', {})
    print('Typ:', uid.get('type'))
    print('ARN:', uid.get('arn'))
    print('Benutzer:', uid.get('userName', 'n/a'))
    print('Zeit:', detail.get('eventTime'))
    print('Quell-IP:', detail.get('sourceIPAddress'))
    print('---')
"

Erfahrung aus der Praxis: Die falsche Diagnose und was wirklich dahintersteckte

Ein Produktionsteam meldet: Instanz i-0abc1234def567890 ist weg. Kein Deployment geplant, kein Incident-Ticket. Erster Verdacht: ein Entwickler hat versehentlich auf 'Terminate' geklickt.

CloudTrail zeigt TerminateInstances — aufgerufen von autoscaling.amazonaws.com. Typ: AWSService. Kein Mensch, kein Fehler. Die Auto-Scaling-Gruppe hatte eine Health-Check-Richtlinie, die die Instanz nach einem fehlgeschlagenen ELB-Health-Check als ungesund markiert und ersetzt hat. Die eigentliche Ursache war ein Fehler im Health-Check-Pfad der Anwendung — nicht ein menschlicher Eingriff.

Ohne den invokedBy-Wert im CloudTrail-Ereignis wäre das Team stundenlang in die falsche Richtung ermittelt. Die Instanz-ID allein sagt nichts — der Aufrufertyp ist der entscheidende Filter.

Schritt 5: Suche über 90 Tage hinaus — CloudTrail S3-Logs und CloudTrail Lake

Der Event-Verlauf in der Konsole und lookup-events in der CLI sind auf 90 Tage begrenzt. Wenn der Vorfall älter ist oder eine vollständige Audit-Kette benötigt wird, gibt es zwei Wege:

S3-Trail-Logs direkt abfragen: Wenn ein Trail konfiguriert ist, der Logs in S3 schreibt, liegen die Ereignisse als gzip-komprimierte JSON-Dateien in der Struktur s3://bucket/AWSLogs/account-id/CloudTrail/region/YYYY/MM/DD/. Diese können mit Amazon Athena über eine externe Tabelle abgefragt werden.

CloudTrail Lake: CloudTrail Lake ist ein verwalteter Datensee für CloudTrail-Ereignisse, der SQL-basierte Abfragen direkt auf Ereignisdaten ermöglicht. Aufbewahrungszeiträume sind konfigurierbar. Für Organisationen mit strengen Compliance-Anforderungen ist das der strukturiertere Ansatz.

aws cloudtrail get-event-data-store \
  --event-data-store arn:aws:cloudtrail:eu-central-1:123456789012:eventdatastore/EXAMPLE-ID \
  --region eu-central-1

CLI-Beispiele für CloudTrail Lake-Abfragen werden hier nicht vollständig gezeigt, da die Event Data Store ARN und Query-Syntax von der jeweiligen Konfiguration abhängen. Die offizielle AWS-Dokumentation zu CloudTrail Lake enthält vollständige SQL-Beispiele.

Erforderliche IAM-Berechtigungen für die Untersuchung

Wer lookup-events ausführt, benötigt mindestens die folgende Berechtigung. Read-Aktionen wie cloudtrail:LookupEvents erfordern "Resource": "*", da keine ressourcenspezifische ARN-Einschränkung für diese Aktion unterstützt wird — dies ist im Service Authorization Reference dokumentiert.

🔽 Minimale IAM-Richtlinie für CloudTrail-Untersuchung
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "CloudTrailLookup",
      "Effect": "Allow",
      "Action": [
        "cloudtrail:LookupEvents",
        "cloudtrail:GetEventSelectors",
        "cloudtrail:DescribeTrails"
      ],
      "Resource": "*"
    }
  ]
}

Für den Zugriff auf S3-Trail-Logs wird zusätzlich s3:GetObject auf den Trail-Bucket benötigt. Für CloudTrail Lake-Abfragen kommen cloudtrail:StartQuery und cloudtrail:GetQueryResults hinzu.

Wer hat die EC2-Instanz gelöscht? — Zusammenfassung und nächste Schritte

CloudTrail Event History beantwortet die Frage nach dem Verursacher zuverlässig — sofern der Vorfall innerhalb der letzten 90 Tage liegt und in der richtigen Region gesucht wird. Der kritische Punkt ist nicht das Finden des Ereignisses, sondern das korrekte Lesen des userIdentity-Objekts: Typ, ARN und Session-Kontext zusammen ergeben die vollständige Identität.

Für Umgebungen, in denen solche Vorfälle häufiger auftreten oder proaktiv erkannt werden sollen, empfiehlt sich die Kombination aus CloudTrail mit Amazon EventBridge: Eine Regel auf TerminateInstances-Ereignisse kann in Echtzeit eine Benachrichtigung oder eine automatisierte Reaktion auslösen.

Glossar

BegriffBedeutung
Management EventCloudTrail-Ereignistyp für Kontrolleben-API-Aufrufe (z.B. Erstellen, Löschen, Konfigurieren von Ressourcen). Standardmäßig erfasst.
userIdentityJSON-Objekt im CloudTrail-Ereignis, das die aufrufende IAM-Identität beschreibt — Typ, ARN, Session-Kontext.
AssumedRoleuserIdentity-Typ, der anzeigt, dass eine IAM-Rolle per sts:AssumeRole übernommen wurde. Session-Details stehen in sessionContext.
Event-Verlauf (Event History)Kostenloser, 90-tägiger Ereignisspeicher in CloudTrail. Regionsspezifisch. Keine Konfiguration erforderlich.
CloudTrail LakeVerwalteter Ereignisdatensee mit SQL-Abfragemöglichkeit. Konfigurierbare Aufbewahrung. Kostenpflichtig.

Related Posts

Kommentare

Beliebte Posts aus diesem Blog

EC2 ohne Internetzugang im eigenen VPC – Internet Gateway und Route Table korrekt einrichten

LSI vs. GSI in DynamoDB: Den richtigen Sekundärindex wählen

ElastiCache Redis einsetzen: Wann ein Caching-Layer deine RDS-Datenbank entlastet