Building near real-time automatic remediation for disabled S3 Block Public Access with serverless tools
Tags: aws, security, serverless, eventdriven
Published: 2024-02-22 09:31:29

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

<a href="https://aws.amazon.com/security-hub/" class="link" target="_blank" rel="noopener noreferrer">AWS Security Hub</a> is a one-place-to-go service that collects security findings from other services, like <a href="https://aws.amazon.com/guardduty/" class="link" target="_blank" rel="noopener noreferrer">GuardDuty</a>, <a href="https://aws.amazon.com/macie/" class="link" target="_blank" rel="noopener noreferrer">Macie</a> or <a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/what-is-access-analyzer.html" class="link" target="_blank" rel="noopener noreferrer">IAM Access Analyzer</a>. It also has its own **controls** that come from security standards like <a href="https://docs.aws.amazon.com/securityhub/latest/userguide/cis-aws-foundations-benchmark.html" class="link" target="_blank" rel="noopener noreferrer">CIS AWS Foundations Benchmark</a> or <a href="https://docs.aws.amazon.com/securityhub/latest/userguide/pci-standard.html" class="link" target="_blank" rel="noopener noreferrer">PCI DSS</a>. 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 <a href="https://docs.aws.amazon.com/config/latest/developerguide/WhatIsConfig.html" class="link" target="_blank" rel="noopener noreferrer">AWS Config</a> 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 <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/creating-buckets-s3.html" class="link" target="_blank" rel="noopener noreferrer">S3 buckets</a>. 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7kdr0aiv8vnn3sfqt73h.png)

Bob will create an <a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-rules.html" class="link" target="_blank" rel="noopener noreferrer">EventBridge</a> **rule** that matches the block public access finding event pattern from Security Hub. The rule will have a <a href="https://aws.amazon.com/lambda" class="link" target="_blank" rel="noopener noreferrer">Lambda</a> 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 <a href="https://aws.amazon.com/sns/" class="link" target="_blank" rel="noopener noreferrer">SNS</a> topic. Bob and his managers' email addresses are subscribers to the topic, and they will receive an email with the link to an <a href="https://aws.amazon.com/api-gateway/" class="link" target="_blank" rel="noopener noreferrer">API Gateway</a> 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

<ul>
  <li>create EventBridge rules</li>
  <li>enable Security Hub controls</li>
  <li>create Lambda functions</li>
  <li>create an API Gateway</li>
  <li>create an SNS topic.</li>
</ul>

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:

```json
{
  "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:

```ts
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:

```ts
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 <a href="https://www.typescriptlang.org/" class="link" target="_blank" rel="noopener noreferrer">TypeScript</a> 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

<a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-create-rule.html" class="link" target="_blank" rel="noopener noreferrer">Creating Amazon EventBridge rules that react to events</a> - How to create EventBridge rules

<a href="https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-enable-disable-controls.html" class="link" target="_blank" rel="noopener noreferrer">Enabling and disabling controls in all standards</a> - Enabling Security Hub controls

<a href="https://docs.aws.amazon.com/lambda/latest/dg/getting-started.html" class="link" target="_blank" rel="noopener noreferrer">Getting started with Lambda</a> - How to create a Lambda function

<a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-create-api.html" class="link" target="_blank" rel="noopener noreferrer">Creating a REST API in Amazon API Gateway</a> - The title says it all

<a href="https://docs.aws.amazon.com/sns/latest/dg/sns-create-topic.html" class="link" target="_blank" rel="noopener noreferrer">Creating an Amazon SNS topic</a> - Same here