AWS GuardDuty Threat Detection Tuning for Production

Reducing GuardDuty noise by 60% while maintaining detection of real threats through suppression rules, trusted IP lists, and finding prioritization

#aws#guardduty#security#threat-detection
Cover image for the article: AWS GuardDuty Threat Detection Tuning for Production

Introduction

AWS GuardDuty continuously monitors VPC Flow Logs, CloudTrail events, DNS logs, EKS audit logs, and S3 data events to detect threats using machine learning and threat intelligence. Out of the box, GuardDuty generates significant noise: in a typical 50-account organization, expect 200-500 findings per week, of which 60-80% are benign or informational.

The challenge is not enabling GuardDuty but tuning it so that security teams can focus on genuine threats rather than drowning in false positives. Proper tuning reduces actionable findings by 60% without missing real incidents.

GuardDuty Finding Categories

CategoryFinding TypesTypical VolumeTrue Positive Rate
ReconnaissancePort scanning, API enumerationHigh (40%)15%
Instance CompromiseCryptocurrency, C2 communicationMedium (20%)75%
Account CompromiseImpossible travel, unusual API callsMedium (15%)45%
Bucket CompromisePublic access, unusual S3 accessLow (10%)60%
KubernetesPrivileged container, anonymous accessMedium (15%)50%

Chart

Most Common Findings (and Their Signal Value)

Finding TypeFrequencyActionTypical Cause
Recon:EC2/PortProbeUnprotectedPortVery HighSuppress (most)Internet noise
UnauthorizedAccess:EC2/SSHBruteForceHighSuppress if bastionKnown pattern
Behavior:EC2/NetworkPortUnusualMediumInvestigateOften benign service
CryptoCurrency:EC2/BitcoinTool.BLowCritical alertReal compromise
Discovery:S3/MaliciousIPCallerMediumInvestigateScan or real probe
Impact:EC2/PortSweepHighSuppress if scannerSecurity scanning tools
UnauthorizedAccess:IAMUser/InstanceCredentialExfiltrationLowCritical alertCredential theft

Suppression Rules

Suppression rules archive findings that match known benign patterns:

# Suppress port scanning from known security scanners
aws guardduty create-filter \
  --detector-id abc123 \
  --name "suppress-security-scanners" \
  --action ARCHIVE \
  --finding-criteria '{
    "Criterion": {
      "type": {
        "Eq": ["Recon:EC2/PortProbeUnprotectedPort", "Recon:EC2/Portscan"]
      },
      "service.action.portProbeAction.remoteIpDetails.ipAddressV4": {
        "Eq": ["203.0.113.10", "203.0.113.11", "198.51.100.50"]
      }
    }
  }' \
  --description "Suppress findings from authorized vulnerability scanners"

# Suppress SSH brute force on bastion hosts (expected)
aws guardduty create-filter \
  --detector-id abc123 \
  --name "suppress-bastion-ssh" \
  --action ARCHIVE \
  --finding-criteria '{
    "Criterion": {
      "type": {
        "Eq": ["UnauthorizedAccess:EC2/SSHBruteForce"]
      },
      "resource.instanceDetails.tags.value": {
        "Eq": ["bastion"]
      }
    }
  }' \
  --description "SSH brute force on bastion hosts is expected traffic"

# Suppress DNS findings for known SaaS services
aws guardduty create-filter \
  --detector-id abc123 \
  --name "suppress-known-dns" \
  --action ARCHIVE \
  --finding-criteria '{
    "Criterion": {
      "type": {
        "Eq": ["Trojan:EC2/DNSDataExfiltration"]
      },
      "service.action.dnsRequestAction.domain": {
        "Eq": ["api.segment.io", "events.pendo.io", "dc.services.visualstudio.com"]
      }
    }
  }' \
  --description "Known SaaS services that trigger DNS exfiltration findings"

Trusted IP Lists

Trusted IP lists prevent findings from being generated for known-good sources:

# Create trusted IP list for corporate egress IPs
aws guardduty create-ip-set \
  --detector-id abc123 \
  --name "corporate-egress-ips" \
  --format TXT \
  --location s3://security-configs/trusted-ips.txt \
  --activate

# trusted-ips.txt content:
# 203.0.113.0/24    # Main office
# 198.51.100.0/24   # VPN egress
# 192.0.2.0/24      # Data center

Threat IP Lists (Custom Threat Intelligence)

# Add custom threat intelligence
aws guardduty create-threat-intel-set \
  --detector-id abc123 \
  --name "internal-threat-feed" \
  --format TXT \
  --location s3://security-configs/threat-ips.txt \
  --activate

Organization-Wide Configuration

import boto3

def configure_guardduty_org(admin_account_id, detector_id):
    """Configure GuardDuty across the organization."""
    gd = boto3.client('guardduty')

    # Enable all protection plans
    gd.update_organization_configuration(
        DetectorId=detector_id,
        AutoEnable=True,
        DataSources={
            'S3Logs': {'AutoEnable': True},
            'Kubernetes': {'AuditLogs': {'AutoEnable': True}},
            'MalwareProtection': {
                'ScanEc2InstanceWithFindings': {
                    'EbsVolumes': {'AutoEnable': True}
                }
            }
        },
        Features=[
            {
                'Name': 'EKS_RUNTIME_MONITORING',
                'AutoEnable': 'ALL'
            },
            {
                'Name': 'LAMBDA_NETWORK_LOGS',
                'AutoEnable': 'ALL'
            },
            {
                'Name': 'RDS_LOGIN_EVENTS',
                'AutoEnable': 'ALL'
            }
        ]
    )

    # Set publishing frequency to 15 minutes (fastest)
    gd.update_detector(
        DetectorId=detector_id,
        FindingPublishingFrequency='FIFTEEN_MINUTES'
    )

Automated Response Pipeline

EventBridge Rules by Severity

{
  "source": ["aws.guardduty"],
  "detail-type": ["GuardDuty Finding"],
  "detail": {
    "severity": [{"numeric": [">=", 7]}],
    "type": [{
      "anything-but": {
        "prefix": "Recon:"
      }
    }]
  }
}

Response Lambda

import boto3
import json

def handler(event, context):
    """Automated response to high-severity GuardDuty findings."""
    finding = event['detail']
    finding_type = finding['type']
    severity = finding['severity']

    # Critical responses (severity >= 8)
    if severity >= 8:
        if 'CryptoCurrency' in finding_type:
            isolate_instance(finding)
            alert_security_team(finding, priority='P1')

        elif 'InstanceCredentialExfiltration' in finding_type:
            revoke_instance_credentials(finding)
            alert_security_team(finding, priority='P1')

        elif 'Backdoor' in finding_type:
            isolate_instance(finding)
            snapshot_for_forensics(finding)
            alert_security_team(finding, priority='P1')

    # High severity responses (severity 7-8)
    elif severity >= 7:
        alert_security_team(finding, priority='P2')
        create_investigation_ticket(finding)

def isolate_instance(finding):
    """Apply restrictive security group to isolate instance."""
    ec2 = boto3.client('ec2')
    instance_id = finding['resource']['instanceDetails']['instanceId']

    # Create isolation security group (no inbound, no outbound)
    vpc_id = finding['resource']['instanceDetails']['networkInterfaces'][0]['vpcId']

    sg = ec2.create_security_group(
        GroupName=f'isolation-{instance_id}',
        Description=f'Isolation SG for compromised {instance_id}',
        VpcId=vpc_id
    )

    # Remove all egress rules
    ec2.revoke_security_group_egress(
        GroupId=sg['GroupId'],
        IpPermissions=[{
            'IpProtocol': '-1',
            'IpRanges': [{'CidrIp': '0.0.0.0/0'}]
        }]
    )

    # Replace instance security groups with isolation group
    ec2.modify_instance_attribute(
        InstanceId=instance_id,
        Groups=[sg['GroupId']]
    )

def revoke_instance_credentials(finding):
    """Revoke compromised instance role credentials."""
    iam = boto3.client('iam')
    role_name = finding['resource']['accessKeyDetails']['userName']

    # Attach deny-all policy to invalidate existing sessions
    deny_policy = {
        "Version": "2012-10-17",
        "Statement": [{
            "Effect": "Deny",
            "Action": "*",
            "Resource": "*",
            "Condition": {
                "DateLessThan": {
                    "aws:TokenIssueTime": finding['service']['eventFirstSeen']
                }
            }
        }]
    }

    iam.put_role_policy(
        RoleName=role_name,
        PolicyName='EmergencySessionRevocation',
        PolicyDocument=json.dumps(deny_policy)
    )

Tuning Metrics

Before and After Tuning

MetricBefore TuningAfter TuningChange
Weekly findings450180-60%
True positives investigated35350%
False positives reviewed415145-65%
Time spent on triage20 hrs/week8 hrs/week-60%
Mean time to investigate (real)4 hours1.5 hours-63%
Findings auto-remediated015/weekNew

KPI Targets

KPITargetMeasurement
Critical finding MTTR< 1 hourFinding to containment
High finding MTTR< 4 hoursFinding to investigation start
Suppression ratio40-60%Suppressed / total generated
Auto-remediation rate> 30%Auto-handled / total actionable
False positive rate (post-tuning)< 40%False positives / findings reviewed

Key Takeaways

  • Suppress reconnaissance findings from known sources (security scanners, bastion hosts, monitoring tools) to eliminate 40-50% of noise immediately.
  • Create trusted IP lists for corporate networks to prevent findings from legitimate employee activity and CI/CD pipelines.
  • Automate response for cryptocurrency and credential exfiltration findings since these have 75%+ true positive rates and require immediate containment.
  • Set publishing frequency to 15 minutes for faster detection-to-response times on critical findings.
  • Enable all protection plans (S3, EKS, Lambda, RDS, Malware) for comprehensive threat coverage across the full AWS surface area.
  • Tune iteratively over 4-6 weeks, starting with the highest-volume finding types and validating each suppression rule does not hide real threats.
  • Proper tuning reduces security team triage time by 60% while maintaining the same detection rate for genuine threats.

Comments

    No comments yet. Be the first to share your thoughts.