← All labs
LAB 01High

Automated EC2 Quarantine on GuardDuty Findings

When GuardDuty raises a high severity finding against an EC2 instance, this pipeline strips its network access automatically. EventBridge filters the finding, a Lambda captures the instance’s state and swaps it onto a zero rule security group, and every step is verified independently rather than trusted at face value.

Architecture

GuardDuty → EventBridge (severity filter) → Lambda → EC2 (security group swap).

Diagram: a GuardDuty finding with severity 8.9 reaches an EventBridge rule matching severity greater than or equal to 7, which invokes a Lambda. The Lambda calls describe_instances then modify_instance_attribute, swapping the EC2 instance from guardduty-lab-normal-sg to lab-quarantine-sg, which has zero inbound and zero outbound rules.
Fig. 1: Detection to response mechanism: a HIGH severity finding triggers the Lambda, which captures the instance's current security groups, then swaps it onto a zero rule quarantine group.
Quarantine SG
0 inbound · 0 outbound
Lambda IAM scope
2 EC2 actions + logs only
Instance role
SSM only (forensics ready)
Region
us-east-1

Two real bugs, found before they mattered

The skeleton was already wired. GuardDuty on, EventBridge pointed at a Lambda, roles provisioned. The Lambda itself was a return "Hello from Lambda" stub. I wrote the actual containment logic, and caught two problems that would have made it fail silently against a real finding.

Bug 1: environment variable name mismatch

I wrote the Lambda to read QUARANTINE_SECURITY_GROUP_ID. The variable actually provisioned in the function’s configuration was named QUARANTINE_SG_ID. Read against the wrong key, this returns None with no error at deploy time. It would only surface the first time a real finding tried to invoke the function. Caught by checking the Lambda’s actual Configuration tab instead of assuming my variable name matched what was already there.

Bug 2: EventBridge severity filter, exact match on floats

The rule’s pattern was "severity": [7, 8, 9], an exact match array. GuardDuty severities are floats (8.9, 7.4), so a real high severity finding could pass through this filter completely unmatched. Fixed to AWS’s numeric comparison syntax: {"numeric": [">=", 7]}.

Validation

I didn’t trust the Lambda’s own “success” response. I checked containment independently at every layer: a controlled test invocation against the real instance, the EC2 console showing the security group actually changed, the full CloudWatch Logs execution trace, and a CloudTrail record showing exactly one ModifyInstanceAttribute call attributed to the Lambda’s own role at the matching timestamp. The test was run twice. The second run against an already quarantined instance confirmed the operation is idempotent.

Evidence

Every screenshot below is from the live AWS console for this lab, shown at full size. Account IDs, access keys, and source IPs are redacted; resource IDs and security group names are left visible.