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.
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
SECURITY=222233334444REGIONS=$(aws ec2 describe-regions --query "Regions[].RegionName" --output text)# management account: delegate once per Regionfor r in $REGIONS; doaws guardduty enable-organization-admin-account --region "$r" --admin-account-id "$SECURITY"done# security (delegated administrator) account: a detector per Region, members auto-enabledfor r in $REGIONS; dodet=$(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.
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)"; doneeu-west-1us-east-1ap-southeast-2 333344445555 Disabledone account, one Region, opted out: exactly the gap the loop exists to findSeverity is the routing table
GuardDuty severity bands and one workable routing
| Band | Value | What the docs say it means | Route |
|---|---|---|---|
| Critical | 9.0–10.0 | an attack sequence is in progress or recently happened; resources potentially compromised | page, and start the runbook for the finding type |
| High | 7.0–8.9 | the resource is compromised and actively used for unauthorised purposes | page |
| Medium | 4.0–6.9 | suspicious activity that deviates from normal behaviour | security channel, triaged within the working day |
| Low | 1.0–3.9 | an attempt that did not compromise anything, such as a blocked port scan | dashboard; 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
{"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
# 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.
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