How to enable AWS GuardDuty in a multi-account environment using Cloudformation ?

During the development of a cloud native application running on Kubernetes we we’re asked to perform implement a security mechanism that pro-actively scans the complete cloud native infrastructure for security threads.

We came up with the idea to implement AWS GuardDuty since is a threat detection service that continuously monitors for malicious activity and unauthorized behavior to protect your AWS accounts and workloads.

Because we have a multi-account strategy setting the whole thing up from an infra-as-code perspective appeared a bit challenging since documentation was not always on-par.

So basically the first thing to understand it that you need to promote 1 account to be the AWS GuardDuty Master and invite numerous accounts to become members. This can be done through the console but we off course want scripts.

So first we define the Master:

  ## Adds the detector and activates the Master account
    Type: AWS::GuardDuty::Detector
      Enable: True
      FindingPublishingFrequency: SIX_HOURS #default

Then we can invite the numerous members of our centralized GuardDuty implementation. What is remarkable here is that you do off course need the AWS account ID, but instead of the usual IAM Role cross-account authentication you need to verify you are familiar with the account by defining the root account it’s email-address:

    Type: AWS::GuardDuty::Member
      DetectorId: !Ref Detector
      DisableEmailNotification: true
      MemberId: 1234567890
      Message: "You are invited to enable Amazon Guardduty."
      Status: Invited

So when logging in to the AWS console of the member account(s) and check the GuardDuty – Accounts menu we are able to accept the invitation.

How to enable AWS GuardDuty in a multi-account environment using Cloudformation ?


Since monitoring is good, but proper alerting is even better we decided to implement a Cloudwatch Event and an SNS Topic to alert. So first we create the SNS Topic that in below example basically sends an e-mail.

    Type: AWS::SNS::Topic
        - Endpoint:
          Protocol: email         
      TopicName: "GuardDutyFinding"  

Then we define the Cloudwatch Event rule that will be triggered on any GuardDuty Finding. In below example we filter on the severity so that only Medium and High GuardDuty findings will trigger the alert.

    Type: AWS::Events::Rule
      Description: "GuardDutyFindingEventRule"
          - "aws.guardduty"      
          - "GuardDuty Finding"
          # high=[7 until 8.9]
          # medium=[4 until 6.9]   
          severity: [
            4, 4.0, 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7, 4.8, 4.9, 
            5, 5.0, 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 5.7, 5.8, 5.9, 
            6, 6.0, 6.1, 6.2, 6.3, 6.4, 6.5, 6.6, 6.7, 6.8, 6.9, 
            7, 7.0, 7.1, 7.2, 7.3, 7.4, 7.5, 7.6, 7.7, 7.8, 7.9, 
            8, 8.0, 8.1, 8.2, 8.3, 8.4, 8.5, 8.6, 8.7, 8.8, 8.9
      State: "ENABLED"
            Ref: "GuardDutyFindingTopic"
          Id: "GuardDutyFindingTopic"

Trusted IP

As soon as we had GuardDuty running we got some false positives on a corporate security scanner that scans all our VPC’s. So naturally we tried to whitelist this probe to prevent all the Low findings of port scans.

So first we tried with the AWS::GuardDuty::IPSet option. Which basically allows you to create a file with trusted IP’s, store them in a S3 bucket and let GuardDuty use it. This does sound a bit like an over-engineered hassle and it is.

How to enable AWS GuardDuty in a multi-account environment using Cloudformation ?

    Type: AWS::GuardDuty::IPSet
        Activate: True
        DetectorId: "12abc34d567e8f4912ab3d45e67891f2"
        Format: "TXT"
        Location: ""
        Name: "MyIPSet"

But sadly after doing this, it did not work. So eventually this thread on the AWS Forum answered the question why.

The Trusted IP list will not accept RFC1918 addresses, this is due to the ephemeral nature of IP addresses in VPCs. For your use case where you’d like to suppress findings from your ┬áscanner you should use the Filter and Auto-Archive feature to create a filter that is defined by the AMI-ID of the scanner, you can also scope the suppression down even further by specifying the finding types you expect.

Our probe is running in our VPC and therefor has a (RFC1918) address. So this will not work and we started looking at the AWS GuardDuty filter finding  documentation we got it working by adding Suppression Rules filter so the findings were automatically archived.

    Type: AWS::GuardDuty::Filter
      Action: ARCHIVE
      Description: ProbeSuppressFilter
      DetectorId: !Ref Detector
      Rank : 2
      Name : ProbeSuppressFilter

Hope it helps.