Building near-real time automatic remediation for disabled S3 Block Public Access with serverless tools

Security Hub regularly creates findings on various controls and assigns them different priorities. We can create automation to remedy these issues using EventBridge rules and Lambda functions.

1. The problem

AWS Security Hub is a one-place-to-go service that collects security findings from other services, like GuardDuty, Macie or IAM Access Analyzer. It also has its own controls that come from security standards like CIS AWS Foundations Benchmark or PCI DSS. We can enable one or more of these standards, and Security Hub will start checking for the status of the controls that belong to the given standards using AWS Config in the background.

It’s not only me who likes Security Hub, but Bob does, too! His latest task is to create an automated alert solution that gets triggered when someone has turned off the Block Public Access setting in any S3 buckets. Although Bob believes there’s hardly any good reason to make a bucket public today, he wants to evaluate the situation first. So he and his managers would like to receive an email notification near real-time when that happens. Who knows, there might be a legitimate reason for the Block Public Access setting to be disabled. Then, they can click a link to enable Block Public Access again if needed.

2. Solution overview

As said above, Security Hub keeps creating its findings on the controls and evaluates their compliance status.

Block Public Access automation architecture
Block Public Access automation architecture

Bob will create an EventBridge rule that matches the block public access finding event pattern from Security Hub. The rule will have a Lambda function target that extracts the bucket name from the event object.

The function then creates a URL with the bucket name in a query parameter and publishes a message with the link to an SNS topic. Bob and his managers’ email addresses are subscribers to the topic, and they will receive an email with the link to an API Gateway endpoint. A second Lambda function will enable the block public access setting on the given bucket when they click the link.

3. Pre-requisites

This post won’t explain how to

  • create EventBridge rules
  • enable Security Hub controls
  • create Lambda functions
  • create an API Gateway
  • create an SNS topic.

I’ll provide some links at the end of the post that will help provision these resources if needed.

4. Main steps

Let’s go over the main steps of this solution.

4.1. EventBridge rule

First, we’ll need to know when someone disables Block Public Access at the bucket level. The AWS Foundational Security Best Practices v1.0.0 standard in Security Hub has a control that monitors this setting.

Luckily, Security Hub evaluates the status of the controls and sends events to EventBridge. All we have to do is listen to the relevant event, and then we can build automation to fix any issues.

The event pattern we want to match in the rule can look like this:

{
  "source": ["aws.securityhub"],
  "detail-type": ["Security Hub Findings - Imported"],
  "detail": {
    "findings": {
      "ProductArn": ["arn:aws:securityhub:REGION::product/aws/securityhub"],
      "Title": ["S3 Block Public Access setting should be enabled at the bucket-level"],
      "Compliance": {
        "Status": ["FAILED"]
      },
      "RecordState": ["ACTIVE"]
    }
  }
}

The main properties are Title, Compliance and RecordsState.

We want Title in the filter since it is the Security Hub control we need to monitor.

Security Hub also sends events on compliant and archived (i.e., not active) findings too. So even if a bucket has Block Public Access enabled, Title alone would still produce a matching pattern. This way, we need to filter the pattern more. So we add Compliance and RecordState filters with FAILED and ACTIVE statuses, respectively.

4.2. Target function

The rule’s target in this example is a Lambda function. Its code can look like this:

import { EventBridgeEvent } from 'aws-lambda';
import { PublishCommand, PublishCommandInput, SNSClient } from '@aws-sdk/client-sns';

interface BucketResources {
  Type: 'AwsS3Bucket';
  Details: {
    AwsS3Bucket: {
      Name: string;
    };
  };
  // Some properties are left out
}

interface S3BlockPublicAccessSettingFinding {
  Resources: BucketResources[];
  // Other properties are left out
}
interface SecurityHubFindingDetail {
  findings: S3BlockPublicAccessSettingFinding[];
}

const { TOPIC_ARN, API_URL } = process.env;

const snsClient = new SNSClient();

export async function handler(event: EventBridgeEvent<'Security Hub Findings - Imported', SecurityHubFindingDetail>) {
  // 1. Get the bucket names from the findings
  const buckets = event.detail.findings.flatMap((finding) => {
    const bucketNames = finding.Resources.map((resource) => resource.Details.AwsS3Bucket.Name);
    return bucketNames;
  });

  const promises = buckets.map((bucket) => {
    // 2. Create the URL
    const url = `${API_URL}/block-public-access?bucket=${bucket}`;
    const publishInput: PublishCommandInput = {
      TopicArn: TOPIC_ARN,
      Subject: 'Block public access has been disabled!',
      Message: `Block public access is turned off on bucket ${bucket}. Click here to turn it back on: ${url}`,
    };
    const publishCommand = new PublishCommand(publishInput);
    return snsClient.send(publishCommand);
  });

  try {
    // 3. Publish the messages to SNS
    await Promise.all(promises);
  } catch (error) {
    console.log('ERROR', error);
  }
}

First, the code extracts the bucket names from the event object (1). Then, it creates a URL for each bucket where the bucket name is the value of the bucket query parameter (2). Finally, the function publishes a message for each bucket to an SNS topic (3). We pass the topic name, and the API Gateway invoke URL as environment variables to the handler.

4.3. Block public access function

At this point, when someone turns off the Block Public Access setting in our account, we should receive an email with a link that points to the API Gateway’s /block-public-access endpoint. The link will have a format similar to https://API_ID.execute-api.REGION.amazonaws.com/STAGE_NAME/block-public-access?bucket=BUCKET_NAME.

The last step is to enable Block Public Access on the bucket when someone clicks the link in the email.

We can perform this task with another Lambda function, which we configure as the integration behind the /block-public-access endpoint.

The function’s code can look like this:

import { APIGatewayProxyEventV2 } from 'aws-lambda';
import { S3Client, PutPublicAccessBlockCommand, PutPublicAccessBlockCommandInput } from '@aws-sdk/client-s3';

const s3Client = new S3Client();

export async function handler(event: APIGatewayProxyEventV2) {
  // 1. Get the bucket name from the URL
  const bucketName = event.queryStringParameters?.bucket;
  if (!bucketName) {
    return {
      statusCode: 400,
      body: 'No bucket name provided.',
    };
  }

  const publicAccessBlockInput: PutPublicAccessBlockCommandInput = {
    Bucket: bucketName,
    PublicAccessBlockConfiguration: {
      BlockPublicAcls: true,
      BlockPublicPolicy: true,
      IgnorePublicAcls: true,
      RestrictPublicBuckets: true,
    },
  };
  const command = new PutPublicAccessBlockCommand(publicAccessBlockInput);

  try {
    // 2. Enable Block Public Access
    await s3Client.send(command);
  } catch (error) {
    console.log('ERROR', error);
    return {
      statusCode: 500,
      body: 'An error occurred',
    };
  }

  return {
    statusCode: 200,
    body: `Public access has been blocked on bucket ${bucketName}`,
  };
}

First, we get the bucket name from the URL (1), then we call the PutPublicAccessBlock S3 API with the block public access settings using the TypeScript SDK (2). Ensure that the function’s execution role contains the s3:PubBucketPublicAccessBlock permission.

API Gateway will invoke the Lambda function, and now the bucket should have its Block Public Access setting enabled again!

5. Considerations

As always, the example presented in this post is not the only solution. We can solve the majority of any challenges in more than one way.

Other services also monitor the public access setting at the bucket level. We could use Macie, IAM Access Analyzer, GuardDuty or CloudTrail to create an automated solution. I used Security Hub in this example because it’s a central place to monitor findings from various security services.

We can also use Security Hub and EventBridge to create automation to resolve other non-compliant findings similarly.

6. Summary

Security Hub is a service that collects findings from other security services. It also creates findings based on the controls that belong to the security standards we choose to enable.

We can create automation by reacting to events Security Hub sends to EventBridge. Turning off the Block Public Access setting at the S3 bucket level is one such event that we can apply an automated remediation by using Lambda functions and API Gateway.

7. Further reading

Creating Amazon EventBridge rules that react to events - How to create EventBridge rules

Enabling and disabling controls in all standards - Enabling Security Hub controls

Getting started with Lambda - How to create a Lambda function

Creating a REST API in Amazon API Gateway - The title says it all

Creating an Amazon SNS topic - Same here