How To Manage Amazon GuardDuty in AWS Organizations Using Terraform

Anthony Wat - Apr 23 - - Dev Community

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:

Control Tower standard OU and account structure

The relevant accounts for our use case in the landing zone are:

  1. The Management account for the organization where AWS Organizations is configured. For details, refer to Managing GuardDuty accounts with AWS Organizations.

  2. 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" 
}
Enter fullscreen mode Exit fullscreen mode

⚠ 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]
}
Enter fullscreen mode Exit fullscreen mode

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]
}
Enter fullscreen mode Exit fullscreen mode

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
    })))
  }))
}
Enter fullscreen mode Exit fullscreen mode
# 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"
  }
}
Enter fullscreen mode Exit fullscreen mode

⚠ The EKS_RUNTIME_MONITORING feature has been superseded by the RUNTIME_MONITORING feature, but to avoid perpetual differences in Terraform configuration, we must set its enablement state to NONE.

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]
}
Enter fullscreen mode Exit fullscreen mode

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
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

✅ 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
Enter fullscreen mode Exit fullscreen mode

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!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .