CloudTrail and GuardDuty baseline
The two services to turn on everywhere, today.
A bank vault runs on two systems that never touch the money. One is the ledger: every time a door opens or a drawer slides out, a camera and a logbook record who did it, when, and from where. The other is the guard watching those feeds, trained to spot the face of a known robber or a teller acting strange at 3 a.m. Neither the ledger nor the guard can lock a single door. Without them, though, you learn about the robbery when the vault is already empty.
AWS hands you the same pair. CloudTrail is the ledger: it records nearly every API call (application programming interface call, the request sitting under every console click, CLI (command-line interface) command, and script) in your account, capturing who made it, from which IP address (internet protocol address, the network address a request came from), with which credentials, and what changed. GuardDuty is the guard: a managed detection service that reads those records plus your network and DNS traffic and raises a finding when something looks like an attack. Neither one blocks anything. Turn both on in every region and you have shrunk the gap between a break-in and your knowledge of it from months to minutes. Leave them off and your first alert is the bill.
What CloudTrail Actually Records
Everything you do in AWS is an API call underneath. Launch a server, attach a policy, read a file: each one is a request to a service, and CloudTrail writes it down as a JSON event (JSON is JavaScript Object Notation, a plain-text format of labeled fields). The event carries the caller's identity (the userIdentity block), the source IP address, a timestamp, the request parameters, and the response. When one AWS service calls another on your behalf, that shows up too.
There are two flavors, and the gap between them costs money. Management events are the control-plane actions, the ones that change the shape of your account: RunInstances, CreateUser, PutBucketPolicy, StopLogging. You get these by default. Data events are the object-level reads and writes: pulling a single object from S3 (Simple Storage Service, AWS's object storage) with s3:GetObject, invoking a Lambda function. They are off by default, because one busy bucket can throw off millions of them an hour and CloudTrail charges by volume.
Every AWS account already keeps the last 90 days of management events in something called Event history, free, with zero setup. That sounds like a baseline. It is not. Event history is per-region, it is clumsy to query past simple filters, and it evaporates on day 91. What you want is a trail: a delivery pipeline that copies events into an S3 bucket you own, across every region, kept for as long as your retention policy says.
Standing Up an Organization Trail
Create the trail once, from your organization's management account, as an organization trail so member accounts inherit it and cannot quietly opt out. CloudTrail checks the destination bucket's policy while it builds the trail, so the log-archive bucket and its policy have to exist first. (You can also delegate trail administration to a separate account if you would rather not work in the management account at all.)
aws cloudtrail create-trail \--name org-trail \--s3-bucket-name acme-cloudtrail-logs \--is-multi-region-trail \--is-organization-trail \--enable-log-file-validation \--kms-key-id alias/cloudtrail-logs
{"Name": "org-trail","S3BucketName": "acme-cloudtrail-logs","IncludeGlobalServiceEvents": true,"IsMultiRegionTrail": true,"TrailARN": "arn:aws:cloudtrail:us-east-1:111122223333:trail/org-trail","LogFileValidationEnabled": true,"KmsKeyId": "arn:aws:kms:us-east-1:111122223333:key/1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d","IsOrganizationTrail": true}
The response hands back the trail's settings. The two flags that matter for security are the last two you passed. --enable-log-file-validation tells CloudTrail to write a signed digest file every hour holding a SHA-256 checksum (a cryptographic fingerprint) of each log file it delivered; later, in the middle of an incident, aws cloudtrail validate-logs can prove that nobody edited or deleted a log file since it was written. --kms-key-id encrypts the logs with a customer-managed key from KMS (Key Management Service, AWS's managed encryption), so reading the trail now takes both S3 permissions and KMS permissions. Notice the response echoed the key back as a full ARN (Amazon Resource Name, the unique identifier for any AWS resource) even though you handed it a short alias.
aws cloudtrail start-logging --name org-trail# confirm it is actually recordingaws cloudtrail get-trail-status --name org-trail --query 'IsLogging'
true
Creating a trail does not start it. start-logging flips it on, and get-trail-status confirms it is running. Put that check into your infrastructure tests, because a trail that exists but is not logging is the most dangerous kind: it shows up green on a dashboard and records nothing.
Asking the Trail Questions
The first move in an incident is to ask the ledger a question. Who turned off logging? Who minted that access key at 3 a.m.? For the last 90 days of management events you can query CloudTrail directly with lookup-events, no data warehouse needed.
aws cloudtrail lookup-events \--lookup-attributes AttributeKey=EventName,AttributeValue=StopLogging \--start-time 2026-07-01 \--max-results 5 \--query 'Events[].{time:EventTime,user:Username,event:EventName}'
[{"time": "2026-07-11T03:12:44+00:00","user": "deploy-ci","event": "StopLogging"}]
That output says the deploy-ci identity ran StopLogging on July 11th. Now you have a name, a time, and a thread to pull. lookup-events reaches back only 90 days and only sees management events in the region you call it from, so for anything older, cross-region, or object-level you run real SQL over the logs: Athena (AWS's run-SQL-over-S3 service) reads the trail's bucket directly, or CloudTrail Lake pulls the events into a managed store you query the same way. Reading logs by hand is forensics, though, not detection. Nobody eyeballs a few million events a day. That is the guard's job.
GuardDuty, a Guard You Do Not Have to Wire Up
Most AWS security tools make you build a pipeline before they do anything. GuardDuty is the opposite: you feed it nothing. Switch it on and it starts reading three streams on its own, whether or not you have set them up yourself: CloudTrail management events, VPC Flow Logs (records of network connections inside your virtual private cloud, your private slice of the AWS network), and Route 53 DNS (domain name system) query logs, the domain-name lookups your instances make. It gets its own copy. You do not have to enable Flow Logs, or pay for them, to give GuardDuty its network view. The DNS view only covers instances using the default AWS resolver; run your own and GuardDuty goes blind to those lookups.
It pushes each stream through two engines. One is threat intelligence: maintained lists of known-bad IP addresses (command-and-control servers that malware phones home to, Tor exit nodes, cryptomining pools). The other is anomaly detection trained per account: an access key that suddenly calls APIs from a country it has never touched, an EC2 instance (Elastic Compute Cloud, an AWS virtual server) resolving randomly generated hostnames, a set of credentials that only ever deployed Lambda functions abruptly running ListBuckets to look around. When something trips a wire, GuardDuty emits a finding: a typed, severity-scored JSON document.
Each finding carries a severity score on a scale from 1.0 to 10.0, sorted into four bands: Low (1.0 to 3.9), Medium (4.0 to 6.9), High (7.0 to 8.9), and Critical (9.0 and above). That top band is the newest one, reserved for the multi-step attack sequences that GuardDuty's Extended Threat Detection stitches together out of findings that look minor on their own. A type like UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS almost reads as a sentence once you know the grammar: someone lifted the temporary credentials off an EC2 instance role and is replaying them from outside AWS.
Turning it on is one call:
aws guardduty create-detector \--enable \--finding-publishing-frequency FIFTEEN_MINUTES
{"DetectorId": "e2b1abc3d4e5f6a7b8c9d0e1f2a3b4c5"}
One detector covers exactly one region. In an organization you delegate GuardDuty administration to your security account and switch on auto-enable, so every current and future member account gets a detector without anyone having to remember. Now pull the High-and-above findings, newest first:
DETECTOR=$(aws guardduty list-detectors --query 'DetectorIds[0]' --output text)# High and above (severity >= 7), newest firstaws guardduty list-findings \--detector-id "$DETECTOR" \--finding-criteria '{"Criterion":{"severity":{"GreaterThanOrEqual":7}}}' \--sort-criteria AttributeName=updatedAt,OrderBy=DESC
{"FindingIds": ["9cb2a1f0e3d4c5b6a7f8e9d0c1b2a3f4"]}
A finding ID on its own is thin. get-findings turns it into the story:
aws guardduty get-findings \--detector-id "$DETECTOR" \--finding-ids 9cb2a1f0e3d4c5b6a7f8e9d0c1b2a3f4 \--query 'Findings[0].{type:Type,severity:Severity,region:Region,resource:Resource.ResourceType,action:Service.Action.ActionType}'
{"type": "UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS","severity": 8,"region": "us-east-1","resource": "AccessKey","action": "AWS_API_CALL"}
There it is: instance credentials being replayed from outside AWS, severity 8, in us-east-1. That is the exact scenario the next lesson walks through containing.
Findings Nobody Routes Are Findings Nobody Reads
A finding sitting unread in a console at 3 a.m. is worth nothing. The routing is part of the baseline, not an add-on you get to next quarter. GuardDuty writes every finding into EventBridge (AWS's event bus, a switchboard that matches events against rules and forwards them) on its own. You write one rule that catches the high-severity findings and sends them somewhere that wakes a human, and let everything else drain into a ticket queue you sweep weekly. The rule matches on the finding's shape:
{"source": ["aws.guardduty"],"detail-type": ["GuardDuty Finding"],"detail": {"severity": [ { "numeric": [ ">=", 7 ] } ]}}
That pattern catches any GuardDuty finding scored 7 or higher, which now means High and Critical both. Create the rule from it, then point the rule at a target (an SNS topic (Simple Notification Service, AWS's pub/sub and paging service) that reaches your on-call tool, or a Lambda that opens a ticket) with aws events put-targets.
aws events put-rule \--name guardduty-high-severity \--event-pattern file://guardduty-high.json \--query 'RuleArn'
"arn:aws:events:us-east-1:111122223333:rule/guardduty-high-severity"
One number trips people here. --finding-publishing-frequency, which you set to FIFTEEN_MINUTES earlier, does not control how fast a new finding reaches you. A brand-new finding lands in EventBridge in near real-time, and you cannot slow that down. The frequency only governs how often GuardDuty re-exports later occurrences of a finding it has already reported, so a chatty repeat does not page you every few seconds. FIFTEEN_MINUTES is the fastest of the three settings (the others are ONE_HOUR and SIX_HOURS); there is no five-minute option to reach for. Detection itself still takes minutes, so set your paging expectations around minutes, not seconds.
What It Costs, and What It Will Not Catch
The trade-offs are honest ones. CloudTrail's first copy of management events into a trail is free; extra copies cost about $2.00 per 100,000 events, and data events run about $0.10 per 100,000. Flip data events on for a busy bucket and they can dwarf the rest of your bill, so scope them with advanced event selectors down to the handful of buckets holding secrets or PII (personally identifiable information, data that points back to a real person), never to every bucket at once. GuardDuty prices on the volume it analyzes (CloudTrail events plus Flow Logs plus DNS), so a chatty VPC costs more; the 30-day free trial in each account shows your real monthly rate before you commit.
Know the ceiling too. GuardDuty is not a SIEM (security information and event management, the big log-correlation platforms like Splunk). You cannot write custom detection rules, it will not weave findings into a single storyline the way a human analyst would (those attack-sequence findings are a narrow exception), and its latency is measured in minutes. It will miss a trusted insider with real credentials doing quietly harmful things that look statistically normal. And to say it plainly one more time, because this is the mistake people make: neither service prevents anything. They watch. Prevention lives in IAM (identity and access management) policies and SCPs. This pair is your evidence locker and your tripwire.
When the tripwire fires, a clock starts. The next lesson takes the finding you pulled here, InstanceCredentialExfiltration.OutsideAWS, and walks the containment path under time pressure: scoping the blast radius with lookup-events, killing the live sessions, and rotating what leaked, in that order, because doing them out of sequence either tips off the attacker or locks out your own responders.