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).

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.
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.
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.
Before writing any code, I wanted proof GuardDuty was actually live in this account, not just enabled in name. This confirmed it: us-east-1, 410 real findings already logged. There's no point automating a response to a detector that isn't actually watching anything.
Before I touched the Lambda, I wanted to understand where the target instance actually lived. I pulled up the default VPC to confirm the subnet layout so I understood the network boundary I'd be isolating the instance inside of.
This is the instance's everyday security group, captured before I ran anything. I wanted a real 'before' state on record so I could prove, later, that the quarantine actually changed something rather than just reporting success.
This is the containment group the whole pipeline is built around: zero inbound, zero outbound. I checked it directly in the console rather than trusting my own memory of what I'd configured, since this rule set is the actual mechanism of the quarantine, not just a label.
Checked the instance's own IAM role next. I'd scoped it to SSM only, on purpose, so that even after quarantine I could still reach the instance for forensics without having to reopen its network access to do it.
Before writing the remediation logic, I looked at the identity that would actually be running it. I wanted to confirm this role existed and wasn't already carrying permissions broader than what the Lambda would need.
Opened the actual inline policy to verify scope: exactly two EC2 actions plus log writes, nothing else. I did that deliberately, if this function's credentials ever leaked, the blast radius is 'can move a security group,' not 'can do anything in the account.'
This is the containment logic I actually wrote, the function was a 'Hello from Lambda' stub when I started. It pulls the instance ID out of the event, records the current security groups so the before state isn't lost, then swaps the instance onto the quarantine group.
This is the fix for the first real bug I found. The original pattern matched severity with an exact integer array, but GuardDuty reports severities as floats like 8.9, so a real finding could have passed through unmatched. I changed it to a numeric greater than or equal comparison.
I confirmed the rule was actually wired to invoke the Lambda, and that EventBridge had its own invoke role to do it. I wanted to see that explicitly rather than assume the target was correct just because the rule existed.
I ran a controlled test directly against the real instance to prove the logic worked, not just that it deployed without errors. Got a 200 back with the original security group named in the response, which gave me a recorded before state going into the swap.
I didn't want to just trust the Lambda's own success message, so I checked CloudTrail independently. Found exactly one ModifyInstanceAttribute call at the matching timestamp, attributed to the Lambda's own role, an outside record confirming the automation actually did what it claimed.
Pulled the full execution trace as a second independent check: event received, security groups captured before the swap, then the isolation step completing. This is the log trail I'd actually want on hand if I ever had to explain this action after the fact.
Last verification step: I went back into the EC2 console itself, independent of the Lambda and independent of the logs, and confirmed the instance's Security tab actually showed the quarantine group attached with zero inbound rules. That's the real end state, not a reported one.