Programmatically reacting to S3 bucket external access exposures
Tags: aws, security, serverless, s3
Published: 2023-08-19 08:15:47

We can easily create a serverless architecture that lets us know when someone makes a resource externally available. The solution will notify us within seconds after someone has modified the resource-based policy to provide external access.

## 1. Problem statement

Example Corp. has a multi-account structure, where some accounts contain shared resources.

Alice is a system administrator, and one of her tasks is to monitor if the resource and identity policies follow the **least privilege principle**. That is, only entities that need access to a service receive the minimum necessary permission.

Because shared resources need to allow external access, some policies must contain statements that permit it. But how will Alice know that the external access is legitimate or someone is trying to compromise the resource?

## 2. Access Analyzer

<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> can help us discover which resources have policies attached that provide external access.

The tool is part of <a href="https://aws.amazon.com/iam/" class="link" target="_blank" rel="noopener noreferrer">IAM</a>. First, we must create an **analyzer**, which can be account- or <a href="https://docs.aws.amazon.com/organizations/latest/userguide/orgs_introduction.html" class="link" target="_blank" rel="noopener noreferrer">organization</a>-based. The account or the organization will become the **zone of trust**. In this example, the zone of trust will be an account.

Access Analyzer uses an internal service called **Zelkova** that decides if a **resource-based** policy allows access from a source outside the zone of trust. Zelkova applies mathematics and automated reasoning to make sophisticated decisions. It translates the resource policies into mathematical language and compares their content to its internal best-practice permissions. When the resource policy is more permissive than Zelkova's baseline, Access Analyzer creates a finding.

It's not a reactive tool because it doesn't analyze logs or API calls. It doesn't monitor if someone has accessed the resource. It just warns us that, in theory, one or more entities outside the zone of trust can connect to the service.

Zelkova supports multiple AWS services. For example, when we make an <a href="https://aws.amazon.com/s3/" class="link" target="_blank" rel="noopener noreferrer">S3</a> bucket public, a red warning badge will appear at the top of the page. This warning comes from Zelkova.

Access Analyzer works in three areas.

### 2.1. Finding external access

Access Analyzer supports <a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/access-analyzer-resources.html" class="link" target="_blank" rel="noopener noreferrer">various resource types</a> where we can control access using resource-based policies. If we add an external principal to the policy, Access Analyzer will create a **finding**. It will run each time we create, modify or delete a policy. It usually does it within a few seconds, but sometimes we should wait 10-15 minutes to see the finding. One thing to note is that Access Analyzer doesn't create reports for service principals.

![Public bucket finding](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9nkv8o6vpkqsdrpysgi6.png)

When we make an S3 bucket **public**, we will see the finding in the IAM page and the **IAM Access Analyzer for S3** section in S3. The new finding's status becomes `Active`.

If we react to the finding and <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html" class="link" target="_blank" rel="noopener noreferrer">block public access</a> to the bucket, that is, we **rectify** the issue, Access Analyzer will set the status to `Resolved`.

If we intentionally made the bucket public (very few use cases justify it), we can approve the finding by setting its status to `Archived` manually or programmatically. Then it will disappear from the **Active** tab on the Access Analyzer page.

![Cross-account access finding](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wyjg8kcagou0sxjug6b0.png)

Access Analyzer will also create a finding when the bucket is not public but allows cross-account access. In this case, we can view it in the **other AWS accounts** section in S3.

Access Analyzer can check S3 bucket policies in <a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/access-analyzer-access-preview.html" class="link" target="_blank" rel="noopener noreferrer">preview mode</a>, that is we can run the analyzer before we add the policy to the bucket.

The post's topic is to receive notifications when Access Analyzer finds policies providing access to resources outside the trusted zone.

### 2.2. Validate policies

Access Analyzer can also validate policies. When we create or edit a policy, we'll see a thin section at the bottom of the page.

![Access Analyzer policy validation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xhfl4ahcew2n4rg1w6t9.png)

The validator will tell us about security issues, policy errors, and warnings. It can also provide suggestions, e.g., use the `IpAddress` condition operator instead of `StringEquals` when we want to restrict access to a specific IP-address range.

### 2.3. Generate policies

The third task Access Analyzer can do is create a policy based on a role's past activity.

![Access Analyzer policy generation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/861izqgbtw466yspo85r.png)

It uses <a href="https://aws.amazon.com/cloudtrail/" class="link" target="_blank" rel="noopener noreferrer">CloudTrail</a> events up to 90 days in the past and creates a tailor-made policy for the role based on the activity.

## 3. Getting notified

It's all great, but neither we nor Alice want to sit by the open Console and monitor Access Analyzer if it has found something. Instead, we can get notified when someone generates a new resource-based policy or edits an existing one that allows existing access.

### 3.1. Using EventBridge

Access Analyzer along with many other services send events to <a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-what-is.html" class="link" target="_blank" rel="noopener noreferrer">EventBridge</a>. We can set up **targets** that EventBridge will invoke with the event or custom content based on the event when it receives a matching rule.

Say we want to know if someone adds external read access permission to the objects in an S3 bucket. We can create an EventBridge event pattern like this:

```json
{
  "source": ["aws.access-analyzer"],
  "detail-type": ["Access Analyzer Finding"],
  "detail": {
    "resourceType": ["AWS::S3::Bucket"],
    "action": ["s3:GetObject"]
  }
}
```

If we omit the `detail` object, EventBridge will trigger the targets for any Access Analyzer findings from all supported services. We should create the rule in the **default** event bus because AWS services send notifications there.

### 3.2. Adding notification

Let's make it simple and add an <a href="https://aws.amazon.com/sns/" class="link" target="_blank" rel="noopener noreferrer">SNS</a> topic target to the rule. We can add multiple different <a href="https://docs.aws.amazon.com/sns/latest/dg/sns-create-subscribe-endpoint-to-topic.html" class="link" target="_blank" rel="noopener noreferrer">subscribers</a> to the topic. Any time when Access Analyzer creates a new finding, SNS can, for example, send an email or a customized <a href="https://dev.to/aws-builders/receiving-slack-notifications-when-cloudtrail-logging-gets-turned-off-5ad" class="link" target="_blank" rel="noopener noreferrer">Slack</a> message.

## 4. Monitoring

One thing to remember with Access Analyzer is that it reports the finding when a create, modify, or delete policy event occurs. Other than that, it won't do anything else. If Alice misses the notification or is too busy to address the issue immediately, the finding will remain in `Active` status.

Luckily, we can set up a simple monitoring service, which uses a **scheduled** EventBridge rule and a simple <a href="https://aws.amazon.com/lambda/" class="link" target="_blank" rel="noopener noreferrer">Lambda function</a>.

EventBridge can invoke the target every, say, 6 hours. The function will retrieve all findings with `Active` status and publishes a message with some finding properties to the SNS topic.

A sample <a href="https://www.typescriptlang.org/" class="link" target="_blank" rel="noopener noreferrer">TypeScript</a> code for getting all `Active` findings can look like this:

```ts
import {
  AccessAnalyzerClient,
  ListFindingsCommand,
  ListFindingsCommandInput,
  FindingSummary,
} from '@aws-sdk/client-accessanalyzer';

const accessAnalyzerClient = new AccessAnalyzerClient();

const findActiveFindings = async (): Promise<FindingSummary[]> => {
  const findingsInput: ListFindingsCommandInput = {
    analyzerArn: 'arn:aws:access-analyzer:eu-central-1:123456789012:analyzer/test-analyzer',
    filter: {
      status: {
        eq: ['ACTIVE'],
      },
    },
  };
  const listFindingsCommand = new ListFindingsCommand(findingsInput);

  const response = await accessAnalyzerClient.send(listFindingsCommand);
  const findings = response.findings;
  if (!findings) {
    console.log('No active findings have been found');
    return [];
  }

  return findings;
};
```

This function return **all** `Active` findings, not only the ones that come from S3. We can filter by `resourceType` if we want to narrow it down:

```ts
filter: {
  resourceType: {
    eq: 'AWS::S3::Bucket'
  },
},
```

We can filter by other <a href="https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-accessanalyzer/interfaces/findingsummary.html" class="link" target="_blank" rel="noopener noreferrer">properties</a>, too, based on our use case.

The notifier function that publishes the notification to an SNS topic can look like this:

```ts
import { SNSClient, PublishCommand, PublishInput, PublishCommandOutput } from '@aws-sdk/client-sns';

const snsClient = new SNSClient();

export const notifyAdmins = async (
  activeFindings: FindingSummary[],
  time: string,
): Promise<PublishCommandOutput | null> => {
  if (!activeFindings.length) {
    return null;
  }

  const messageInput = activeFindings.map((finding) => {
    return {
      time,
      findingId: finding.id,
      resourceName: finding.resource,
    };
  });

  const publishInput: PublishInput = {
    TopicArn: 'arn:aws:sns:eu-central-1:123456789012:test-topic',
    Subject: 'Unresolved findings',
    Message: JSON.stringify({
      message: 'Some resources still have external access!',
      findings: messageInput,
    }),
  };

  const publishCommand = new PublishCommand(publishInput);
  const response = await snsClient.send(publishCommand);

  return response;
};
```

Let's put them together in the handler:

```ts
import { ScheduledEvent } from 'aws-lambda';

interface FindingsHandlerResponse {
  success: boolean;
  messageId: string;
}

export const handler = async (event: ScheduledEvent): Promise<FindingsHandlerResponse> => {
  const time = event.time;

  try {
    const activeFindings = await findActiveFindings();
    const response = await notifyAdmins(activeFindings, time);
    return {
      success: true,
      messageId: response?.MessageId ?? 'No message has been sent',
    };
  } catch (error) {
    console.error('Error while publishing message: ', error);
    return {
      success: false,
    };
  }
};
```

The execution role must have `access-analyzer:ListFindings` and `sns:Publish` permissions.

The system will continually remind Alice from now to check the findings, and she will hopefully address them at her earliest convenience.

## 5. Summary

IAM Access Analyzer creates findings for resource-based policies that provide access to entities outside the zone of trust. These findings will be in `Active` status until we resolve (i.e., solve the problem) or archive (accept the external access) them. Access Analyzer can also validate policies and create new ones based on past activities.

We can create a solution that sends notifications when Access Analyzer discovers a new finding and programmatically monitors for them in `Active` status.

## 6. Further reading

<a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/access-analyzer-resources.html" class="link" target="_blank" rel="noopener noreferrer">IAM Access Analyzer resource types</a> - Supported resource types for Access Analyzer

<a href="https://aws.amazon.com/blogs/security/protect-sensitive-data-in-the-cloud-with-automated-reasoning-zelkova/" class="link" target="_blank" rel="noopener noreferrer">How AWS uses automated reasoning to help you achieve security at scale</a> - AWS blog post about Zelkova and automated reasoning for hard-core fans

<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/eventbridge/latest/userguide/eb-create-rule.html" class="link" target="_blank" rel="noopener noreferrer">Creating Amazon EventBridge rules that react to events</a> - Everything about EventBridge rules AWS style

<a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-create-rule-schedule.html" class="link" target="_blank" rel="noopener noreferrer">Creating an Amazon EventBridge rule that runs on a schedule</a> - The title says it all