Amazon GuardDuty is AWS's managed threat detection service. You turn it on in a region and it starts analysing your account's activity for signs of compromise: credentials used from unexpected places, instances talking to known malicious hosts, cryptomining, malware, and multi-step attacks. There are no agents to install for the core detections, and no logs to collect.
Turning it on is the easy part. Teams usually struggle with what comes next: findings that nobody reads, alerts that page for port scans, suppression rules that hide real attacks, and accounts or regions that were never enabled. This article explains what GuardDuty reads, what a finding is, how to roll it out across an organisation, and how to build a response pipeline that people trust. Details come from the GuardDuty user guide as read on 2026-10-03.
How GuardDuty fits together
What GuardDuty reads
GuardDuty does not read your CloudTrail trail or your flow logs. When you enable it, it consumes independent copies of three foundational sources for the account: CloudTrail management events, VPC flow logs from EC2 instances, and DNS query logs. You do not need to create a trail or turn on flow logs, and disabling yours does not blind GuardDuty. It also does not give you access to those copies, so they are not a replacement for your own logging. One gap is worth remembering: DNS findings depend on instances using the AWS-provided resolver. Traffic sent to your own resolvers is not visible as DNS logs.
On top of the foundation sit optional protection plans, each reading another source. The current list is shown below. When you enable GuardDuty for the first time, every plan except Runtime Monitoring is turned on automatically and covered by a 30-day free trial. Plans launched after you enabled GuardDuty are not switched on automatically, so check the list periodically.
| Protection plan | Reads | Catches, for example |
|---|---|---|
| S3 Protection | CloudTrail S3 data events | Unusual bulk reads or deletes, access from malicious IPs |
| EKS Protection | EKS audit logs | Anonymous access, privileged pods, suspicious RBAC changes |
| Runtime Monitoring | OS events from an agent on EKS, EC2, ECS and Fargate | Reverse shells, miners, suspicious processes |
| Malware Protection for EC2 | Scans of attached EBS volumes | Malware on instances and container hosts |
| Malware Protection for S3 | Newly uploaded objects | Malicious uploads (usable without GuardDuty) |
| Malware Protection for AWS Backup | EBS snapshots, AMIs, recovery points | Infected backups before restore |
| RDS Protection | Aurora and RDS login activity | Anomalous or brute-force logins |
| Lambda Protection | Lambda network activity | Functions mining or calling bad hosts |
| AI Protection | CloudTrail data events for Bedrock, AgentCore, SageMaker AI | Anomalous model invocations, cost harvesting |
GuardDuty matches all of this against AWS threat intelligence (known bad IPs, domains and file hashes), machine-learning baselines of normal behaviour for your account, and lists you provide. The service is regional, so a detector in us-east-1 sees nothing that happens in eu-west-1.
Anatomy of a finding
Each detection becomes a finding: a JSON document with a type, a severity, the affected resource and the evidence. Finding types follow the pattern ThreatPurpose:ResourceType/ThreatFamily.DetectionMechanism!Artifact. So UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS means that credentials issued to an EC2 instance role were used from outside AWS, and CryptoCurrency:EC2/BitcoinTool.B!DNS means that an instance resolved a mining-pool domain.
Severity is a number from 1.0 to 10.0, grouped into four bands. Low (1.0 to 3.9) is attempted activity that did not succeed, such as a port probe. Medium (4.0 to 6.9) is behaviour that deviates from the norm and may indicate compromise. High (7.0 to 8.9) means a resource is probably compromised and being used. Critical (9.0 to 10.0) is reserved for attack sequences. When the same activity repeats, GuardDuty does not create a new finding each time. It increments service.count on the existing finding and updates its last-seen time. GuardDuty keeps findings for 90 days, so export them if you need longer history.
Extended Threat Detection
Single findings answer 'is this one event bad?'. Real intrusions are chains of events that are each harmless on their own: a role lists buckets, then a bucket policy becomes public, then data leaves. Extended Threat Detection, on by default at no extra cost, correlates API activity and existing findings (which GuardDuty calls signals) within a 24-hour rolling window, and raises one Critical attack-sequence finding such as AttackSequence:IAM/CompromisedCredentials, AttackSequence:S3/CompromisedData or AttackSequence:EKS/CompromisedCluster.
It can only correlate signals it receives. S3 attack sequences need S3 Protection, EKS sequences need EKS Protection or Runtime Monitoring, and ECS and EC2 instance-group sequences benefit from Runtime Monitoring. Turning plans off to save money therefore also removes multi-step detections, not just the individual findings. GuardDuty also offers newer AI-assisted investigation (in preview) and curated custom detection rules over CloudTrail management events; evaluate those separately, as their scope is still changing.
Rolling out across an organisation
In an organisation, use a delegated administrator: a dedicated security account that manages GuardDuty for every member and sees all their findings. Because GuardDuty is regional, every step must be repeated in every region you use, and also in regions you do not use, since an attacker with stolen keys will happily run miners where nobody looks. Service control policies that deny unused regions shrink that surface, but they do not remove the need to enable GuardDuty everywhere.
# From the management account, once per region:
for r in $(aws ec2 describe-regions --query 'Regions[].RegionName' --output text); do
aws guardduty enable-organization-admin-account --region "$r" \
--admin-account-id 222233334444
done
# From the delegated admin account, again once per region:
for r in $(aws ec2 describe-regions --query 'Regions[].RegionName' --output text); do
D=$(aws guardduty list-detectors --region "$r" --query 'DetectorIds[0]' --output text)
aws guardduty update-organization-configuration --region "$r" \
--detector-id "$D" --auto-enable-organization-members ALL
aws guardduty update-detector --region "$r" --detector-id "$D" \
--finding-publishing-frequency FIFTEEN_MINUTES
aws guardduty create-publishing-destination --region "$r" --detector-id "$D" \
--destination-type S3 \
--destination-properties DestinationArn=arn:aws:s3:::org-guardduty-findings,KmsKeyArn=$KMS_KEY_ARN
doneUsing ALL enrols existing members as well as new ones. The publishing frequency controls how often repeat occurrences of an existing finding are sent to EventBridge and S3. The options are 15 minutes, 1 hour or the default of 6 hours, and only the administrator can set it. New findings are always sent in near real time. The S3 export requires a KMS key whose policy allows GuardDuty to use it, and that export is your long-term record once the 90 days have passed.
Routing findings
Every finding is published to EventBridge in the account that owns it, and also in the administrator account, with source aws.guardduty and detail-type GuardDuty Finding. Route events by severity: everything to the SIEM, Medium and above to a ticket queue, High and above to a page or an automated containment step. EventBridge numeric matching keeps the rule short:
{
"source": ["aws.guardduty"],
"detail-type": ["GuardDuty Finding"],
"detail": { "severity": [{ "numeric": [">=", 7] }] }
}
Worked example: stolen instance credentials
Consider UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS. An attacker has used a server-side request forgery bug to read an instance role's temporary credentials from the metadata service and is now calling AWS APIs from their own machine. The credentials stay valid until they expire, so waiting for a human costs real time. A Lambda in the security account can revoke every session issued before now by attaching a deny policy conditioned on token issue time, the same technique as the IAM console's 'revoke active sessions' action:
import datetime, json, boto3
TARGET = "UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS"
def handler(event, _ctx):
f = event["detail"]
if f["type"] != TARGET or f["service"].get("archived"):
return
if f["service"].get("additionalInfo", {}).get("sample"):
print("sample finding; routing verified, no action")
return
account = f["accountId"]
role = f["resource"]["accessKeyDetails"]["userName"]
now = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
# Assume a pre-provisioned responder role in the member account.
creds = boto3.client("sts").assume_role(
RoleArn=f"arn:aws:iam::{account}:role/GuardDutyResponder",
RoleSessionName=f"gd-{f['id'][:20]}")["Credentials"]
iam = boto3.client("iam",
aws_access_key_id=creds["AccessKeyId"],
aws_secret_access_key=creds["SecretAccessKey"],
aws_session_token=creds["SessionToken"])
iam.put_role_policy(RoleName=role, PolicyName="AWSRevokeOlderSessions",
PolicyDocument=json.dumps({"Version": "2012-10-17", "Statement": [{
"Effect": "Deny", "Action": "*", "Resource": "*",
"Condition": {"DateLessThan": {"aws:TokenIssueTime": now}}}]}))
print(json.dumps({"finding": f["id"], "role": role, "revoked_before": now}))This stops the stolen credentials, but it does not fix the cause. The instance fetches fresh credentials issued after the cutoff, so the application keeps working, and so would a second theft through the same bug. The human follow-up is to require IMDSv2 with a hop limit of 1, patch the request-forgery bug, review what the role did using CloudTrail, and narrow the role's permissions following IAM least privilege. For an EC2 finding such as a miner, the usual automated step is to swap the instance's security groups for an empty isolation group and take an EBS snapshot for forensics. Note that existing tracked connections may survive a security group change, so a network ACL deny is the stronger cut.
Tuning without going blind
Three controls shape what reaches people. Each one has a sharp edge.
- Suppression rules are filters with action
ARCHIVE. Matching findings are archived automatically, are not sent to EventBridge, and are not used by Extended Threat Detection. Make every rule as narrow as possible: one finding type plus a tag or resource identifier, never a whole severity band. - Trusted IP lists stop findings for traffic from listed addresses. Treat them like firewall rules, with an owner and an expiry, because a trusted range that is later reassigned hides an attacker completely.
- Threat lists add your own indicators, such as IPs from an incident or a sharing community, so GuardDuty raises findings when they appear.
# Archive port-probe findings only for the tagged vulnerability scanner fleet.
aws guardduty create-filter --detector-id "$D" --name scanner-probes \
--action ARCHIVE --rank 1 --finding-criteria '{
"Criterion": {
"type": {"Eq": ["Recon:EC2/PortProbeUnprotectedPort"]},
"resource.instanceDetails.tags.value": {"Eq": ["vuln-scanner"]}
}}'
# Prove routing end to end; the handler logs and skips samples.
aws guardduty create-sample-findings --detector-id "$D" \
--finding-types UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS
Failure modes
| Failure | Effect | Prevention |
|---|---|---|
| Region or account not enabled | Attacks there produce no findings | Organisation auto-enable ALL in every region; audit with AWS Config |
| Broad suppression rule | Real attacks archived, never routed, not correlated | Narrow rules, reviewed quarterly |
| Findings only in the console | Nobody sees them | EventBridge rules to SIEM, tickets and pages |
| No S3 export | History gone after 90 days | KMS-encrypted publishing destination |
| Plans disabled to save cost | Lost individual and attack-sequence detections | Decide per plan with the coverage loss written down |
| Custom DNS resolvers | No DNS-based findings | Know the gap; compensate with resolver logging |
| Auto-remediation on samples | Responders act on placeholder resources | Skip service.additionalInfo.sample in handlers; test real actions in a sandbox account |
Trade-offs
GuardDuty is cheap to start and needs no maintenance, but it is a detector, not a log store or a general security information and event management system. You cannot write arbitrary queries against its inputs, and it detects patterns that AWS has modelled. Pair it with your own CloudTrail and flow logs for investigation, and with posture tools that find misconfiguration before it is exploited. Costs scale with event volume, so busy S3 data-event or runtime workloads deserve a look at the usage page during the trial. Automation is a trade-off of speed against blast radius: automate reversible containment such as session revocation and isolation, and leave termination and deletion to people.
What to do next
- Designate a security account as the GuardDuty delegated administrator and enable auto-enable
ALLin every region. - Review which protection plans are on, and enable any that cover workloads you actually run.
- Create a KMS-encrypted S3 publishing destination and set the repeat frequency to 15 minutes.
- Add EventBridge rules routing by severity: all to the SIEM, Medium and above to tickets, High and above to on-call.
- Write one automated containment for credential exfiltration, test it with sample findings in a sandbox account, and require IMDSv2 across the fleet.
- Audit existing suppression rules, and delete or narrow any that match a whole severity or finding family.
- Run a quarterly drill: generate samples, follow one finding from detection to closure, and time it.