# How To Manage Amazon GuardDuty in AWS Organizations Using Terraform

## Introduction

Since I released the blog series [How to implement the AWS Startup Security Baseline (SSB) using Terraform](https://blog.avangards.io/series/aws-ssb-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](https://docs.aws.amazon.com/guardduty/latest/ug/what-is-guardduty.html) 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](https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_organizations.html) 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](https://docs.aws.amazon.com/controltower/latest/userguide/what-is-control-tower.html), we will use the [standard account structure](https://docs.aws.amazon.com/controltower/latest/userguide/accounts.html) in a Control Tower landing zone to demonstrate how to configure GuardDuty in Terraform:

![Control Tower standard OU and account structure](https://cdn.hashnode.com/res/hashnode/image/upload/v1713858762060/1346a5a6-bdfc-426b-bf3c-562570b155b3.png align="center")

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](https://docs.aws.amazon.com/organizations/latest/userguide/services-that-can-integrate-guardduty.html).
    
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:

```dockerfile
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" 
}
```

<div data-node-type="callout">
<div data-node-type="callout-emoji">⚠</div>
<div data-node-type="callout-text">Since GuardDuty is a regional service, you must apply this Terraform configuration on each region that you are using. Consider using the <code>region</code> argument in your provider definition and a variable to make your Terraform configuration rerunnable in other regions.</div>
</div>

We can then use the [`aws_guardduty_organization_admin_account` resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/guardduty_organization_admin_account) 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](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/guardduty_detector). We will also manage the protection plans using the [`aws_guardduty_detector_feature` resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/guardduty_detector_feature) 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):

```dockerfile
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](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/guardduty_organization_configuration). 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:

```dockerfile
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](https://docs.aws.amazon.com/guardduty/latest/ug/guardduty-eks-audit-log-monitoring.html). To ensure full configurability, we will define the setting for all protection plans using a variable:

```dockerfile
# 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
    })))
  }))
}
```

```dockerfile
# 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"
  }
}
```

<div data-node-type="callout">
<div data-node-type="callout-emoji">⚠</div>
<div data-node-type="callout-text">The <code>EKS_RUNTIME_MONITORING</code> feature has been superseded by the <code>RUNTIME_MONITORING</code> feature, but to avoid perpetual differences in Terraform configuration, we must set its enablement state to <code>NONE</code>.</div>
</div>

We can then use this variable with the [`for_each` meta-argument](https://developer.hashicorp.com/terraform/language/meta-arguments/for_each) with [the `aws_guardduty_organization_configuration_feature` resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/guardduty_organization_configuration_feature) as follows:

```dockerfile
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 recalibrating the protection plan settings for the **Audit** account itself. Let's piggyback on the same variable and use [the `aws_guardduty_detector_feature` resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/guardduty_detector_feature) to achieve this:

```dockerfile
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
    }
  }
}
```

<div data-node-type="callout">
<div data-node-type="callout-emoji">✅</div>
<div data-node-type="callout-text">You can find the complete Terraform in the <a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/acwwat/terraform-aws-guardduty-organization-example" style="pointer-events: none">GitHub repository</a> that accompanies this blog post.</div>
</div>

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](https://docs.aws.amazon.com/guardduty/latest/APIReference/API_UpdateOrganizationConfiguration.html#guardduty-UpdateOrganizationConfiguration-request-autoEnableOrganizationMembers) for GuardDuty to automatically enable it in all accounts. YMMV, but it took about 3 hours in the evening in the Eastern time zone.

<div data-node-type="callout">
<div data-node-type="callout-emoji">⚠</div>
<div data-node-type="callout-text">There is currently an <a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/hashicorp/terraform-provider-aws/issues/36400" style="pointer-events: none">issue</a> where the <code>additional_configuration</code> block order causes differences when applying the Terraform configuration without making any changes.</div>
</div>

## 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](https://docs.aws.amazon.com/guardduty/latest/APIReference/API_StopMonitoringMembers.html). For your convenience, you can use the following shell script to do so before running `terraform destroy`:

```bash
#!/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](https://blog.avangards.io). Thank you and have a great one!
