Introduction
Since I released the blog series How to implement the AWS Startup Security Baseline (SSB) using Terraform recently, I've received some feedback and questions on it. In particular, there were some questions around setting up GuardDuty in an organization using Terraform. Since the configuration involves multiple accounts and there are some quirks with the resources, I decided to write a separate blog post on how to properly implement it with explanation on each step.
About the use case
Amazon GuardDuty is a managed threat detection service that continuously monitors AWS accounts and workloads for malicious or unauthorized activity using machine learning, anomaly detection, and integrated threat intelligence.
GuardDuty supports managing multiple accounts with AWS Organizations via the delegated administrator feature, with which you designate an AWS account in the organization to centrally manage GuardDuty for all members. This is great for managing a multi-account landing zone by centralizing management of GuardDuty settings in a consistent manner.
Since it is increasingly common to establish an AWS landing zone using AWS Control Tower, we will use the standard account structure in a Control Tower landing zone to demonstrate how to configure GuardDuty in Terraform:
The relevant accounts for our use case in the landing zone are:
The Management account for the organization where AWS Organizations is configured. For details, refer to Managing GuardDuty accounts with AWS Organizations.
The Audit account where security and compliance services are typically centralized in a Control Tower landing zone.
The objective is to delegate GuardDuty administrative duties from the Management account to the Audit account, after which all organization configurations are managed in the Audit account. With that said, let's see how we can achieve this using Terraform!
Designating a GuardDuty administrator account
GuardDuty delegated administrator is configured in the Management account, so we need a provider associated with it in Terraform. To keep things simple, we will take a multi-provider approach by defining two providers, one for the Management account and another for the Audit account, using AWS CLI profiles as follows:
provider "aws" {
alias = "management"
# Use "aws configure" to create the "management" profile with the Management account credentials
profile = "management"
}
provider "aws" {
alias = "audit"
# Use "aws configure" to create the "audit" profile with the Audit account credentials
profile = "audit"
}
⚠ Since GuardDuty is a regional service, you must apply this Terraform configuration on each region that you are using. Consider using the
region
argument in your provider definition and a variable to make your Terraform configuration rerunnable in other regions.
We can then use the aws_guardduty_organization_admin_account
resource to set the delegated administrator. However, I noticed the following in the Audit account:
After this resource is created, GuardDuty will be enabled with both the foundational data sources and all protection plans enabled.
When the resource is deleted, GuardDuty remains enabled.
These side effects are not desirable since we would ideally want full control over the lifecycle and configuration of GuardDuty in Terraform. To address this issue, we will preemptively enable GuardDuty in the Audit account using the aws_guardduty_detector
resource. We will also manage the protection plans using the aws_guardduty_detector_feature
resource in subsequent steps after we define the org-wide settings.
The resulting Terraform configuration should be defined as follows (pay special attention to the provider
argument in each resource):
data "aws_caller_identity" "audit" {
provider = aws.audit
}
resource "aws_guardduty_detector" "audit" {
provider = aws.audit
}
resource "aws_guardduty_organization_admin_account" "this" {
provider = aws.management
admin_account_id = data.aws_caller_identity.audit.account_id
depends_on = [aws_guardduty_detector.audit]
}
With the Audit account designated as the GuardDuty administrator, we can now manage the organization configuration.
Configuring organization auto-enable preferences
GuardDuty distinguishes the foundational data sources settings from the protection plans settings. The former is managed using the aws_guardduty_organization_configuration
resource. In our case, we want to manage GuardDuty for all accounts (i.e. both new and existing accounts). The resulting Terraform configuration should thus look like the following:
resource "aws_guardduty_organization_configuration" "this" {
provider = aws.audit
auto_enable_organization_members = "ALL"
detector_id = aws_guardduty_detector.audit.id
depends_on = [aws_guardduty_organization_admin_account.this]
}
Next, let's manage the protection plan configuration. For illustration, let's assume that we only want to enable only EKS Audit Log Monitoring. To ensure full configurability, we will define the setting for all protection plans using a variable:
# Terraform configuration (.tf)
variable "guardduty_features" {
description = "An object map that defines the GuardDuty organization configuration."
type = map(object({
auto_enable = string
name = string
additional_configuration = optional(list(object({
auto_enable = string
name = string
})))
}))
}
# Variable definition (.tfvars)
guardduty_features = {
s3 = {
auto_enable = "NONE"
name = "S3_DATA_EVENTS"
}
eks = {
auto_enable = "ALL"
name = "EKS_AUDIT_LOGS"
}
eks_runtime_monitoring = {
# EKS_RUNTIME_MONITORING is deprecated and should thus be explicitly disabled
auto_enable = "NONE"
name = "EKS_RUNTIME_MONITORING"
additional_configuration = [
{
auto_enable = "NONE"
name = "EKS_ADDON_MANAGEMENT"
},
]
}
runtime_monitoring = {
auto_enable = "NONE"
name = "RUNTIME_MONITORING"
additional_configuration = [
{
auto_enable = "NONE"
name = "EKS_ADDON_MANAGEMENT"
},
{
auto_enable = "NONE"
name = "ECS_FARGATE_AGENT_MANAGEMENT"
},
{
auto_enable = "NONE"
name = "EC2_AGENT_MANAGEMENT"
}
]
}
malware = {
auto_enable = "NONE"
name = "EBS_MALWARE_PROTECTION"
}
rds = {
auto_enable = "NONE"
name = "RDS_LOGIN_EVENTS"
}
lambda = {
auto_enable = "NONE"
name = "LAMBDA_NETWORK_LOGS"
}
}
⚠ The
EKS_RUNTIME_MONITORING
feature has been superseded by theRUNTIME_MONITORING
feature, but to avoid perpetual differences in Terraform configuration, we must set its enablement state toNONE
.
We can then use this variable with the for_each
meta-argument with the aws_guardduty_organization_configuration_feature
resource as follows:
resource "aws_guardduty_organization_configuration_feature" "this" {
provider = aws.audit
for_each = var.guardduty_features
auto_enable = each.value.auto_enable
detector_id = aws_guardduty_detector.audit.id
name = each.value.name
dynamic "additional_configuration" {
for_each = try(each.value.additional_configuration, [])
content {
auto_enable = additional_configuration.value.auto_enable
name = additional_configuration.value.name
}
}
depends_on = [aws_guardduty_organization_admin_account.this]
}
Lastly, we will circle back to re-celebrating the protection plan settings for the Audit account itself. Let's piggyback on the same variable and use the aws_guardduty_detector_feature
resource to achieve this:
resource "aws_guardduty_detector_feature" "audit" {
provider = aws.audit
for_each = var.guardduty_features
detector_id = aws_guardduty_detector.audit.id
name = each.value.name
status = each.value.auto_enable == "NONE" ? "DISABLED" : "ENABLED"
dynamic "additional_configuration" {
for_each = try(each.value.additional_configuration, [])
content {
status = additional_configuration.value.auto_enable == "NONE" ? "DISABLED" : "ENABLED"
name = additional_configuration.value.name
}
}
}
✅ You can find the complete Terraform in the GitHub repository that accompanies this blog post.
With the complete Terraform configuration, you can now apply it to establish the Audit account as the delegated administrator and apply organization settings to all accounts in the target region. Note that it will take up to 24 hours for GuardDuty to automatically enable it in all accounts. YMMV, but it took about 3 hours in the evening in the Eastern time zone.
⚠ There is currently an issue where the
additional_configuration
block order causes differences when applying the Terraform configuration without making any changes.
Caveats about suspending GuardDuty in member accounts
Due to limitations with the GuardDuty Terraform resources, GuardDuty is unfortunately not automatically disabled when you run terraform destroy
. Normally this wouldn't be a problem for a production landing zone. However, if you are only testing, this could lead to unexpected costs especially when GuardDuty is a somewhat costly service.
As a workaround, I would recommend using the AWS CLI or AWS SDK to at least suspend GuardDuty for all members using the StopMonitoringMembers
API. For your convenience, you can use the following shell script to do so before running terraform destroy
:
#!/bin/bash
# Note: Make sure that you set the AWS_PROFILE environment variable to "audit" before running the script
# Get the GuardDuty detector ID
DETECTOR_ID=$(aws guardduty list-detectors --query DetectorIds[0] --output text)
# Disable auto-enable organization members
aws guardduty update-organization-configuration --detector-id $DETECTOR_ID --auto-enable-organization-member NONE
# Loop through each member account and disable GuardDuty
MEMBER_ACCOUNTS=$(aws guardduty list-members --detector-id $DETECTOR_ID --query Members[*].AccountId --output text)
for MEMBER_ACCOUNT in $MEMBER_ACCOUNTS
do
echo "Suspending GuardDuty for account $MEMBER_ACCOUNT"
aws guardduty stop-monitoring-members --account-ids $MEMBER_ACCOUNT --detector-id $DETECTOR_ID
done
Summary
In this blog post, you learned how to manage Amazon GuardDuty in AWS Organizations using Terraform. While there are some caveats, this allows you to streamline the setup of a security baseline for your AWS landing zone. The centralized approach to detective security can help you ensure compliance and timely reaction to security incidents.
I hope you found this blog post helpful. If you are interested in this type of content, be sure to check out other blog posts in the Avangards Blog. Thank you and have a great one!