AWS GuardDuty: Intelligent Threat Detection and Incident Response

AWS GuardDuty Threat Detection

AWS GuardDuty is a fully managed threat detection service that continuously monitors your AWS accounts, workloads, and data for malicious activity. Unlike traditional IDS solutions that require you to collect, correlate, and analyze logs yourself, GuardDuty is a no-infrastructure service — you enable it with a single API call and it immediately starts analyzing billions of events per second using machine learning, anomaly detection, and integrated threat intelligence from Amazon, CrowdStrike, and Proofpoint. This guide covers every dimension of GuardDuty: the threat categories and data sources it monitors, how to enable it at scale with CLI and Terraform, how to read and act on findings, how to wire up automated incident response with EventBridge and Lambda, how to integrate with Security Hub, how to use Malware Protection for EBS and S3, and how to manage costs with selective data sources.

1. GuardDuty Threat Categories and Data Sources

GuardDuty detects threats across seven broad categories, each mapped to specific AWS data sources. Understanding this mapping tells you exactly what GuardDuty can and cannot see — and helps you justify enabling optional (paid) data sources for higher-value workloads.

Threat Categories

  • Backdoor — Command-and-control (C2) callbacks, cryptocurrency mining beacons, BitTorrent clients running on EC2
  • Behavior — Unusual API call patterns, IAM users calling APIs in regions they've never used before, unusual data transfer volumes
  • Credential Access — Brute-force SSH/RDP attempts, leaked IAM credential usage from external IP ranges (including Tor exit nodes)
  • Discovery — Reconnaissance via port scanning, AWS API calls that enumerate S3 buckets, IAM roles, or security groups at high velocity
  • Exfiltration — Large S3 data downloads from unusual principals, DNS exfiltration via long DNS query strings
  • Impact — Ransomware-like behavior (deleting backups, modifying S3 bucket policies to expose data publicly)
  • Stealth — CloudTrail disabling, VPC Flow Logs disabling, GuardDuty itself being disabled by an IAM principal

Core Data Sources (always active, no extra charge)

VPC Flow Logs — GuardDuty consumes VPC Flow Logs independently of whether you have them enabled in CloudWatch. It analyzes network connections to identify communication with known-malicious IP addresses (threat intel feeds), port scans from your EC2 instances, and unusual outbound data transfer. GuardDuty processes Flow Logs without you incurring CloudWatch Logs ingestion costs for GuardDuty's copy — it reads directly from the VPC metadata stream.

AWS CloudTrail Management Events — Every management API call (CreateUser, PutRolePolicy, RunInstances, DeleteTrail, etc.) is analyzed. GuardDuty builds a behavioral baseline per IAM principal and flags deviations: an IAM role that has never called GetObject suddenly accessing thousands of S3 objects, or a root account being used for the first time in months.

Route 53 DNS Resolver Logs — DNS queries from EC2 instances and Lambda functions are analyzed against GuardDuty's domain reputation lists. A compromised EC2 instance calling back to a known C2 domain or using DNS for data exfiltration (long encoded subdomains) will generate a finding even before the TCP connection is established.

Optional Data Sources (additional cost)

CloudTrail S3 Data Events — Adds S3 object-level operations (GetObject, PutObject, DeleteObject) to the analysis. Without this, GuardDuty can't detect data exfiltration via S3 at the object level — it only sees the management plane. Enable this for S3 buckets holding sensitive data (PII, financial records, code artifacts).

EKS Audit Logs — Kubernetes API server audit logs from EKS clusters. GuardDuty detects container escape attempts, privileged pod deployment, suspicious exec calls, and unusual API calls from within pods. This is the primary data source for the Kubernetes/* finding family.

EKS Runtime Monitoring — A lightweight GuardDuty security agent deployed as a DaemonSet in your EKS nodes. It monitors system calls at the kernel level: privilege escalation, container breakout, reverse shell execution, file system tampering in sensitive paths.

RDS Login Activity — Monitors authentication activity for Aurora MySQL and Aurora PostgreSQL. Detects brute-force login attempts, anomalous logins from previously unseen user accounts, and logins from known-malicious IP addresses.

Lambda Network Activity — Analyzes outbound network connections from Lambda functions. A Lambda function that calls an external C2 domain or communicates with a Tor exit node generates a Lambda/CryptoCurrency or Lambda/TrojanEC2 finding.

Data source coverage matrix: Enable EKS Audit Logs + EKS Runtime for container workloads, RDS Login Activity for databases holding PII, and S3 Data Events for your most sensitive buckets. You can scope S3 Data Events to specific buckets rather than all buckets to control cost.

2. Enabling GuardDuty: CLI, Terraform, and Multi-Account via Organizations

GuardDuty is regional — you must enable it in every region you use. The fastest path for a single account is the AWS console or CLI. For multi-account, AWS Organizations + GuardDuty delegated administrator is the only scalable approach.

Single Account — AWS CLI

# Enable GuardDuty in the current region
aws guardduty create-detector \
  --enable \
  --finding-publishing-frequency FIFTEEN_MINUTES \
  --data-sources '{
    "S3Logs": {"Enable": true},
    "Kubernetes": {"AuditLogs": {"Enable": true}},
    "MalwareProtection": {"ScanEc2InstanceWithFindings": {"EbsVolumes": true}}
  }'

# Get the detector ID (needed for all subsequent commands)
DETECTOR_ID=$(aws guardduty list-detectors --query 'DetectorIds[0]' --output text)
echo "Detector ID: $DETECTOR_ID"

# Verify it's active
aws guardduty get-detector --detector-id $DETECTOR_ID \
  --query '{Status:Status,UpdatedAt:UpdatedAt,DataSources:DataSources}'

Enable Across All Regions with a Shell Loop

# Enable GuardDuty in all AWS regions your account uses
for REGION in $(aws ec2 describe-regions --query 'Regions[].RegionName' --output text); do
  echo "Enabling GuardDuty in $REGION..."
  aws guardduty create-detector \
    --enable \
    --finding-publishing-frequency FIFTEEN_MINUTES \
    --region $REGION 2>/dev/null || echo "  Already enabled or error in $REGION"
done

Terraform — Single Account

resource "aws_guardduty_detector" "main" {
  enable                       = true
  finding_publishing_frequency = "FIFTEEN_MINUTES"

  datasources {
    s3_logs {
      enable = true
    }
    kubernetes {
      audit_logs {
        enable = true
      }
    }
    malware_protection {
      scan_ec2_instance_with_findings {
        ebs_volumes {
          enable = true
        }
      }
    }
  }

  tags = {
    Environment = "production"
    ManagedBy   = "terraform"
  }
}

output "guardduty_detector_id" {
  value = aws_guardduty_detector.main.id
}

Multi-Account via AWS Organizations

In an AWS Organizations setup, one account (typically your Security account) becomes the GuardDuty delegated administrator. The delegated admin can enable GuardDuty in all member accounts, aggregate all findings centrally, and manage suppression rules org-wide.

# Step 1: From the management (root) account — designate Security account as delegated admin
aws guardduty enable-organization-admin-account \
  --admin-account-id 123456789012   # your Security account ID

# Step 2: From the Security account — configure auto-enable for new members
DETECTOR_ID=$(aws guardduty list-detectors --query 'DetectorIds[0]' --output text)

aws guardduty update-organization-configuration \
  --detector-id $DETECTOR_ID \
  --auto-enable ALL \
  --datasources '{
    "S3Logs": {"AutoEnable": true},
    "Kubernetes": {"AuditLogs": {"AutoEnable": true}},
    "MalwareProtection": {"ScanEc2InstanceWithFindings": {"AutoEnable": true}}
  }'

# Step 3: List all member accounts and their status
aws guardduty list-members \
  --detector-id $DETECTOR_ID \
  --query 'Members[*].{AccountId:AccountId,Status:RelationshipStatus}'
Auto-enable modes: ALL enables GuardDuty for all existing and future members. NEW only enables it for accounts that join the organization after this setting is applied. Use ALL in production to close gaps on legacy accounts.

3. Understanding Findings: Severity, JSON Anatomy, and Real Examples

Every GuardDuty finding is a structured JSON document describing a specific threat. Understanding the anatomy of a finding — and especially the severity scoring — is essential for triaging the flood of alerts in a real environment.

Severity Levels

SeverityScore RangeColorWhat It Means
High7.0 – 8.9RedActive threat with high confidence — EC2 communicating with C2, IAM credentials used from Tor exit node
Medium4.0 – 6.9OrangeSuspicious activity requiring investigation — unusual API calls, port scanning from your instance
Low1.0 – 3.9YellowReconnaissance or behavioral anomaly — low risk on its own but worth correlating with other events

Finding Type Naming Convention

GuardDuty finding types follow the pattern ThreatPurpose:ResourceTypeAffected/ThreatFamilyName.DetectionMechanism!Artifact. For example:

  • UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS — IAM credentials from an EC2 instance metadata service were used from an IP outside AWS
  • CryptoCurrency:EC2/BitcoinTool.B!DNS — EC2 instance querying DNS names associated with cryptocurrency mining pools
  • Recon:EC2/PortProbeUnprotectedPort — EC2 instance's open port is being probed (reconnaissance)
  • Stealth:IAMUser/CloudTrailLoggingDisabled — CloudTrail was disabled by an IAM user (attacker covering tracks)

Finding JSON Anatomy

{
  "schemaVersion": "2.0",
  "id": "abc123def456",
  "type": "UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS",
  "severity": 8.0,
  "title": "EC2 instance credential used from external IP address",
  "description": "Credentials created from the EC2 instance i-0abc123 were used from an external IP address.",
  "createdAt": "2026-06-08T14:23:11Z",
  "updatedAt": "2026-06-08T14:23:11Z",
  "count": 1,
  "region": "us-east-1",
  "accountId": "111122223333",
  "resource": {
    "resourceType": "AccessKey",
    "accessKeyDetails": {
      "accessKeyId": "ASIA...",
      "principalId": "AROAI...",
      "userType": "Role",
      "userName": "prod-app-role"
    }
  },
  "service": {
    "action": {
      "actionType": "AWS_API_CALL",
      "awsApiCallAction": {
        "api": "GetObject",
        "serviceName": "s3.amazonaws.com",
        "remoteIpDetails": {
          "ipAddressV4": "185.220.101.55",
          "country": {"countryName": "Germany"},
          "organization": {"org": "Tor Project", "asn": "4242"},
          "isTorNode": true
        },
        "callerType": "Remote IP"
      }
    },
    "additionalInfo": {
      "threatListName": "ProofPoint Emerging Threats",
      "threatName": "Tor_Exit_Node"
    },
    "evidence": {
      "threatIntelligenceDetails": [
        {"threatListName": "ProofPoint", "threatNames": ["TorNode"]}
      ]
    }
  }
}

Key fields to focus on during triage: type (tells you exactly what happened), severity (prioritization), resource (what is affected), service.action.awsApiCallAction.remoteIpDetails (where the call came from), and service.evidence.threatIntelligenceDetails (which threat feed flagged this IP or domain).

Finding deduplication: GuardDuty deduplicates findings within a 24-hour window. If the same threat pattern recurs, the existing finding's count field increments and updatedAt advances rather than creating duplicate findings. This means a count of 50 on a single finding is more alarming than it looks — it indicates the attacker is actively retrying.

4. Automated Response: EventBridge + Lambda for EC2 Isolation and IAM Key Revocation

Manual response to GuardDuty findings does not scale. The production-grade pattern is: GuardDuty finding → EventBridge rule → Lambda function that takes automated remediation action. You define different Lambda functions for different finding types, with severity thresholds that gate destructive actions.

Step 1: EventBridge Rule

{
  "source": ["aws.guardduty"],
  "detail-type": ["GuardDuty Finding"],
  "detail": {
    "severity": [{"numeric": [">=", 7]}],
    "type": [
      {"prefix": "UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration"},
      {"prefix": "CryptoCurrency:EC2/"},
      {"prefix": "Backdoor:EC2/"},
      {"prefix": "Trojan:EC2/"}
    ]
  }
}

Create this EventBridge rule targeting a Lambda function:

aws events put-rule \
  --name "GuardDutyHighSeverityResponse" \
  --event-pattern file://guardduty-rule-pattern.json \
  --state ENABLED \
  --description "Trigger automated response for GuardDuty high severity findings"

aws events put-targets \
  --rule GuardDutyHighSeverityResponse \
  --targets Id=1,Arn=arn:aws:lambda:us-east-1:111122223333:function:GuardDutyAutoResponder

Step 2: Lambda — Automated Responder

import boto3
import json
import logging
from datetime import datetime, timezone

logger = logging.getLogger()
logger.setLevel(logging.INFO)

ec2 = boto3.client('ec2')
iam = boto3.client('iam')
sns = boto3.client('sns')
ALERT_TOPIC_ARN = 'arn:aws:sns:us-east-1:111122223333:SecurityAlerts'


def lambda_handler(event, context):
    finding = event['detail']
    finding_type = finding['type']
    severity = finding['severity']
    resource = finding['resource']

    logger.info(f"Processing finding: {finding_type} (severity {severity})")

    actions_taken = []

    # EC2-based threats — isolate the instance
    if resource.get('resourceType') == 'Instance':
        instance_id = resource['instanceDetails']['instanceId']
        result = isolate_ec2_instance(instance_id)
        actions_taken.append(result)

    # IAM credential exfiltration — revoke all sessions for the role
    if 'InstanceCredentialExfiltration' in finding_type:
        access_key_details = resource.get('accessKeyDetails', {})
        principal_id = access_key_details.get('principalId', '')
        user_name = access_key_details.get('userName', '')
        if user_name:
            result = revoke_iam_sessions(user_name)
            actions_taken.append(result)

    # Send security alert
    notify_security_team(finding, actions_taken)
    return {'statusCode': 200, 'actions': actions_taken}


def isolate_ec2_instance(instance_id: str) -> dict:
    """Move instance to an isolation security group with no ingress/egress."""
    try:
        # Get the VPC of the instance
        resp = ec2.describe_instances(InstanceIds=[instance_id])
        reservation = resp['Reservations'][0]['Instances'][0]
        vpc_id = reservation['VpcId']

        # Create (or find) an isolation security group
        sg_name = f'guardduty-isolation-{datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")}'
        sg = ec2.create_security_group(
            GroupName=sg_name,
            Description='GuardDuty auto-isolation: no inbound or outbound traffic',
            VpcId=vpc_id
        )
        isolation_sg_id = sg['GroupId']

        # Remove the default outbound-all rule
        ec2.revoke_security_group_egress(
            GroupId=isolation_sg_id,
            IpPermissions=[{'IpProtocol': '-1', 'IpRanges': [{'CidrIp': '0.0.0.0/0'}]}]
        )

        # Replace the instance's security groups with the isolation SG
        ec2.modify_instance_attribute(
            InstanceId=instance_id,
            Groups=[isolation_sg_id]
        )

        # Tag the instance to document the action
        ec2.create_tags(
            Resources=[instance_id],
            Tags=[
                {'Key': 'GuardDuty-Isolated', 'Value': 'true'},
                {'Key': 'GuardDuty-IsolatedAt', 'Value': datetime.now(timezone.utc).isoformat()},
                {'Key': 'GuardDuty-IsolationSG', 'Value': isolation_sg_id}
            ]
        )

        logger.info(f"Isolated EC2 instance {instance_id} into SG {isolation_sg_id}")
        return {'action': 'ec2_isolated', 'instance_id': instance_id, 'isolation_sg': isolation_sg_id}

    except Exception as e:
        logger.error(f"Failed to isolate {instance_id}: {e}")
        return {'action': 'ec2_isolation_failed', 'instance_id': instance_id, 'error': str(e)}


def revoke_iam_sessions(user_or_role_name: str) -> dict:
    """Deny all active sessions for an IAM role by attaching an inline deny policy."""
    try:
        deny_policy = {
            "Version": "2012-10-17",
            "Statement": [{
                "Effect": "Deny",
                "Action": "*",
                "Resource": "*",
                "Condition": {
                    "DateLessThan": {
                        "aws:TokenIssueTime": datetime.now(timezone.utc).isoformat()
                    }
                }
            }]
        }

        # Try as role first, then user
        try:
            iam.put_role_policy(
                RoleName=user_or_role_name,
                PolicyName='GuardDutyRevoke',
                PolicyDocument=json.dumps(deny_policy)
            )
            logger.info(f"Revoked sessions for role {user_or_role_name}")
            return {'action': 'iam_sessions_revoked', 'role': user_or_role_name}
        except iam.exceptions.NoSuchEntityException:
            iam.put_user_policy(
                UserName=user_or_role_name,
                PolicyName='GuardDutyRevoke',
                PolicyDocument=json.dumps(deny_policy)
            )
            logger.info(f"Revoked sessions for user {user_or_role_name}")
            return {'action': 'iam_sessions_revoked', 'user': user_or_role_name}

    except Exception as e:
        logger.error(f"Failed to revoke sessions for {user_or_role_name}: {e}")
        return {'action': 'iam_revoke_failed', 'principal': user_or_role_name, 'error': str(e)}


def notify_security_team(finding: dict, actions_taken: list):
    """Send SNS notification with finding summary and actions taken."""
    message = {
        'finding_id': finding['id'],
        'type': finding['type'],
        'severity': finding['severity'],
        'region': finding['region'],
        'account_id': finding['accountId'],
        'title': finding['title'],
        'actions_taken': actions_taken,
        'console_url': f"https://console.aws.amazon.com/guardduty/home?region={finding['region']}#/findings?fId={finding['id']}"
    }
    sns.publish(
        TopicArn=ALERT_TOPIC_ARN,
        Subject=f"[GuardDuty HIGH] {finding['type']}",
        Message=json.dumps(message, indent=2)
    )
Important: The IAM session revocation trick works by attaching a deny policy conditioned on aws:TokenIssueTime. Any token issued before the current timestamp is immediately denied — existing valid sessions are invalidated without rotating the actual access key. This is the AWS-recommended method for emergency credential invalidation.

Terraform: Lambda + EventBridge + IAM Role

resource "aws_iam_role" "guardduty_responder" {
  name = "guardduty-auto-responder"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { Service = "lambda.amazonaws.com" }
      Action    = "sts:AssumeRole"
    }]
  })
}

resource "aws_iam_role_policy" "guardduty_responder" {
  name   = "guardduty-responder-policy"
  role   = aws_iam_role.guardduty_responder.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect   = "Allow"
        Action   = ["ec2:DescribeInstances", "ec2:CreateSecurityGroup",
                    "ec2:RevokeSecurityGroupEgress", "ec2:ModifyInstanceAttribute",
                    "ec2:CreateTags"]
        Resource = "*"
      },
      {
        Effect   = "Allow"
        Action   = ["iam:PutRolePolicy", "iam:PutUserPolicy"]
        Resource = "*"
      },
      {
        Effect   = "Allow"
        Action   = ["sns:Publish"]
        Resource = aws_sns_topic.security_alerts.arn
      },
      {
        Effect   = "Allow"
        Action   = ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"]
        Resource = "arn:aws:logs:*:*:*"
      }
    ]
  })
}

resource "aws_cloudwatch_event_rule" "guardduty_high" {
  name        = "guardduty-high-severity"
  description = "GuardDuty high severity finding auto-response"
  event_pattern = jsonencode({
    source        = ["aws.guardduty"]
    "detail-type" = ["GuardDuty Finding"]
    detail = {
      severity = [{ numeric = [">=", 7] }]
    }
  })
}

resource "aws_cloudwatch_event_target" "guardduty_lambda" {
  rule      = aws_cloudwatch_event_rule.guardduty_high.name
  target_id = "GuardDutyResponder"
  arn       = aws_lambda_function.guardduty_responder.arn
}

5. GuardDuty + Security Hub: Aggregating Findings and Suppression Rules

AWS Security Hub is the SIEM-lite aggregator for your AWS security findings. When you enable Security Hub and connect GuardDuty, every GuardDuty finding is automatically sent to Security Hub in the ASFF (Amazon Security Finding Format) — a normalized JSON schema that lets you correlate findings from GuardDuty, AWS Config, Macie, Inspector, IAM Access Analyzer, and third-party tools in one place.

Enable Security Hub and GuardDuty Integration

# Enable Security Hub (must be done before GuardDuty integration)
aws securityhub enable-security-hub \
  --enable-default-standards \
  --tags Environment=production

# GuardDuty auto-publishes to Security Hub once both are enabled
# Verify the integration is active:
aws securityhub list-enabled-products-for-import \
  --query 'ProductSubscriptions' --output table

Querying GuardDuty Findings in Security Hub

# Get all GuardDuty findings with CRITICAL or HIGH severity from the last 24h
aws securityhub get-findings \
  --filters '{
    "ProductName": [{"Value": "GuardDuty", "Comparison": "EQUALS"}],
    "SeverityLabel": [
      {"Value": "CRITICAL", "Comparison": "EQUALS"},
      {"Value": "HIGH", "Comparison": "EQUALS"}
    ],
    "RecordState": [{"Value": "ACTIVE", "Comparison": "EQUALS"}],
    "WorkflowStatus": [{"Value": "NEW", "Comparison": "EQUALS"}]
  }' \
  --sort-criteria '[{"Field":"SeverityScore","SortOrder":"DESCENDING"}]' \
  --max-items 20 \
  --query 'Findings[*].{Title:Title,Severity:Severity.Label,AccountId:AwsAccountId,Region:Region}'

Creating Suppression Rules in Security Hub

Suppression rules in Security Hub auto-archive findings that match a filter. This is the right place for org-wide suppressions (e.g., suppress Recon findings from your own penetration testing CIDR).

# Create a suppression rule to suppress low-severity port scan findings
# from our known security scanning IP range (10.0.100.0/24)
aws securityhub create-automation-rule \
  --rule-name "Suppress-PortProbe-InternalScanners" \
  --rule-order 1 \
  --description "Suppress Recon:EC2/PortProbeUnprotectedPort from internal security scanners" \
  --criteria '{
    "ProductName": [{"Value": "GuardDuty", "Comparison": "EQUALS"}],
    "Type": [{"Value": "Recon:EC2/PortProbeUnprotectedPort", "Comparison": "EQUALS"}],
    "NetworkSourceIpV4": [{"Cidr": "10.0.100.0/24"}]
  }' \
  --actions '[{
    "Type": "FINDING_FIELDS_UPDATE",
    "FindingFieldsUpdate": {
      "Workflow": {"Status": "SUPPRESSED"},
      "Note": {"Text": "Suppressed: known internal security scanner", "UpdatedBy": "automation"}
    }
  }]'
Multi-region aggregation: Enable Security Hub's finding aggregation feature in your home region. It pulls findings from all enabled regions into a single aggregator region, giving you one pane of glass for your entire AWS organization's security posture without manually querying each region.

6. Malware Protection: EBS and S3 Scanning

GuardDuty Malware Protection adds antimalware scanning capabilities to two surfaces: EC2 EBS volumes (triggered by a GuardDuty finding) and S3 objects (triggered by upload events).

EBS Malware Scanning

When GuardDuty generates an EC2-related finding (e.g., Backdoor:EC2/C&CActivity.B), Malware Protection can automatically scan the attached EBS volumes of the flagged instance. GuardDuty creates a snapshot of the volume, mounts it read-only in an AWS-managed environment, runs the malware scan, then deletes the snapshot. Your production instance is never paused.

# Verify Malware Protection (EBS) is enabled on your detector
aws guardduty get-detector \
  --detector-id $DETECTOR_ID \
  --query 'Features[?Name==`EBS_MALWARE_PROTECTION`]'

# Initiate a manual malware scan on a specific instance (for incident response)
aws guardduty create-malware-scans \
  --detector-id $DETECTOR_ID \
  --scan-resource-criteria '{
    "include": {
      "ec2InstanceTag": {
        "mapEquals": [{"key": "GuardDuty-Isolated", "value": "true"}]
      }
    }
  }'

# List recent scan results
aws guardduty list-malware-scans \
  --detector-id $DETECTOR_ID \
  --query 'Scans[*].{ScanId:ScanId,Status:ScanStatus,StartTime:ScanStartTime,ThreatCount:ThreatsDetectedItemCount}'

Interpreting EBS Scan Results

After a scan, GuardDuty generates a Malware:EC2/MaliciousFile finding (if threats are found) or publishes a scan-complete event to EventBridge with no finding (if clean). The finding includes:

  • The volume ID and snapshot ID that was scanned
  • The file path(s) where malware was detected
  • The threat name (e.g., Trojan.Gen, CoinMiner, Backdoor.Linux.Mirai)
  • The hash of the malicious file (SHA-256) for threat intelligence lookup

S3 Malware Protection

S3 Malware Protection scans objects on upload before they are accessible to downstream consumers. This is ideal for file-upload APIs, data lake ingestion pipelines, and shared collaboration buckets.

# Enable S3 Malware Protection for a specific bucket
aws guardduty create-malware-scans \
  --detector-id $DETECTOR_ID

# Create a GuardDuty S3 protection plan
aws guardduty create-s3-protection \
  --detector-id $DETECTOR_ID \
  --bucket-criteria '{
    "includes": {
      "bucketNames": ["my-upload-bucket", "data-ingestion-landing"]
    }
  }'

After scanning, GuardDuty tags the S3 object with GuardDutyMalwareScanStatus = THREATS_FOUND or NO_THREATS_FOUND. You then use an S3 Event Notification → Lambda to quarantine objects tagged as threats (move to a quarantine prefix, remove public ACL, alert the security team) before they're consumed by downstream services.

import boto3

s3 = boto3.client('s3')

def quarantine_handler(event, context):
    """Lambda triggered by S3 Event Notification — quarantine malware-tagged objects."""
    for record in event['Records']:
        bucket = record['s3']['bucket']['name']
        key = record['s3']['object']['key']

        # Get object tags set by GuardDuty
        tags_resp = s3.get_object_tagging(Bucket=bucket, Key=key)
        tags = {t['Key']: t['Value'] for t in tags_resp.get('TagSet', [])}

        scan_status = tags.get('GuardDutyMalwareScanStatus', 'UNKNOWN')
        if scan_status == 'THREATS_FOUND':
            # Move to quarantine prefix
            quarantine_key = f"quarantine/{key}"
            s3.copy_object(
                Bucket=bucket,
                CopySource={'Bucket': bucket, 'Key': key},
                Key=quarantine_key
            )
            s3.delete_object(Bucket=bucket, Key=key)
            print(f"Quarantined {bucket}/{key} → {quarantine_key}")

7. Managing False Positives: Suppression Rules, Trusted IPs, and Threat Lists

In any active AWS environment you will encounter false positives. GuardDuty provides three mechanisms to reduce noise: suppression rules (filter specific finding patterns), trusted IP lists (whitelist known-good IPs), and threat lists (add your own threat intelligence).

GuardDuty Suppression Rules

Suppression rules are filter criteria that automatically archive matching findings. They are evaluated in GuardDuty itself before findings reach EventBridge or Security Hub, which means suppressed findings never trigger your automated response Lambda.

# Suppress Recon:EC2/PortProbeUnprotectedPort findings from a known
# vulnerability scanner instance tag
aws guardduty create-filter \
  --detector-id $DETECTOR_ID \
  --name "suppress-internal-scanner" \
  --description "Suppress port probe findings from internal vulnerability scanner" \
  --action ARCHIVE \
  --finding-criteria '{
    "Criterion": {
      "type": {
        "Eq": ["Recon:EC2/PortProbeUnprotectedPort"]
      },
      "resource.instanceDetails.tags.key": {
        "Eq": ["Role"]
      },
      "resource.instanceDetails.tags.value": {
        "Eq": ["SecurityScanner"]
      }
    }
  }'

# List all suppression filters
aws guardduty list-filters --detector-id $DETECTOR_ID

Trusted IP Lists

Trusted IP lists tell GuardDuty to skip threat analysis for traffic from specific IP ranges. Use this for your office NAT IPs, VPN egress, CI/CD runner IPs, or third-party security scanner IPs that you know are benign.

# Create a text file with one CIDR per line
cat > trusted-ips.txt << 'EOF'
203.0.113.0/24
198.51.100.45/32
192.0.2.0/28
EOF

# Upload to S3
aws s3 cp trusted-ips.txt s3://my-security-bucket/guardduty/trusted-ips.txt

# Create the trusted IP set in GuardDuty
aws guardduty create-ip-set \
  --detector-id $DETECTOR_ID \
  --name "CorpNetworkAndScanners" \
  --format TXT \
  --location s3://my-security-bucket/guardduty/trusted-ips.txt \
  --activate

Custom Threat Lists

You can add your own threat intelligence feeds (e.g., indicators from a recent phishing campaign targeting your industry, internal threat intel) as a threat list. GuardDuty will generate findings whenever your resources communicate with any IP in your custom threat list.

# Upload your threat IP list to S3
aws s3 cp my-threat-ips.txt s3://my-security-bucket/guardduty/threat-list.txt

# Create the threat intel set
aws guardduty create-threat-intel-set \
  --detector-id $DETECTOR_ID \
  --name "InternalThreatIntel-2026" \
  --format TXT \
  --location s3://my-security-bucket/guardduty/threat-list.txt \
  --activate
Suppression vs Trusted IP lists: Suppression rules archive findings after they are generated (the finding exists in GuardDuty but is hidden). Trusted IP lists prevent findings from being generated at all for those IPs. For operational cost and noise reduction, prefer Trusted IP lists for permanently known-good IPs. Use suppression rules for finding-type-specific noise that isn't IP-based (e.g., a specific instance that generates expected port scans).

8. Cost Optimization: 30-Day Trial, Selective Data Sources, and CLI Cost Estimates

GuardDuty pricing is based on the volume of data analyzed per data source. Costs can surprise teams that enable all optional data sources across all accounts and regions without scoping them. Here is how to stay in control.

The 30-Day Free Trial

Every AWS account gets a 30-day free trial when GuardDuty is first enabled. During the trial, the GuardDuty console shows you a projected monthly cost estimate based on your actual data volumes — use this to budget accurately before committing. Optional data sources (EKS, S3 Data Events, Malware Protection) each have their own separate 30-day trials per data source type.

GuardDuty Pricing Model (2026)

Data SourcePricing BasisApproximate Cost
VPC Flow Logs + DNSPer GB analyzed~$1.00/GB (first 500 GB/month, tiered lower after)
CloudTrail Management EventsPer 1M events~$4.00/1M events
S3 Data EventsPer 1M events~$0.80/1M events
EKS Audit LogsPer 1M audit log events~$2.00/1M events
EKS Runtime MonitoringPer vCPU-hour of monitored nodes~$0.01/vCPU-hour
RDS Login ActivityPer RDS instance-hour~$0.002/instance-hour
Lambda Network ActivityPer 1M Lambda invocations~$1.00/1M invocations
Malware Protection (EBS)Per GB of EBS volume scanned~$0.96/GB scanned
Malware Protection (S3)Per GB of S3 objects scanned~$0.96/GB scanned

Get Your Current Cost Estimate via CLI

# Get usage statistics and cost estimate for the current period
aws guardduty get-usage-statistics \
  --detector-id $DETECTOR_ID \
  --usage-statistics-type SUM_BY_DATA_SOURCE \
  --usage-criteria '{
    "DataSources": [
      "FLOW_LOGS",
      "CLOUD_TRAIL",
      "DNS_LOGS",
      "S3_LOGS",
      "KUBERNETES_AUDIT_LOGS",
      "EC2_MALWARE_SCAN"
    ]
  }' \
  --query 'UsageStatistics.SumByDataSource[*].{DataSource:DataSource,EstimatedCost:Total.Amount,Currency:Total.Unit}'

Cost Optimization Strategies

  1. Scope S3 Data Events to sensitive buckets only. Instead of enabling S3 Data Events for all buckets, list only buckets that hold PII, secrets, or regulated data. This can reduce S3 data event costs by 80–90% in accounts with many S3 buckets.
  2. Disable EKS Runtime Monitoring on dev/test clusters. Runtime monitoring runs a DaemonSet agent on every EKS node. Use resource tag-based scoping to enable it only on production clusters.
  3. Use GuardDuty in the management account to aggregate findings rather than enabling full data sources in every member account. Management event monitoring is already org-wide via CloudTrail; avoid redundant analysis.
  4. Disable Lambda Network Activity in accounts with high-volume Lambda workloads if your Lambda functions are well-understood. Re-enable it for accounts where Lambda handles user-supplied data or external integrations.
  5. Set up a cost anomaly alert. Create an AWS Cost Anomaly Detection monitor for the GuardDuty service — a sudden spike in Malware Protection or S3 Data Events costs often indicates a misconfigured data source scope.
# Disable a specific data source to reduce cost (example: Lambda Network Activity)
aws guardduty update-detector \
  --detector-id $DETECTOR_ID \
  --features '[{
    "Name": "LAMBDA_NETWORK_LOGS",
    "Status": "DISABLED"
  }]'

# Scope EKS Runtime Monitoring to production clusters only
aws guardduty update-detector \
  --detector-id $DETECTOR_ID \
  --features '[{
    "Name": "EKS_RUNTIME_MONITORING",
    "Status": "ENABLED",
    "AdditionalConfiguration": [{
      "Name": "EKS_ADDON_MANAGEMENT",
      "Status": "ENABLED"
    }]
  }]'
30-day trial best practice: Enable all data sources on day 1 of your trial. Let the cost estimate mature for 2–3 weeks to get an accurate steady-state projection. Then disable data sources where the cost does not justify the coverage for your threat model. This gives you an empirical basis for budget justification rather than guessing.

Frequently Asked Questions

Does GuardDuty require me to enable VPC Flow Logs or CloudTrail?

No. GuardDuty has its own mechanism to access the raw data from VPC Flow Logs, CloudTrail, and DNS Resolver — independent of whether you have these enabled for your own logging purposes. You do not pay for GuardDuty's copy of these logs. However, enabling CloudTrail in your account is still recommended for operational logging independent of GuardDuty.

What is the difference between GuardDuty suppression rules and Security Hub suppression rules?

GuardDuty suppression rules (called "filters" in the API) archive findings before they leave GuardDuty. They prevent the finding from appearing in Security Hub, triggering EventBridge rules, or being exported to S3. Security Hub suppression rules (automation rules) archive findings after they arrive in Security Hub — they still triggered EventBridge in GuardDuty. For high-volume false positives, use GuardDuty-level suppression to avoid downstream processing costs.

How long does GuardDuty retain findings?

GuardDuty retains active findings for 90 days. Archived/suppressed findings are retained for 90 days from the time they were last updated. To retain findings longer, export them to S3 via the GuardDuty findings export feature or forward them to Security Hub (which has its own 90-day retention by default, extendable via S3 export).

Can GuardDuty detect insider threats?

Partially. GuardDuty's behavioral baselines can detect anomalous IAM usage by legitimate users — a developer accessing production secrets they've never touched before, or an admin calling APIs in an unusual region. However, for comprehensive insider threat detection you need a more complete UEBA (User and Entity Behavior Analytics) solution. GuardDuty's strength is detecting external attack patterns and compromised credentials, not subtle policy violations by authorized users.

What happens if someone disables GuardDuty?

GuardDuty generates a Stealth:IAMUser/PasswordPolicyChange or similar stealth finding before being disabled — but since GuardDuty is what generates the finding, once it's disabled no further findings are generated. The defense is to use AWS Organizations to prevent member accounts from disabling GuardDuty using an SCP (Service Control Policy): deny guardduty:DeleteDetector and guardduty:DisassociateFromMasterAccount for all IAM principals except the security admin role.

{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "DenyGuardDutyDisable",
    "Effect": "Deny",
    "Action": [
      "guardduty:DeleteDetector",
      "guardduty:DisassociateFromMasterAccount",
      "guardduty:StopMonitoringMembers",
      "guardduty:UpdateDetector"
    ],
    "Resource": "*",
    "Condition": {
      "StringNotLike": {
        "aws:PrincipalARN": "arn:aws:iam::*:role/SecurityAdminRole"
      }
    }
  }]
}