Automated integration between Google Threat Intelligence (GTI) and AWS GuardDuty for continuous threat list ingestion.
This solution deploys a Lambda function that:
- Fetches IP addresses and domains from GTI APIs using GTI IOC Stream API and GTI Categorized Threat Lists API
- Stores IOCs in S3-backed threat list files
- Registers and activates ThreatIntelSets (IPs) and ThreatEntitySets (domains) in GuardDuty
- Maintains incremental state via checkpointing
- Runs on a configurable EventBridge schedule
- AWS CLI configured with appropriate permissions
- Valid Google Threat Intelligence API key
- GuardDuty enabled in target region (or permissions to create detector)
aws cloudformation deploy \
--template-file cloud-formation.yaml \
--stack-name gti-guardduty-integration \
--capabilities CAPABILITY_NAMED_IAM \
--parameter-overrides \
SecretValue=<YOUR_GTI_API_KEY>- Navigate to CloudFormation → Create stack
- Upload
cloud-formation.yaml - Set stack name (e.g.,
gti-guardduty-integration) - Enter
SecretValue(your GTI API key) - Acknowledge IAM resource creation
- Submit and wait for
CREATE_COMPLETE
Note: Initial deployment takes several minutes due to Lambda layer creation and custom resource execution.
| Parameter | Description |
|---|---|
SecretValue |
Google Threat Intelligence (GTI) API key |
| Parameter | Default | Description |
|---|---|---|
BucketName |
gti-guard-duty-integration |
S3 bucket name to create (must be globally unique) |
SecretKey |
GTI_API_KEY |
Secrets Manager secret name to create (must be globally unique) |
ScheduleExpression |
cron(0 * * * ? *) |
EventBridge cron expression for Lambda schedule |
| Parameter | Default | Description |
|---|---|---|
SeverityFilter |
VERDICT_MALICIOUS |
Comma-separated GTI verdict levels |
ThreatLists |
(empty) | Comma-separated GTI threat list categories (empty for all) |
LookbackDays |
5 |
Historical lookback period in days for initial sync (max 5) |
⚠️ Note: GuardDuty has a limit of 1,000 domains per threat list file with a maximum of 6 files (6,000 domains total). It is strongly recommended to:
- Specify the
ThreatListsparameter to filter only the threat categories relevant to your use case, rather than syncing all categories- Keep
LookbackDaysto a minimum necessary value to reduce the initial volume of IOCs fetchedThis helps stay within GuardDuty's limits and improves performance.
Severity Filter Options: VERDICT_MALICIOUS, VERDICT_SUSPICIOUS, VERDICT_UNDETECTED, VERDICT_BENIGN, VERDICT_UNKNOWN
| Parameter | Default | Description |
|---|---|---|
EntityFiles |
guard_duty/threat-entity-{1-6}.txt |
S3 object keys for domain threat list files (auto-created) |
IpFiles |
guard_duty/threat-ip-{1-6}.txt |
S3 object keys for IP threat list files (auto-created) |
- EventBridge triggers Lambda on schedule (default: every hour)
- Lambda retrieves GTI API key from Secrets Manager
- Lambda fetches IOCs from GTI IOC Stream and Threat Lists APIs
- Valid IOCs are written to S3 threat list files
- Updated threat lists are activated in GuardDuty
- Checkpoint is saved to S3 for incremental processing
| Resource | Type | Purpose |
|---|---|---|
GTISecret |
Secrets Manager Secret | Stores GTI API key |
IntegrationBucket |
S3 Bucket | Threat lists, checkpoint, Lambda layer |
BucketPolicy |
S3 Bucket Policy | Grants access to Lambda and GuardDuty |
LambdaRole |
IAM Role | Execution role for main Lambda |
DetectorCustomRole |
IAM Role | Role for detector custom resource |
GTILambda |
Lambda Function | Main IOC sync function |
LayerZipCreator |
Lambda Function | Builds boto3 layer at deploy time |
DetectorCustomFunction |
Lambda Function | Gets/creates GuardDuty detector |
ObjectsCustomFunction |
Lambda Function | Creates empty threat files and registers GuardDuty sets |
Boto3LambdaLayer |
Lambda Layer | boto3 ≥1.42 for ThreatEntitySet support |
ScheduledRule |
EventBridge Rule | Triggers Lambda on schedule |
*LogGroup |
CloudWatch Log Groups | 30-day retention for all Lambdas |
- Endpoint: GTI IOC Stream API
- Entity types:
ip_address,domain - Pagination: Cursor-based with 40 items per page
- Checkpointing: Tracks
notification_dateper entity type
- Endpoint: GTI Threat Lists API
- Granularity: Hourly buckets (format:
YYYYMMDDHH) - Pagination: Up to 4,000 IOCs per request
- Checkpointing: Tracks last processed hour per category
- Load checkpoint from S3 (
gti-checkpoint.json) - Validate required environment variables (
ENTITY_FILES,IP_FILES) - Initialize GTI client with API key from Secrets Manager
- Fetch domain IOCs from IOC Stream API
- Fetch IP IOCs from IOC Stream API
- Fetch IOCs from configured threat list categories
- Write invalid IOCs to S3 for review
- Activate updated GuardDuty threat lists
The checkpoint file tracks:
{
"domain_last_notification_date": 1705766400,
"ip_address_last_notification_date": 1705766400,
"threat_lists": {
"category_name": "2024012011"
}
}- IOC Stream: Unix timestamp of last processed notification
- Threat Lists: Hourly timestamp (
YYYYMMDDHH) per category
- On first run (no checkpoint): fetches from
now - LookbackDays - On subsequent runs: fetches from last checkpoint timestamp
- Maximum lookback enforced: 5 days
- IPs: Must be valid IPv4 address or CIDR notation
- Domains: Must match domain regex; URLs rejected; wildcards supported (
*.example.com)
| Constraint | Limit | Implementation |
|---|---|---|
| Threat lists per type | 6 | Enforced; excess IOCs written to exceeded_*.txt |
| File size | 35 MB | Chunked writes |
| IPs per file | 250,000 | MAX_IP_LINES_PER_FILE constant |
| Domains per file | 1,000 | MAX_ENTITY_LINES_PER_FILE constant |
s3://<bucket>/
├── guard_duty/
│ ├── threat-ip-1.txt # IP IOCs (ThreatIntelSet)
│ ├── threat-ip-2.txt
│ ├── ...
│ ├── threat-entity-1.txt # Domain IOCs (ThreatEntitySet)
│ ├── threat-entity-2.txt
│ └── ...
├── invalid_iocs/
│ ├── invalid_ip_address.txt # Failed IP validation
│ └── invalid_domain.txt # Failed domain validation
├── exceeded_ip_address.txt # Overflow IPs (if >6 files needed)
├── exceeded_domain.txt # Overflow domains (if >6 files needed)
├── gti-checkpoint.json # Incremental sync state
└── boto3-layer.zip # Lambda layer artifact
All Lambda functions log to CloudWatch with 30-day retention:
/aws/lambda/<bucket>-gti-ingest— Main sync function/aws/lambda/<bucket>-layer-creator— Layer builder/aws/lambda/DetectorCustomFunction— Detector lookup/aws/lambda/ObjectsCustomFunction— File/set creation
| Prefix | Meaning |
|---|---|
[INFO] |
Normal operation |
[WARN] |
Non-fatal issues (missing files, invalid config) |
[ERROR] |
Failures requiring attention |
[DEBUG] |
Pagination and fetch details |
[INFO] ===== GTI IOC Sync Lambda Starting =====
[INFO] Using lookback days: 5
[INFO] Fetching IOC Stream (domain) | last_date=1705766400
[DEBUG] Fetched page with 40 items | next_cursor=...
[INFO] Updated S3 file: guard_duty/threat-entity-1.txt | Records: 150 | Size: 0.02 MB
[INFO] Activating threat ENTITY list: s3://bucket/guard_duty/threat-entity-1.txt
[INFO] ✔ IOC Sync Completed Successfully
- GTI API key stored in Secrets Manager (not environment variables)
- Lambda retrieves secret at runtime via
secretsmanager:GetSecretValue
- Public access blocked (all four settings enabled)
- Bucket policy restricts access to Lambda and GuardDuty services
The Lambda role includes least-privilege policies:
secretsmanager:GetSecretValue— Scoped to GTI secret ARNs3:GetObject,s3:PutObject,s3:ListBucket— Scoped to integration bucketguardduty:*ThreatIntelSet,guardduty:*ThreatEntitySet— For threat list managementguardduty:ListDetectors— To discover detector ID
- Lambda makes outbound HTTPS calls to Google Threat Intelligence APIs
- No VPC configuration required (uses AWS public endpoints)
Symptom: Task timed out after 900.00 seconds
Cause: Large historical fetch on first run or many threat list categories
Resolution:
- Reduce
LookbackDaysto 1-3 - Limit categories via
ThreatListsparameter - Lambda will resume from checkpoint on next run
Symptom: HTTP 401 or HTTP 403 or API key not found
Resolution:
- Verify secret exists in Secrets Manager with correct name
- Confirm API key is valid in Google Threat Intelligence console
- Check
SecretKeyparameter matches secret name
Symptom: No GuardDuty detector found in the region
Resolution:
- Enable GuardDuty in the AWS Console
- Verify IAM role has
guardduty:ListDetectorspermission - Re-run the stack (custom resource will create detector)
Symptom: Lists remain INACTIVE in GuardDuty
Cause: No new IOCs written during execution
Resolution: This is expected behavior. Lists activate only when files are updated.
Symptom: Exceeded max GuardDuty ip_address files in logs
Cause: More IOCs than can fit in 6 files
Resolution:
- Check
exceeded_ip_address.txtorexceeded_domain.txtin S3 - Tighten
SeverityFilterto reduce volume - Limit threat list categories
Symptom: Some IOCs not appearing in threat lists
Cause: Failed validation (non-IPv4, URLs, malformed domains)
Resolution:
- Review
invalid_iocs/invalid_ip_address.txtandinvalid_iocs/invalid_domain.txt - This is expected behavior; invalid IOCs are logged and skipped
Symptom: Stack stuck in CREATE_FAILED
Resolution:
- Check CloudFormation → Events for failing resource
- Review corresponding Lambda logs
- Common causes: IAM permissions, duplicate bucket/secret names
| Output | Description |
|---|---|
S3Bucket |
Integration bucket name |
SecretName |
Secrets Manager secret name |
LambdaFunction |
Main Lambda function ARN |
DetectorId |
GuardDuty detector ID |