CoursesAWS security engineeringGuardDuty threat detection

GuardDuty threat detection

Agentless findings from CloudTrail, flow, and DNS.

Advanced30 min · lesson 4 of 15

A good security guard doesn't stop every visitor at the front door. They sit in a back room watching a wall of camera feeds, and the second someone starts jiggling the handle on a side door, they radio it in. Amazon GuardDuty is that guard for your AWS account. It's a managed threat-detection service, which just means AWS runs the analysis for you and you switch it on. GuardDuty reads the activity logs your account already produces and raises a 'finding' whenever something looks like an attack. Your effort goes into responding, not into building the detection from scratch.

What it actually reads

GuardDuty watches three log sources, and you install nothing on your servers to feed it. The word for that is 'agentless': no agent, no extra software sitting on the machine. The first source is CloudTrail, AWS's own record of every API call in the account (an API call is just a command sent to AWS, like 'create this server' or 'read this file'), including who made it and from where. The second is VPC flow logs. A VPC, or Virtual Private Cloud, is your own private network inside AWS, and its flow logs are a running list of connections, roughly 'machine A talked to machine B on port 22'. The third is DNS query logs, the domain names your instances look up (DNS, the Domain Name System, is the phone book that turns a name like example.com into a numeric address). GuardDuty checks all of it against two things: a threat-intelligence feed of known-bad addresses and domains, which is a watchlist of known burglars, and a behavioral baseline of what's normal for you. Credentials suddenly used from a country you've never operated in, or a server quietly calling a known crypto-mining pool, turn into a finding.

one detector per account, per region
$ aws guardduty create-detector \
--enable \
--finding-publishing-frequency FIFTEEN_MINUTES
{
"DetectorId": "12abc34d567e8f9a0b1c2d3e4f5a6b7c"
}
$ aws guardduty get-detector --detector-id 12abc34d567e8f9a0b1c2d3e4f5a6b7c
{
"CreatedAt": "2026-07-16T08:55:03.000Z",
"Status": "ENABLED",
"ServiceRole": "arn:aws:iam::111122223333:role/aws-service-role/guardduty.amazonaws.com/AWSServiceRoleForAmazonGuardDuty",
"FindingPublishingFrequency": "FIFTEEN_MINUTES",
"Features": [
{ "Name": "CLOUD_TRAIL", "Status": "ENABLED" },
{ "Name": "DNS_LOGS", "Status": "ENABLED" },
{ "Name": "FLOW_LOGS", "Status": "ENABLED" },
{ "Name": "S3_DATA_EVENTS", "Status": "DISABLED" },
{ "Name": "RUNTIME_MONITORING", "Status": "DISABLED" }
]
}

That one detector covers one account in one region, and nothing else. In an organization with dozens of accounts you don't want to click through each one by hand, so you delegate GuardDuty administration to a single security account and set new members to auto-enable. Every account that exists today, and every one someone spins up next year, gets covered without anyone having to remember. Coverage becomes a property of the organization instead of a line on a checklist that people quietly forget.

Fire-drill it before a real attack

You don't want the first real test of your alerting to be an actual breach. GuardDuty can generate sample findings on demand, one fake but fully-formed finding of whatever type you name, so you can watch it travel through your routing and land on your pager, proving the whole chain works end to end. It's a fire drill. The alarm is real, the fire isn't.

generate a sample, then find its id
$ aws guardduty create-sample-findings \
--detector-id 12abc34d567e8f9a0b1c2d3e4f5a6b7c \
--finding-types "UnauthorizedAccess:EC2/SSHBruteForce"
$ aws guardduty list-findings \
--detector-id 12abc34d567e8f9a0b1c2d3e4f5a6b7c \
--finding-criteria '{"Criterion":{"service.additionalInfo.sample":{"Equals":["true"]}}}'
{
"FindingIds": [
"a4b8c2d1e5f6a7b8c9d0e1f2a3b4c5d6"
]
}

Pull the finding back with get-findings and you get the object your automation will actually parse. Every field earns its place. Type is the machine-readable category, written as ThreatPurpose:ResourceType/ThreatName. Severity is a number from 1.0 to 10.0 that AWS sorts into four buckets, Low, Medium, High, and Critical, and it's the number your routing rules key off. SSH brute force, where an attacker hammers a server's remote login (SSH, the standard way to reach a server's command line) with password guesses, lands at a Low 2, a rattled doorknob rather than a five-alarm fire. The Service block is the evidence. service.action tells you what kind of activity triggered the finding, here an inbound network connection, and the detail nested under it carries the attacker's address, its country and network owner, the local port, and how many times it happened. EventFirstSeen and EventLastSeen mark the start and end of the activity. A real finding and a sample one share this exact schema, which is the whole reason the drill is worth running.

a real GuardDuty finding, field by field
$ aws guardduty get-findings \
--detector-id 12abc34d567e8f9a0b1c2d3e4f5a6b7c \
--finding-ids a4b8c2d1e5f6a7b8c9d0e1f2a3b4c5d6
{
"Findings": [
{
"SchemaVersion": "2.0",
"AccountId": "111122223333",
"Region": "eu-west-1",
"Type": "UnauthorizedAccess:EC2/SSHBruteForce",
"Severity": 2,
"Title": "[SAMPLE] 198.51.100.7 is performing SSH brute force attacks against i-0abcd1234efgh5678.",
"Description": "198.51.100.7 is performing SSH brute force attacks against i-0abcd1234efgh5678. Brute force attacks are used to gain unauthorized access to your instance.",
"Resource": {
"ResourceType": "Instance",
"InstanceDetails": { "InstanceId": "i-0abcd1234efgh5678", "AvailabilityZone": "eu-west-1a" }
},
"Service": {
"Action": {
"ActionType": "NETWORK_CONNECTION",
"NetworkConnectionAction": {
"ConnectionDirection": "INBOUND",
"Protocol": "TCP",
"LocalPortDetails": { "Port": 22, "PortName": "SSH" },
"RemoteIpDetails": {
"IpAddressV4": "198.51.100.7",
"Country": { "CountryName": "Russia" },
"Organization": { "Asn": "12345", "AsnOrg": "ExampleHostingLtd" }
}
}
},
"Count": 27,
"EventFirstSeen": "2026-07-16T09:02:11Z",
"EventLastSeen": "2026-07-16T09:41:52Z"
}
}
]
}

Protection plans, and the bill

The three foundational sources are on by default and cheap, priced on how many CloudTrail events you generate and how many gigabytes of flow and DNS logs. Everything past that is a protection plan you opt into, and each is billed separately by volume. S3 Protection watches access to your files in S3, AWS's object storage. Runtime Monitoring puts a lightweight agent inside your running workloads (EKS and ECS are AWS's Kubernetes and container services, EC2 is a plain virtual server) so it can see process and network behavior the logs never capture, billed per vCPU-hour. Malware Protection scans the disk of a suspect server and charges per gigabyte scanned. RDS Protection watches logins to your managed databases, and Lambda Protection watches the network activity of your small on-demand functions. Each one earns its keep in the right place. Switched on blindly across a large organization, they can multiply your GuardDuty bill several times over, so enable them where the workload justifies it and watch the first month's cost.

A detector guards one region, and plans bill by volume
GuardDuty protects the single region its detector lives in. Enable it in eu-west-1 and an attacker who spins up resources in us-east-1 or ap-southeast-2 runs completely unseen, so turn it on in every region you could ever touch, including the ones you never use on purpose. And before you switch protection plans on org-wide, price them first: Runtime Monitoring bills per vCPU-hour and Malware Protection per gigabyte scanned, which on a big fleet is a real line item, not a rounding error.

Left alone, GuardDuty also surfaces things that are real but expected, like a vulnerability scanner you run on purpose or a backup job that reads data in bulk. Those aren't false alarms. They're true findings you've already decided you don't care about. The wrong fix is disabling the finding type, because that blinds you to the genuine article too. The right fix is a suppression rule: a filter that auto-archives findings matching a tight set of criteria, so they're still recorded but kept out of your responders' queue.

suppress the known-benign without going blind
$ aws guardduty create-filter \
--detector-id 12abc34d567e8f9a0b1c2d3e4f5a6b7c \
--name suppress-sample-ssh-bruteforce \
--action ARCHIVE \
--rank 1 \
--finding-criteria '{"Criterion":{"type":{"Equals":["UnauthorizedAccess:EC2/SSHBruteForce"]},"service.additionalInfo.sample":{"Equals":["true"]}}}'
{
"Name": "suppress-sample-ssh-bruteforce"
}
How a GuardDuty finding is born and where it goes
reads (no agent)
CloudTrail
every API call: who did what
VPC flow logs
which machine talked to which
DNS query logs
domains your hosts look up
detector engine
threat intel + baseline
known-bad and not-normal
finding
type, severity, evidence
protection plans (extra cost)
Runtime Monitoring
agent: EKS/ECS/EC2 behavior
Malware / S3 / RDS / Lambda
targeted, volume-billed add-ons
where it goes
EventBridge
match on severity, fan out
Security Hub / SOAR
correlate, auto-contain
Base sources are agentless and cheap; plans add depth for a price. A finding only matters once EventBridge routes it somewhere that acts, which is the bridge into Security Hub next.
Quick check
01A weekly vulnerability scan you run on purpose keeps firing Recon findings that page on-call at 2am. You want them on the record but out of the queue, without going blind to a genuine recon attack. What do you do?
Correct — The finding is still generated and stored, but auto-archived so it never reaches responders, and recon from any other source still fires normally.
Incorrect — This silences the real thing too. A genuine attacker doing reconnaissance would now produce no finding at all, which is exactly the gap you're trying to avoid.
Incorrect — That removes a whole category of detection for the entire account just to mute one benign source. Enormous blast radius for a small annoyance.
Incorrect — That only changes how often updated findings are exported to EventBridge, not which findings exist or whether they page you.
02GuardDuty is described as agentless. Which three log sources does a foundational GuardDuty detector analyze?
Correct — the three foundational, agentless sources are CloudTrail (who called what), VPC flow logs (which machine talked to which), and DNS query logs (the domains hosts look up).
Incorrect — Config snapshots and CloudWatch metrics are not GuardDuty inputs; its foundational trio is CloudTrail, flow logs, and DNS logs.
Incorrect — GuardDuty installs nothing on your servers for foundational detection; an in-workload agent belongs to the opt-in Runtime Monitoring plan.
Incorrect — S3, RDS, and Lambda are covered by separately billed protection plans, not by the three foundational log sources.
03Your team enabled GuardDuty in eu-west-1, where all production runs. An attacker uses stolen credentials to spin up crypto-mining instances in us-east-1, a region you never deploy to. Does GuardDuty catch it?
Incorrect — GuardDuty is regional, not global; each region needs its own enabled detector.
Incorrect — even though some events are global, a detector evaluates only its own region, so an un-enabled us-east-1 stays blind.
Correct — GuardDuty protects only the single region its detector lives in, so you must enable it in every region, including ones you never use on purpose, or attacker activity there runs completely unseen.
Incorrect — the gap is the missing detector in us-east-1, not a protection-plan or billing setting; foundational detection there simply is not running.

Try this

Work through “Protection plans, and the bill” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.

Takeaway

The trap worth remembering here: a detector guards one region, and plans bill by volume. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.

Related