Programmatically reacting to S3 bucket external access exposures

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

IAM Access Analyzer can help us discover which resources have policies attached that provide external access.

The tool is part of IAM. First, we must create an analyzer, which can be account- or organization-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 S3 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 various resource types 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
Public bucket finding

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 block public access 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
Cross-account access finding

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 preview mode, 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
Access Analyzer policy validation

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 generator
Access Analyzer policy generator

It uses CloudTrail 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 EventBridge. 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:

{
  "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 SNS topic target to the rule. We can add multiple different subscribers to the topic. Any time when Access Analyzer creates a new finding, SNS can, for example, send an email or a customized Slack 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 Lambda function.

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 TypeScript code for getting all Active findings can look like this:

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:

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

We can filter by other properties, too, based on our use case.

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

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:

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

IAM Access Analyzer resource types - Supported resource types for Access Analyzer

How AWS uses automated reasoning to help you achieve security at scale - AWS blog post about Zelkova and automated reasoning for hard-core fans

Getting started with Lambda - How to create a Lambda function

Creating Amazon EventBridge rules that react to events - Everything about EventBridge rules AWS style

Creating an Amazon EventBridge rule that runs on a schedule - The title says it all