Threat detection on AWS with GuardDuty and EventBridge

Turn on GuardDuty across the org and route high-severity findings to Slack and auto-remediation with EventBridge.

Feb 11, 2025·Updated ·6 min readAdvanced·By SecOpsLog · documentation-verified

GuardDuty is a Regional service that people configure as if it were global. Delegating an administrator account enables the detector in that account in the current Region only; the member accounts follow in that Region only; and an organization that ran the rollout from eu-west-1 has nothing watching us-east-1, the Region the console defaults to and a plausible first stop for a stolen credential. The rollout that works is a loop over Regions, followed by a routing table that decides what pages, what posts, and what is archived without anyone reading it.

One administrator, every member, every Region

guardduty-org.sh (run from the management account, then the security account)
SECURITY=222233334444
REGIONS=$(aws ec2 describe-regions --query "Regions[].RegionName" --output text)
# management account: delegate once per Region
for r in $REGIONS; do
aws guardduty enable-organization-admin-account --region "$r" --admin-account-id "$SECURITY"
done
# security (delegated administrator) account: a detector per Region, members auto-enabled
for r in $REGIONS; do
det=$(aws guardduty list-detectors --region "$r" --query "DetectorIds[0]" --output text)
[ "$det" = None ] && det=$(aws guardduty create-detector --region "$r" --enable \
--finding-publishing-frequency FIFTEEN_MINUTES --query DetectorId --output text)
aws guardduty update-organization-configuration --region "$r" --detector-id "$det" \
--auto-enable-organization-members ALL \
--features '[{"Name":"S3_DATA_EVENTS","AutoEnable":"ALL"},{"Name":"EKS_AUDIT_LOGS","AutoEnable":"ALL"}]'
done

ALL covers existing members and every account created later; NEW covers only the latter and leaves a gap the size of the current estate. The features list is where the protection plans are switched on for the organization (S3 data events, EKS audit logs, Runtime Monitoring, and the others in the API's enumeration), and each one is a cost line, so the list is a decision rather than a default. The check that closes the rollout is list-members in each Region: every account should show as enabled, and an account that shows anything else was created outside the org automation or opted out.

bash — the state a finished rollout is in
for r in $REGIONS; do det=$(aws guardduty list-detectors --region $r --query "DetectorIds[0]" --output text);
echo "$r $(aws guardduty list-members --region $r --detector-id $det --query "Members[?RelationshipStatus!='Enabled'].[AccountId,RelationshipStatus]" --output text)"; done
eu-west-1
us-east-1
ap-southeast-2 333344445555 Disabled
one account, one Region, opted out: exactly the gap the loop exists to find

Severity is the routing table

GuardDuty severity bands and one workable routing

BandValueWhat the docs say it meansRoute
Critical9.0–10.0an attack sequence is in progress or recently happened; resources potentially compromisedpage, and start the runbook for the finding type
High7.0–8.9the resource is compromised and actively used for unauthorised purposespage
Medium4.0–6.9suspicious activity that deviates from normal behavioursecurity channel, triaged within the working day
Low1.0–3.9an attempt that did not compromise anything, such as a blocked port scandashboard; suppress the recurring ones

The Critical band is newer than most routing rules; a filter written as severity >= 7 still catches it, but a runbook keyed only on finding type will not know that an attack sequence finding groups several signals into one. A finding's severity also depends on context, so the same type can arrive in different bands; route on the number, and keep the finding type for the runbook lookup. The finding names themselves are specific enough to key a response on: UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS says that an instance role's credentials were used from outside AWS, and the first response step is to revoke that role's sessions before asking how.

Route with EventBridge, from the administrator account

eventbridge-rule.json (event pattern; target: SNS topic for paging, then a Lambda per finding type)
{
"source": ["aws.guardduty"],
"detail-type": ["GuardDuty Finding"],
"detail": {
"severity": [{ "numeric": [">=", 7] }]
}
}

Findings from every member account arrive in the delegated administrator's EventBridge bus in each Region, so the rule lives there and not in each account. New findings are exported as they are created; subsequent occurrences of the same finding are batched at the detector's publishing frequency, six hours unless changed, the reason the rollout above set fifteen minutes. A second rule for the 4.0–6.9 band feeds a chat channel, and a third with no severity filter writes everything to a bucket or Security Hub for the history that a later investigation will want. Automated response belongs behind the paging rule and starts small: a Lambda that snapshots the instance's volumes and tags the finding is safe to run unattended; one that isolates the ENI or revokes a role's sessions is right for some finding types and an outage for others, and gets a human approval step until the false-positive rate is known.

Suppress reactively, by filter

suppress-onprem-egress.sh
# instance credentials used from the corporate egress IP: expected when VPC traffic
# leaves through an on-premises gateway instead of an internet gateway (a documented case)
aws guardduty create-filter --region eu-west-1 --detector-id "$det" \
--name onprem-egress-credential-use --action ARCHIVE --rank 1 \
--finding-criteria '{"Criterion":{
"type":{"Eq":["UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS"]},
"service.action.awsApiCallAction.remoteIpDetails.ipAddressV4":{"Eq":["203.0.113.9"]}}}'

A suppression rule is a filter with ARCHIVE as its action: matching findings are still generated, marked archived, kept for 90 days, and excluded from the EventBridge stream, so the on-call never sees them. The documentation's advice is to write these reactively, for findings that have repeatedly been confirmed as false positives, and to make each one as narrow as the false positive: a finding type plus the specific IP, account or resource, not the type alone. In an organization only the administrator account can create them, which keeps the list in one place where it can be reviewed.

A suppression with no expiry is a blind spot with a name
The pen-test window ends, the scanner IP is reassigned, the on-premises egress moves, and the rule keeps archiving whatever matches. Put the reason and a review date in the filter description, list the filters every quarter, and delete the ones whose reason no longer exists. A rule that archives a real credential exfiltration because the attacker happened to use the range you suppressed is the outcome this discipline exists to prevent.

GuardDuty reports that credentials were misused; what those credentials could do was decided earlier, by IAM policies and by whether CI used OIDC federation instead of long-lived keys. Detection with nothing to limit the blast radius produces a well-documented incident rather than a small one.

Go deeper in a courseAWS security for DevOps engineersGuardDuty, Security Hub, IAM boundaries and the response runbooks that tie them together.View course

Related posts