Home› Blog› Week 11 - Security Hub + GuardDuty: Findings That …
AWS Weekly Lab AWS Terraform

Security Hub + GuardDuty: Findings That Fix Themselves

A security finding is worthless if it sits in a console nobody watches. This week wires Security Hub and GuardDuty to EventBridge and Lambda so the clear-cut, dangerous misconfigurations — a management port open to the whole internet, a public S3 bucket — get fixed automatically in seconds, while active-threat findings that need human judgement get escalated to a person instead of being touched. The whole thing is tag-gated so it can never surprise a resource you meant to leave alone.

AWS Platform Engineering Lab · Week 11

Lab Overview

A security group gets opened to the whole internet on port 22 — someone debugging at 6pm who meant to close it afterwards. GuardDuty and Security Hub are very good at spotting that within minutes. Then the finding sits in a console, because nobody wants to be the person whose automation revoked a rule production depended on.

This lab closes that loop for a narrow, clear-cut set of findings: the fix runs in seconds and the finding is marked resolved, while judgement calls still go to a human. The blast radius is handled by one boring guardrail — nothing is ever modified unless it carries an explicit opt-in tag.

What it doesAuto-remediates world-open security-group rules and public S3 buckets; routes GuardDuty threats to a human instead
Core servicesSecurity Hub CSPM, GuardDuty, EventBridge, Lambda, SNS, SQS, CloudWatch, AWS Config
Terraform resources30 applied, from 16 blocks across three modules (seven of the remediation module's blocks use for_each over the three functions)
Safety modelOpt-in by tag, enforced twice — in the function code and again as an IAM aws:ResourceTag condition
Lab runtimeNot recorded
CostEstimated $5–20/month if left running; the lab ran inside the 30-day free trials of both services. Destroyed: $0
StatusPublished 23 July 2026 — resources destroyed

Cloud Services and Concepts

ServiceIts job here
Security Hub CSPMRuns the AWS Foundational Security Best Practices standard and normalizes every finding, including GuardDuty's, into one schema
Amazon GuardDutyThreat detection over CloudTrail, VPC Flow Logs and DNS; publishes to EventBridge every 15 minutes
EventBridgeContent-based routing — one rule per finding class, matching on the control ID
Lambda (Python 3.13)Three functions: two remediate, one only notifies
SNS / SQSResults and alerts out; a 14-day dead-letter queue for anything that fails
CloudWatchAn alarm on dead-letter queue depth
AWS ConfigBacks the controls being remediated — without it recording, the pipeline deploys cleanly and does nothing

ASFF — one schema for everything

The AWS Security Finding Format is what makes a single pipeline possible. A GuardDuty threat and a failed config control arrive in the same shape, so one set of routing rules and one writeback call cover both.

Classic CSPM, not the unified Security Hub

AWS relaunched a unified Security Hub (v2 APIs) at re:Invent in December 2025, correlating GuardDuty, Inspector, Macie and classic Security Hub into one view. As of mid-2026 the Terraform AWS provider does not stably support it — aws_securityhub_account_v2 is still an open feature request. That costs this build nothing: v2 is a correlation layer above the finding stream and does not change how you react to a finding. Everything here is classic CSPM, which the provider fully supports, and v2 can be enabled alongside it in the console.

Architecture and Workflow

Security Hub + GuardDuty Auto-RemediationAmazon GuardDutyactive-threat detectionSecurity Hub CSPMFSBP standard + aggregationfindings ingestedautomation rule: prod → CRITICAL (before routing)Amazon EventBridgecontent-based routingFindings Imported (NEW)GuardDuty Findingsg_remediatorrevoke world-open ingresss3_remediatorapply Block Public Accessthreat_notifierescalate — no mutationEC2.13/14/19S3.2/3/8aws.guarddutyRevoke 0.0.0.0/0 → :22 / :3389S3 Block Public Access onSNS notify for human triagetag-gated: auto-remediate=truetag-gated: auto-remediate=truewriteback: BatchUpdateFindings → RESOLVEDAmazon SNSemail alertsSQS DLQ14-dayCW alarmDLQ depthon failurealarm → SNSnotify email → katta… (subscribed)
  1. Detect. GuardDuty analyses CloudTrail, VPC Flow Logs and DNS. Security Hub CSPM runs the FSBP standard and ingests GuardDuty's findings, all normalized to ASFF.
  2. Enrich, before routing. A Security Hub automation rule runs inside Security Hub before any EventBridge rule fires, escalating failed controls on resources tagged environment=production to CRITICAL. Downstream paging gets smarter without touching a line of Lambda code.
  3. Route. EventBridge rules match on detail.findings.Compliance.SecurityControlId — EC2.13/14/19 to the security-group remediator, S3.2/3/8 to the S3 remediator — and on the native aws.guardduty event for the threat notifier. Every rule matches Workflow.Status = NEW only, which turns out to be the most important clause in the stack.
  4. Act, but only on opt-in. The security-group remediator revokes just the world-open ingress on high-risk ports; the S3 remediator applies all four Block Public Access settings. Both act only on resources tagged auto-remediate=true. Anything untagged is reported to a human and left exactly as it is.
  5. Write back. Every action is written to the finding via BatchUpdateFindings, so the console reflects reality rather than a backlog of issues that were quietly fixed hours ago. Failed asynchronous invocations land in a dead-letter queue that alarms — a dropped security action is worse than a slow one.

Prerequisites

RequirementWhyHow to check
AWS Config recording security groups and S3 buckets The controls this stack remediates (EC2.13/14/19, S3.2/3/8) are backed by Config. Without a recorder they report NO_DATA, no findings are raised, and the pipeline does nothing at all aws configservice describe-configuration-recorder-status returns recording=True, lastStatus=SUCCESS
No second Config recorder An account normally has one centrally-managed recorder; adding another conflicts. This stack deliberately does not provision one Same command — confirm exactly one recorder exists
Terraform ≥ 1.10, AWS provider ~> 6.0 Classic CSPM, GuardDuty and aws_securityhub_automation_rule are all stable in the 6.x line terraform version
A confirmed SNS email subscription Every remediation receipt and threat alert goes here; an unconfirmed subscription silently drops them The subscription shows Confirmed, not Pending
Patience on the first apply Subscribing the FSBP standard on a cold account takes longer than the provider's three-minute default The resource sets timeouts { create = "15m" }

Implementation

ModuleResourcesIts job
securityhub4Account enablement, the FSBP standard subscription, a cross-region finding aggregator, and the automation rule that escalates production findings
guardduty1One detector — foundational sources, Extended Threat Detection included at no extra cost
remediation25Three Lambdas built from one for_each block, each with its own role, policy, log group, EventBridge rule and invoke permission, plus a shared SNS topic, SQS dead-letter queue and CloudWatch alarm

Setting control_finding_generator = "SECURITY_CONTROL" on the account matters more than it looks: it is what populates Compliance.SecurityControlId, the exact field every EventBridge rule keys on. Without it the routing silently matches nothing.

AWS Lambda functions list showing the three week11 remediation functions with their descriptions
The three actors: two tag-gated remediators and one notify-only threat handler.

Lambda is the right shape here rather than Step Functions or ECS: each remediation is one or two API calls, stateless, event-triggered, finished in seconds, with near-zero concurrency. Step Functions would earn its place if a remediation needed approval steps or ran for minutes. Neither does.

Security Hub CSPM findings console showing findings from both Security Hub and GuardDuty in a single pane with account IDs redacted
One pane: Security Hub's config findings and GuardDuty's threat findings side by side, all in ASFF.

Validation and Evidence

Each path was exercised against real findings in the account, not synthetic events — which is how the third problem below was found at all.

TestExpectedActualStatus
Security group with SSH and RDP open to 0.0.0.0/0, tagged auto-remediate=true World-open rules revoked, finding set to RESOLVED Revoked and resolved; about 90 seconds from finding to fix Pass
Public S3 bucket, tagged for opt-in All four Block Public Access settings applied, finding RESOLVED Applied and resolved, overriding the public bucket policy still present Pass
GuardDuty sample findings (credential exfiltration, crypto-mining) Human notified by SNS; nothing mutated Alert delivered with full context and an explicit note that no action was taken Pass
Untagged public bucket already in the account Reported, never modified A [REVIEW] email was sent; the bucket was left untouched Pass
Failed asynchronous invocation Lands in the dead-letter queue and raises an alarm Alarm configured on queue depth with a 14-day retention Pass
EC2 security group detail page showing two inbound rules for SSH port 22 and RDP port 3389
Before: two management ports open to the entire internet. Config evaluates the new group within minutes.
The same EC2 security group now showing zero inbound rules after remediation
After: the world-open ingress is gone. The legitimate rules (had there been any) would have been left in place.
SNS email with subject RESOLVED Closed open security group, describing the revoked rules
And the receipt: the remediator reports exactly what it did, on the same SNS channel.

Security and Cost

The point of auto-remediation is that it acts with no human in the loop, so the safety has to be structural rather than procedural.

  • Opt-in by tag, enforced twice. Mutating remediations only touch resources tagged auto-remediate=true. The function checks the tag, and its IAM policy carries an aws:ResourceTag condition so RevokeSecurityGroupIngress itself refuses on anything untagged. A bug in the code cannot widen the blast radius.
  • Least privilege per function. Each Lambda has its own role with exactly the actions it needs. The threat notifier has no mutating permissions at all — it can publish to SNS and nothing else.
  • Surgical and idempotent. The remediator strips only the 0.0.0.0/0 and ::/0 ingress on high-risk ports, leaving scoped rules alone, and treats an already-removed rule as success.
  • No silent failures. Failed invocations go to a 14-day dead-letter queue that alarms. Every action is written back to the finding, so there is always a record of what changed and why.
SNS email with subject REVIEW Public S3 bucket not auto-remediated, explaining the bucket was not tagged and no change was made
The tag-gate working in the wild: an untagged bucket is reported, never modified. This is what makes auto-remediation safe to run.

Cost

Estimates for a single low-traffic account in us-east-1, from the published rates in July 2026 — this lab ran inside the 30-day free trials of both Security Hub and GuardDuty, so no charge was measured.

ItemEstimate
GuardDuty foundational sources$2–5 / month
Security Hub CSPM checks ($0.0010 each, first 10,000 findings a month free)$3–15 / month
EventBridge, Lambda, SNS, SQSWithin the free tier
After destroy$0

Troubleshooting and Lessons

The remediator fed itself, about eight times a minute

The S3 remediator began firing indefinitely against one pre-existing untagged bucket, emailing every time.

Root cause: the EventBridge rule matched Workflow.Status of both NEW and NOTIFIED. On the notify path the Lambda called BatchUpdateFindings to set the status to NOTIFIED — and that update was re-emitted as a finding event, which matched the rule again. The automation's own writeback was its trigger.

Fix: match NEW only, so the writeback status falls outside the trigger pattern. This never appeared in synthetic testing; it took the account's real findings to surface it.

The first apply timed out, then undid itself

aws_securityhub_standards_subscription failed with "timeout while waiting for state to become READY".

Root cause: the FSBP standard has roughly 300 controls and sits in PENDING on a cold account for longer than the provider's three-minute default. Each failed apply then disabled and re-enabled the standard, resetting the clock — a loop that looks like a broken resource and is really an impatient one.

Fix: timeouts { create = "15m" }.

The fix was pushed, and the old code ran anyway

A run queued through the HCP API timed out at three minutes again, with the fifteen-minute setting nowhere in evidence.

Root cause: a run created via the HCP API reuses the workspace's most recently ingested configuration version. It does not fetch the new commit, so both runs executed the pre-fix code.

Fix: bind the run to the new commit's configuration version, or let the VCS webhook ingest it first and confirm the run's version matches the commit you intend. "Queue a run via the API" is not "deploy my latest commit".

Cleanup and Key Takeaways

Queue a destroy on the week-11-dev workspace: it disables Security Hub CSPM and the GuardDuty detector and removes the Lambdas, rules, SNS topic and dead-letter queue. A small script tears down the deliberately-insecure test resources. One apply rebuilds the stack in about ten minutes, so destroying is cheap to reverse.

  • Breaking the loop is the whole game. Any find-and-act automation whose action updates the thing that triggered it will feed itself forever unless the writeback status falls outside the trigger pattern.
  • Opt-in, not opt-out. Tag-scoping enforced in both code and IAM is what turns "terrifying" into something you would run in production. An untagged resource is reported, never touched.
  • Fix the mechanical, escalate the judgement calls. Misconfigurations get closed automatically; active threats get a human. Killing the wrong instance mid-incident can be worse than the threat.
  • Know what the pipeline depends on. Security Hub controls need Config recording, and standards take minutes to initialize. Neither is obvious when the symptom is a pipeline that deploys perfectly and does nothing.
  • Real findings find what synthetic events cannot. The feedback loop was invisible to unit tests and obvious within a minute of meeting the account's actual backlog.

References

Comments

How was your experience?
Your feedback helps improve this site.
PoorExcellent