AWS Security Blog

How to use AWS IAM Access Analyzer API to automate detection of public access to AWS KMS keys

In this blog post, I show you how to use AWS IAM Access Analyzer programmatically to automate the detection of public access to your resources in an AWS account. I also show you how to work with the Access Analyzer API, create an analyzer on your account and call specific API functions from your code.

Access Analyzer helps you identify the resources in your organization and accounts, keys that are shared with an external entity. This can include resources such as Amazon Simple Storage Service (Amazon S3) buckets, AWS Identity and Access Management (IAM) roles, AWS Key Management Service (AWS KMS) keys, AWS Lambda functions, and Amazon Simple Queue Service (Amazon SQS) queues. Access Analyzer identifies resources that are shared with external principals by using the formal reasoning initiative to analyze the resource-based policies in your Amazon Web Services (AWS) environment.

You can enable Access Analyzer in the IAM console and use the interactive mode to review findings. You can resolve findings by performing corrective actions and then archive the findings. Access Analyzer is automatically integrated with AWS Security Hub, which aggregates security findings from various AWS services and AWS Partner Network (APN) products including ticketing, incident management, log management, and chat systems.

Access Analyzer is designed to work autonomously to detect public access once it has been enabled on your account in a specific Region. It’s integrated with Amazon EventBridge to launch an automatic scan for public access as soon as any relevant change is made on a supported resource. For example, changes to an Amazon S3 bucket policy or the creation of a grant on an AWS KMS customer key. Access Analyzer publishes all findings in the Access Analyzer dashboard in the IAM console and generates an EventBridge event for each finding. You can process the events programmatically via an EventBridge rule.

Sometimes, however, you want to work with Access Analyzer programmatically via the provided API. For example, if you’re integrating a third-party security monitoring or governance system with your account, or you want to implement a custom user interface for Access Analyzer findings and managing those findings through their lifecycle. Another use case is to implement custom logic or a custom workflow in resource scanning, or for reacting to public access findings.

To implement those and similar use cases, you have to work with the Access Analyzer API. For example, via the popular Python boto3 SDK. In this post, I demonstrate the use of the Access Analyzer API through an example of the detection of publicly accessible AWS KMS keys. You can apply the same code, logic, and design patterns to implement custom logic for your specific use case.

Solution overview – serverless workflow

In this post, you learn how to work with the Access Analyzer API on an example of a serverless solution built by using AWS fully managed services like Amazon Simple Notification Service (Amazon SNS), Lambda functions, Amazon EventBridge, and AWS CloudTrail. Using serverless deployment patterns, you only need to deploy code. AWS is responsible for the operation and durability of the service infrastructure.

Figure 1 shows the architecture for using Access Analyzer to detect public access of AWS KMS keys. The six steps in the architecture are described in the following text.

Figure 1: Overall solution architecture for using Access Analyzer to detect public access of AWS KMS keys

Figure 1: Overall solution architecture for using Access Analyzer to detect public access of AWS KMS keys

Resources

As shown in part 1 of Figure 1, Access Analyzer supports six types of AWS resources:

  • S3 buckets
  • IAM roles
  • AWS KMS keys
  • Lambda functions and layers
  • Amazon SQS queues
  • AWS Secrets Manager secrets

For each of these resources, Access Analyzer analyzes the resource-based policies that are applied to the AWS resources in the Region where you enabled Access Analyzer.

In this post, you examine AWS KMS keys. For AWS KMS customer master keys (CMKs), Access Analyzer analyzes the key policies and grants applied to a key. Access Analyzer generates a finding if a key policy or grant allows an external entity to access the key.

AWS KMS API calls via CloudTrail

As shown in part 2 of Figure 1, EventBridge events are integrated with CloudTrail, a service that provides a record of actions taken by a user, role, or an AWS service. CloudTrail captures API calls made by—or on behalf of—your AWS account.

In this particular case, we’re interested only in specific AWS KMS API calls that can change the access to an AWS KMS key.

At present, AWS KMS supports two resource-based access control mechanisms: Key policies and grants. To be notified of any access changes, you must capture two API calls: PutKeyPolicy and CreateGrant.

Capture AWS KMS API calls in an EventBridge rule

You create an EventBridge rule, shown in part 3 of Figure 1, with the following event pattern:

{
    "source": [
        "aws.kms"
    ],
    "detail-type": [
        "AWS API Call via CloudTrail"
    ],
    "detail": {
        "eventSource": [
            "kms.amazonaws.com"
    ],
    "eventName": [
        "PutKeyPolicy",
        "CreateGrant"
    ]
    }
}

The rule is invoked every time those two AWS KMS API operations—PutKeyPolicy and CreateGrant—are called by any AWS internal or external principal.

Invoke an AWS Lambda function

As shown in part 4 of Figure 1, the EventBridge rule launches an AWS Lambda function.

The Lambda function uses the Access Analyzer API to get access to an existing analyzer or to create a new one, and initiates the resource scan for the resource type AWS::KMS::Key.

Publish scan findings to an EventBridge bus

As shown in part 5A of Figure 1, if there are any Access Analyzer findings of external access to AWS KMS keys generated after the resource scan, the Lambda function puts these findings to an EventBridge event bus. You can create event-driven workflows using EventBridge. For example, you can publish events from the EventBridge event bus to an Amazon SNS topic to send a message via email, push notifications to mobile apps, or text messages to a mobile phone number. Alternatively, you can publish these findings to AWS Security Hub (part 5B of Figure 1) and then build response and remediation actions in Security Hub. This has the advantage of working off a standardized data schema for all Security Hub integrations, the AWS Security Findings Format. You can find more details and a reference architecture for AWS Security Hub automated response and remediation in the AWS Solutions library.

Perform an optional corrective action

As shown in part 6 of Figure 1, you can also create an EventBridge rule, which invokes a Lambda function. The function responds with specific corrective actions. For example, the function can block access to the affected AWS KMS key or keys or revoke a key grant.

Source code repository

The ready to use solution is provided in a GitHub code repository as an AWS Serverless Application Model (AWS SAM) application. You can deploy the application using AWS SAM CLI.

Note: If you want to know how to deploy the solution using AWS CLI, instructions are included in the README.md file of the code repository.

Step-by-step implementation

In the rest of the post, you learn how to use the Access Analyzer API to scan AWS KMS key policies and grants to detect unintended public access. The examples that follow were created using Python 3.8 and the AWS SDK for Python (Boto3).

A prerequisite for running the following commands is an AWS account and AWS Command Line Interface (AWS CLI) configured with administrator permissions. You can find the detailed installation and configuration instructions in the AWS CLI user guide.

You can follow along the walkthrough below by downloading the code from aws-iam-access-analyzer-kms-blog on the GitHub website.

Create or get Access Analyzer

Before you can call the Access Analyzer API, you have to create an analyzer or get an existing one.

Note: There is a service quota of one analyzer per account and Region and your code will not be able to create a new analyzer if there is already one active in the given Region.

If there is no active analyzer, you create a new one by calling the AWS SDK API CreateAnalyzer operation. Use a random uuid in the analyzer name to avoid any conflict with existing names.

The analyzer ARN has the following format:

arn:aws:access-analyzer:REGION:ACCOUNT_ID:analyzer/ConsoleAnalyzer-4127f66a-c812-436e-b01d-7427ba8ed6db

If you get an exception while trying to run the commands to create or get an analyzer, check that you have permissions—access-analyzer:CreateAnalyzer—to create an analyzer as the IAM user you’re currently logged in as. If you don’t have the necessary permissions, you will get an AccessDeniedException like the following in Step 2 of the following procedure:

AccessDeniedException: An error occurred (AccessDeniedException) when calling the CreateAnalyzer operation.

If you don’t have an AdministratorAccess permission policy attached to your IAM user, you can give your IAM user full access to Access Analyzer by attaching the AWS managed policy IAMAccessAnalyzerFullAccess to your IAM user or IAM role.

To create or get an analyzer

  1. Import the necessary Python modules and define some global variables.
    import boto3
    import uuid
    
    aa_client = boto3.client('accessanalyzer')
    analyzer_arn = ""
    
  2. Get the ARN of Access Analyzer and store it in the analyzer_arn variable.
    try:
        # get all active analyzers for the given account
        active_analyzers = [a for a in aa_client.list_analyzers(type="ACCOUNT").get("analyzers") if a["status"] == "ACTIVE"]
        
        if active_analyzers:
            # take the first active analyzer if there are any active analyzer
            analyzer_arn = active_analyzers[0]["arn"]
        else:
            # try to create a new analyzer if there is no analyzer already created for the account
            a_name = "AccessAnalyzer-" + str(uuid.uuid1())
            analyzer_arn = aa_client.create_analyzer(
                analyzerName=a_name,
                type="ACCOUNT").get("arn")
    
    except Exception as e:
        print(f"Exception during get analyzer: {str(e)}")
    

The preceding code snippet looks for any Active Analyzer that might already have been created in the given account and Region.

Prepare a list of resources to scan

Once you have the Access Analyzer ARN, you can scan resources and get findings from the Access Analyzer.

To prepare a list of recourses

  1. Scan all KMS keys that aren’t managed by AWS in your account in the given Region and use boto3 AWS KMS client to access the AWS KMS API.
    kms_client = boto3.client("kms")
  2. Now, get all customer managed keys in AWS KMS:
    customer_keys_arns = []
    for page in kms_client.get_paginator("list_keys").paginate():
        # get the KeyManager (AWS or Customer) for each returned key
        for k in page["Keys"]:
            k_data = kms_client.describe_key(KeyId=k["KeyId"])["KeyMetadata"]
            # take only Customer key (where KeyManager not AWS)
            if k_data["KeyManager"] not in "AWS":
                customer_keys_arns.append(k_data["Arn"])
    

    The preceding code iterates through the AWS KMS keys returned by the list_keys call and saves the keys that aren’t managed by AWS into a separate list—customer_keys_arn—for further processing.

  3. Now you have a list of the ARNs of all the CMKs in the customer_keys_arns list, like the following example.
    ['arn:aws:kms:us-east-1:ACCOUNT_ID:key/786b353b-49f1-4dd2-b4b5-573ae99b3670',
     'arn:aws:kms:us-east-1:ACCOUNT_ID:key/9078ac09-1420-49fd-a2f3-be4de05d8d04',
     'arn:aws:kms:us-east-1:ACCOUNT_ID:key/b1d3dcb6-58b8-46a8-803b-6597ecc57293']
    

Scan resources and get results

Now that you have a list of resources, you can call the Access Analyzer API to scan them and return the results.

StartResourceScan is one way to scan resources and retrieve findings. After the operation is started, wait until the requested resources have been analyzed by calling ListAnalyzedResources, and finally retrieve the resource findings with GetAnalyzedResource.

To scan resources

Let’s put it all together in the Python code. For readability, this example omits logging and exception handling, which you will have in your production code.

  1. For each customer key to be analyzed, call StartResourceScan, passing your analyzer ARN and resource ARN.
    # scan customer keys using access analyzer
    resource_scan = {}
    
    # initiate scan for resources
    for r_arn in customer_keys_arns:
        res = aa_client.start_resource_scan(
            analyzerArn=analyzer_arn,
            resourceArn=r_arn
            )
        print(f"Start_resouce_scan for {r_arn}:{res}")
    
        resource_scan[r_arn] = False
    

    The list of scan requests is kept in the resource_scan dictionary.

  2. Call ListAnalyzedResources and continue calling it until all the requested resources have been analyzed or the timeout has been reached.
    import time
    
    # wait till all resources get analyzed
    MAX_LIST_ANALYZED_RESOURSES_ATTEMPTS = 10
    rType = "AWS::KMS::Key"
    
    for _ in range(MAX_LIST_ANALYZED_RESOURSES_ATTEMPTS):
    
        for page in aa_client.get_paginator("list_analyzed_resources").paginate(analyzerArn=analyzer_arn, resourceType=rType):
            for r in page["analyzedResources"]:
                if r["resourceArn"] in resource_scan:
                    resource_scan[r["resourceArn"]] = True
                    
        pending = {r:s for r,s in resource_scan.items() if not s}
    
        if not pending: # exit if all requested resources are processed
                break
        time.sleep(0.5)
    
  3. Call GetAnalyzedResource for each analyzed AWS KMS key to get the findings.
    findings = []
    
    for r_arn in {r for r,s in resource_scan.items() if s}:
        res = aa_client.get_analyzed_resource(
            analyzerArn=analyzer_arn,
            resourceArn=r_arn)
    
        resource = res["resource"]
        if resource.get("isPublic") and resource.get("status") in "ACTIVE":
            print(f"Found public resource: {r_arn}:{resource}")
            findings.append(resource)
    

Understanding the analyzer findings

The operation GetAnalyzedResource returns a structure containing response metadata and information about the analyzed resource in the resource key of the dictionary, as shown in the following example:

{
  'ResponseMetadata': {
    'RequestId': 'd2348bca-1d69-417f-8a66-28fe084333d4',
    'HTTPStatusCode': 200,
    'HTTPHeaders': {
      'date': 'Sat, 28 Nov 2020 21:35:24 GMT',
      'content-type': 'application/json',
      'content-length': '228',
      'connection': 'keep-alive',
      'x-amzn-requestid': 'd2348bca-1d69-417f-8a66-28fe084333d4',
      'x-amz-apigw-id': 'WvNYZHiHIAMF7-A=',
      'x-amzn-trace-id': 'Root=1-5fc2c29c-55b61a3b5d3cf324317ddbe7'
    },
    'RetryAttempts': 0
  },
  'resource': {
    'analyzedAt': datetime.datetime(2020, 11, 28, 21, 25, 16, tzinfo = tzutc()),
    'isPublic': False,
    'resourceArn': 'arn:aws:kms:us-east-1:ACCOUNT_ID:key/786b353b-49f1-4dd2-b4b5-573ae99b3670',
    'resourceOwnerAccount': 'ACCOUNT_ID',
    'resourceType': 'AWS::KMS::Key'
  }
}

You’re interested in the value of the key isPublic from the resource dictionary. The structure of the resource key dictionary depends on the isPublic value. If the resource detected is public, isPublic is set to True and there are additional data that provide more details:

'resource': {
    'actions': ['kms:CreateGrant', 'kms:Decrypt', 'kms:DescribeKey', 'kms:Encrypt', 'kms:GenerateDataKey', 'kms:GenerateDataKeyPair', 'kms:GenerateDataKeyPairWithoutPlaintext', 'kms:GenerateDataKeyWithoutPlaintext', 'kms:GetKeyRotationStatus', 'kms:GetPublicKey', 'kms:ListGrants', 'kms:ReEncryptFrom', 'kms:ReEncryptTo', 'kms:RetireGrant', 'kms:RevokeGrant', 'kms:Sign', 'kms:Verify'],
    'analyzedAt': datetime.datetime(2020, 11, 29, 9, 18, 45, tzinfo = tzutc()),
    'createdAt': datetime.datetime(2020, 11, 29, 8, 29, 58, 456000, tzinfo = tzutc()),
    'isPublic': True,
    'resourceArn': 'arn:aws:kms:us-east-1:ACCOUNT_ID:key/786b353b-49f1-4dd2-b4b5-573ae99b3670',
    'resourceOwnerAccount': 'ACCOUNT_ID',
    'resourceType': 'AWS::KMS::Key',
    'status': 'ACTIVE',
    'updatedAt': datetime.datetime(2020, 11, 29, 9, 18, 45, tzinfo = tzutc())
}

For example, the actions key lists what API operations are allowed on the affected resource, status shows if this Access Analyzer finding is active or resolved, and there are various timestamp keys that provide detailed timeline information.

Your own function logic can use that information and also the values of the analyzedAt or updatedAt fields for further custom processing and user notification.

Access Analyzer findings for non-public resources

If the isPublic flag isn’t set to True, Access Analyzer can still generate findings. For example, if Access Analyzer detects access via key policy or key grant to an AWS external principle outside of the current account zone of trust, like the following example:

'resource': {
    'actions': ['kms:Decrypt'],
    'analyzedAt': datetime.datetime(2020, 11, 29, 10, 1, 23, tzinfo = tzutc()),
    'createdAt': datetime.datetime(2020, 11, 29, 9, 46, 37, 899000, tzinfo = tzutc()),
    'isPublic': False,
    'resourceArn': 'arn:aws:kms:us-east-1:ACCOUNT_ID:key/786b353b-49f1-4dd2-b4b5-573ae99b3670',
    'resourceOwnerAccount': 'ACCOUNT_ID',
    'resourceType': 'AWS::KMS::Key',
    'status': 'ACTIVE',
    'updatedAt': datetime.datetime(2020, 11, 29, 9, 46, 37, 899000, tzinfo = tzutc())
  }
}

You can process these cases and notify the user about active Access Analyzer findings for supported and monitored resources.

Publish findings

In the previous section, you stored the list of active Access Analyzer findings for AWS KMS customer keys in the findings list. Now you need to publish these findings to the default EventBridge event bus and also notify any other components of your solution and possibly the user about these findings.

As mentioned in the solution overview, EventBridge is used for an event-driven workflow. There are over 15 AWS services available as event targets for EventBridge. We are going to create an EventBridge rule to publish the findings event to an Amazon SNS topic, for example for sending user email or launching another Lambda function in your solution.

To publish findings

  1. Create a new Amazon SNS topic.
    aws sns create-topic --name access-analyzer-kms-keys-findings

    The AWS CLI create-topic operation returns a topic ARN, which you need in the next command.

    aws sns subscribe \
        --topic-arn <TOPIC_ARN> \
        --protocol email \
        --notification-endpoint <YOUR_EMAIL_ADDRESS>
    

    A confirmation email is sent that must be approved to confirm subscription.

  2. Create an EventBridge rule to publish findings to the Amazon SNS topic.
    aws events put-rule \
        --name kms-key-access-findings \
        --event-pattern "{\"source\": [\"access-analyzer-kms-function\"],\"detail-type\": [\"Access Analyzer KMS Findings\"]}" 
    

    Set the Amazon SNS topic as a target for the EventBridge rule:

    TOPIC_ARN=<Amazon SNS topic ARN>
    
    aws events put-targets \
        --rule kms-key-access-findings \
        --targets "Id"="1","Arn"="${TOPIC_ARN}"
    
  3. Add permissions to enable EventBridge to publish to SNS topic
    Using the TOPIC_ARN, add the resource-based policy to the Amazon SNS topic:
ACCOUNT_ID=<Enter your AWS account id>
TOPIC_ARN=<Amazon SNS topic ARN>

aws sns set-topic-attributes --topic-arn "${TOPIC_ARN}" \
    --attribute-name Policy \
    --attribute-value "{\"Version\":\"2012-10-17\",\"Id\":\"__default_policy_ID\",\"Statement\":[{\"Sid\":\"__default_statement_ID\",\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"*\"},\"Action\":[\"SNS:GetTopicAttributes\",\"SNS:SetTopicAttributes\",\"SNS:AddPermission\",\"SNS:RemovePermission\",\"SNS:DeleteTopic\",\"SNS:Subscribe\",\"SNS:ListSubscriptionsByTopic\",\"SNS:Publish\",\"SNS:Receive\"],\"Resource\":\"${TOPIC_ARN}\",\"Condition\":{\"StringEquals\":{\"AWS:SourceOwner\":\"${ACCOUNT_ID}\"}}}, {\"Sid\":\"PublishEventsToSNSTopic\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"events.amazonaws.com\"},\"Action\":\"sns:Publish\",\"Resource\":\"${TOPIC_ARN}\"}]}"

Now you’re ready to put your findings to the default EventBridge event bus, which will invoke the created EventBridge rule to publish the findings event to the created Amazon SNS topic, which, in turn, will send an email notification.

  • Use boto3 AWS SDK to get access to the EventBridge.
    events = boto3.client('events')
    
  • Publish your findings to the EventBridge bus.
    import json
    import datetime
    
    # class JSONEncoder
    class DateTimeEncoder(json.JSONEncoder):
            #Override the default method
            def default(self, obj):
                if isinstance(obj, (datetime.date, datetime.datetime)):
                    return str(obj.isoformat())
    
    if bool(findings):
        r = events.put_events(
            Entries=[
                {
                    "Source":"access-analyzer-kms-function",
                    "Resources":[r["resourceArn"] for r in findings],
                    "DetailType":"Access Analyzer KMS Findings",
                    "Detail":json.dumps({"Findings":findings}, indent=2, cls=DateTimeEncoder), 
                }
            ]
        )
    

 

Note the use of a custom DateTimeEncoder to serialize the JSON representation of the findings list into a string. This is done because the EventBridge PutEvents API operation accepts only a string value for the Detail parameter.

You have implemented code to:

  1. Create an Access Analyzer.
  2. Enumerate all AWS KMS customer keys for the account and region.
  3. Invoke the Access Analyzer scan on the customer keys.
  4. Publish any findings of public access for the AWS KMS keys to the EventBridge event bus
  5. Create an EventBridge rule to publish findings to the Amazon SNS topic

Before you move to testing your solution, let’s take a closer look at Lambda execution role permissions and function invocation pattern.

Lambda function execution role and role permissions

In addition to the basic Lambda permissions for creating a CloudWatch log stream and directing events to the log stream, you must add specific permissions to allow your function to perform the following actions:

  • Create Access Analyzer and service-linked role
  • Work with Access Analyzer API
  • List and describe AWS KMS keys
  • Put events to an EventBridge bus

These permissions are listed in the permissions policy, which is attached to the Lambda execution role.

{
    "Version": "2012-10-17",
    "Statement": 
    [
        {
            "Action": [
            "iam:CreateServiceLinkedRole"
            ],
            "Resource": "arn:aws:iam::ACCOUNT_ID:role/aws-service-role/access-analyzer.amazonaws.com/AWSServiceRoleForAccessAnalyzer",
            "Effect": "Allow",
            "Sid": "AccessAnalyzerCreateServiceLinkedRole"
        },
        {
            "Action": [
                "access-analyzer:Get*",
                "access-analyzer:List*",
                "access-analyzer:Start*",
                "access-analyzer:CreateAnalyzer"
            ],
            "Resource": "*",
            "Effect": "Allow",
            "Sid": "AccessAnalyzerAccessPolicy"
        },
        {
            "Action": [
                "kms:ListKeys",
                "kms:DescribeKey"
            ],
            "Resource": "*",
            "Effect": "Allow",
            "Sid": "KMSAccessPolicy"
        },
        {
            "Action": [
                "events:PutEvents"
            ],
            "Resource": "*",
            "Effect": "Allow",
            "Sid": "EventBridgeAccess"
        }
    ]
}

Special consideration should be given to the following permissions policy statement, particular, iam:CreateServiceLinkedRole.

{
    "Action": [
    "iam:CreateServiceLinkedRole"
    ],
    "Resource": "arn:aws:iam::ACCOUNT_ID:role/aws-service-role/access-analyzer.amazonaws.com/AWSServiceRoleForAccessAnalyzer",
    "Effect": "Allow",
    "Sid": "AccessAnalyzerCreateServiceLinkedRole"
}

Access Analyzer uses an AWS service-linked role. A service-linked role is a unique type of IAM role that is linked directly to Access Analyzer. Service-linked roles are predefined by Access Analyzer and include all the permissions that the feature requires to call other AWS services on your behalf.

Access Analyzer uses the service-linked role named AWSServiceRoleForAccessAnalyzer. This allows Access Analyzer to analyze resource metadata.

Add these permissions to allow your Lambda function execution role to create a service-linked role in order to be able to create and use Access Analyzer. You use a special operation—iam:CreateServiceLinkedRole—in your permissions policy. For more information, see Service-linked role permissions in the IAM user guide.

Use EventBridge rule to launch the Lambda function

The last piece in the architecture of this solution is an EventBridge rule that launches the Lambda function.

As described in the overview section, you want to launch the Access Analyzer KMS key scan on any changes in key policy or on creation of a key grant for any customer KMS key. The two API operations responsible for this are PutKeyPolicy and CreateGrant.

To launch the Lambda function

  1. To create a rule that is launched by those actions, you must specify the following event pattern in the rule definition to capture the specific AWS KMS API calls in EventBridge via AWS CloudTrail.
    {
        "source": [
            "aws.kms"
        ],
        "detail-type": [
            "AWS API Call via CloudTrail"
        ],
        "detail": {
            "eventSource": [
                "kms.amazonaws.com"
        ],
        "eventName": [
            "PutKeyPolicy",
            "CreateGrant"
        ]
        }
    }
    
  2. Enter your Access Analyzer Lambda function as an invocation target for the rule.
  3. Allow the EventBridge rule to invoke your Lambda function by adding a resource-based policy to the function with lambda:InvokeFunction permission given to the rule.
    {
    "Version": "2012-10-17",
    "Id": "default",
    "Statement": [
        {
            "Sid": "EventBridgeRuleLambdaPermission",
            "Effect": "Allow",
            "Principal": {
                "Service": "events.amazonaws.com"
            },
            "Action": "lambda:InvokeFunction",
            "Resource": "arn:aws:lambda:REGION:ACCOUNT_ID:function:access-analyzer-kms-function",
            "Condition": {
                "ArnLike": {
                "AWS:SourceArn": "arn:aws:events:REGION:ACCOUNT_ID:rule/kms-key-access-changes"
                }
            }
            }
        ]
    }
    

Test detection of public access for AWS KMS key policies

To test the solution, create an AWS KMS customer key and allow public access in the key policy. Then check that your Lambda function is invoked, scans the key policy, and sends an email message with the Access Analyzer findings.

You can use the following AWS CLI commands to test the system.

To test the system

  1. Create an AWS KMS customer key:
    aws kms create-key \
        --description "Access Analyzer KMS customer key scan test"
    

    The call returns the KeyId, shown in the following example, which you need for the next step.

    The customer key is created with a default policy. You can see the default policy in the AWS KMS console.

    {
        "Version": "2012-10-17",
        "Id": "key-default-policy",
        "Statement": [
            {
                "Sid": "Enable IAM User Permissions",
                "Effect": "Allow",
                "Principal": {
                    "AWS":"arn:aws:iam::ACCOUNT_ID:root"
                },
                "Action": "kms:*",
                "Resource": "*"
            }
        ]
    }
    
  2. Update the policy to include the “*” principal. This simulates public access of the AWK KMS customer key. Replace KEY_ID with the value from the first step and ACCOUNT_ID with your account id.
    aws kms put-key-policy \
        --key-id <KEY_ID> \
        --policy-name default \
        --policy "{\"Version\": \"2012-10-17\",\"Id\": \"key-default-policy\",\"Statement\": [{\"Sid\": \"Enable IAM User Permissions\",\"Effect\": \"Allow\",\"Principal\": {\"AWS\": [\"arn:aws:iam::ACCOUNT_ID:root\",
    \"*\"]},\"Action\": \"kms:*\",\"Resource\": \"*\"}]}"
    

    When you call the AWS KMS API PutKeyPolicy operation, a series of steps follows:

    1. CloudTrail sends the notification to an EventBridge bus.
    2. An EventBridge rule launches the Lambda function.
    3. The Lambda function invokes Access Analyzer to scan all customer keys.
    4. The Access Analyzer finds “*” public access to the key and generates findings.
    5. Your Lambda function publishes the findings to the EventBridge bus.
    6. EventBridge rule invokes publishing the event to the configured Amazon SNS topic
    7. Email subscription on the Amazon SNS topic generates an email

    When all of the steps are complete, you should have an email message with the subject AWS Notification Message and with content similar to the following:

    [
      {
        "actions": [
          "kms:CreateGrant",
          "kms:Decrypt",
          "kms:DescribeKey",
          "kms:Encrypt",
          "kms:GenerateDataKey",
          "kms:GenerateDataKeyPair",
          "kms:GenerateDataKeyPairWithoutPlaintext",
          "kms:GenerateDataKeyWithoutPlaintext",
          "kms:GetKeyRotationStatus",
          "kms:GetPublicKey",
          "kms:ListGrants",
          "kms:ReEncryptFrom",
          "kms:ReEncryptTo",
          "kms:RetireGrant",
          "kms:RevokeGrant",
          "kms:Sign",
          "kms:Verify"
        ],
        "analyzedAt": "2020-11-29T13:23:27+00:00",
        "createdAt": "2020-11-29T08:29:58.456000+00:00",
        "isPublic": true,
        "resourceArn": "arn:aws:kms:us-east-1:ACCOUNT_ID:key/<KEY_ID>",
        "resourceOwnerAccount": "ACCOUNT_ID",
        "resourceType": "AWS::KMS::Key",
        "status": "ACTIVE",
        "updatedAt": "2020-11-29T13:23:27+00:00"
      }
    ]
    

Congratulations, your solution works! Now you’re using Access Analyzer to detect any public access to AWS KMS customer keys in near-real-time.

To clean up

Clean up and replace the public key policy with the default version when you’ve

aws kms put-key-policy \
    --key-id <KEY_ID> \
    --policy-name default \
    --policy "{\"Version\": \"2012-10-17\",\"Id\": \"key-default-policy\",\"Statement\": [{\"Sid\": \"Enable IAM User Permissions\",\"Effect\": \"Allow\",\"Principal\": {\"AWS\": [\"arn:aws:iam::ACCOUNT_ID:root\"]},\"Action\": \"kms:*\",\"Resource\": \"*\"}]}"

Take corrective actions

Because all Access Analyzer findings are sent to the EventBridge event bus, you can implement event-driven workflows and extend the solution by adding reactions to the findings. For example, you can specify a Lambda function as a target for an EventBridge rule. This Lambda function can invoke corrective or compensatory actions to prevent or block any detected public access to your AWS KMS customer keys. The exact nature of the actions depends on your design, environment, and security requirements. One possible action is to replace a public key policy with the default policy, as shown in the following code:

{
    "Version": "2012-10-17",
    "Id": "key-default-policy",
    "Statement": [
        {
            "Sid": "Enable IAM User Permissions",
            "Effect": "Allow",
            "Principal": {
                "AWS":"arn:aws:iam::ACCOUNT_ID:root"
            },
            "Action": "kms:*",
            "Resource": "*"
        }
    ]
}

This default key policy has one policy statement that gives the AWS account—the root user in the preceding example—that owns the CMK full access to the CMK and enables IAM policies in the account to allow access to the CMK. For more information about how to use AWS KMS key policies, see Using key policies.

Note: Consider the possible impact on your AWS environment of replacing the original key policy with the default key policy. Other components and services might rely on the additional permissions and principals in the original key policy, and the replacement could adversely impact them.

Approaches for Access Analyzer use

In this section, we look at various approaches for invoking and using Access Analyzer. Access Analyzer is a regional service and you can create a new Access Analyzer in each Region via AWS Management Console or programmatically as described in the preceding sections.

Access Analyzer findings

Access Analyzer automatically generates a finding for each instance of a resource-based policy that grants access to a resource within your zone of trust to a principal that is outside your zone of trust. Each time a resource-based policy is modified, Access Analyzer analyzes the policy and reports any finding. Review all of the findings in your account to determine whether the sharing is expected and approved. Access Analyzer automatically publishes an event to EventBridge for any finding on the supported resources. Use the EventBridge rule with the following event pattern to capture all findings and invoke your custom logic:

{
    "source": [
      "aws.access-analyzer"
    ],
    "detail-type": [
      "Access Analyzer Finding"
    ]
}

For example, you can use this EventBridge rule to publish all new findings to an Amazon SNS topic or launch a Lambda function.

At the time of writing, Access Analyzer doesn’t publish findings in real time and it can take up to 30 minutes after a policy is modified for Access Analyzer to analyze the resource and update the findings. If your environment requires near real-time notification and a quick response, you should use the approach described in this post.

Context-based invocation via EventBridge rule

The most flexible approach to run Access Analyzer is to launch the resource scan as soon as an access policy for a specific resource type is modified. You can configure the EventBridge rule on any API call via CloudTrail. For each of the supported resource types, you select the set of API operations that can change the access to the resource. For an AWS KMS key policy, you set up the EventBridge rule for PutKeyPolicy and CreateGrant operations. For an S3 bucket, you can monitor PutBucketAcl, PutBucketPolicy, and DeleteBucketPolicy and use the following event pattern:

{
    "detail-type": [
      "AWS API Call via CloudTrail"
    ],
    "source": [
      "aws.s3"
    ],
    "detail": {
      "eventSource": [
        "s3.amazonaws.com"
      ],
      "eventName": [
        "PutBucketAcl",
        "PutBucketPolicy",
        "DeleteBucketPolicy"
      ]
    }
}

Each resource type requires careful study of the corresponding API and an understanding of how the resource policy can be altered before setting up the EventBridge rule event pattern.

However, this approach comes with a downside. By using custom logic to capture resource-specific API calls—like PutKeyPolicy or CreateGrant—and using the Access Analyzer API directly, you make a trade-off between full control of how the solution works and how generally it can be applied. For example, if AWS KMS adds new API calls that can change the key access policy beyond PutKeyPolicy and CreateGrant, you must update your solution to adjust to these changes. The Access Analyzer managed service automatically updates to adjust to changes in API calls.

Scheduled invocation with EventBridge rule

An additional approach to Access Analyzer invocation is to define an EventBridge rule that invokes a Lambda function at either a fixed interval or based on a Cron syntax. The Lambda function runs Access Analyzer on configured resource types based on the schedule you define.

Extending this solution to other supported AWS resources

In this post, I described a solution for one resource type, AWS KMS keys. However, Access Analyzer supports five additional types:

  • Amazon S3 buckets
  • IAM roles
  • Lambda functions and layers
  • Amazon SQS queues
  • Secrets Manager secrets

You can use the same approach and code to implement detection of public access to these resource types.

Caveats

Keep in mind that Access Analyzer is a regional service and analyzes only resources in the AWS Region in which it’s enabled. To monitor all resources in your AWS environment, you must create an analyzer to enable Access Analyzer in each Region where you’re using supported AWS resources. Findings that are generated by an analyzer in a Region are separate from findings from analyzers in other Regions.

The zone of trust for the analyzer determines the set of resources that are analyzed. Access to resources from within the zone of trust is trusted. The organization or account you choose becomes the zone of trust for the analyzer. For example, if you specify an organization, access to any resources within that organization is trusted for any principal within the same organization. You can learn more about how Access Analyzer generates findings in Access Analyzer findings.

In EventBridge, it’s possible to create rules that lead to loops, where a rule runs repeatedly. For example, a rule might detect that a key policy has changed on a KMS key, and initiate an action to change it to the desired state. If the initiating pattern and rule aren’t written carefully, the subsequent change to the key policy causes the rule to run again, creating a loop.

To prevent this, write the rules so that the actions initiated by the rule don’t rerun the rule. For example, the rule described above could be configured to run only if a key policy is found to be in a bad state, instead of after any change. It’s also a good idea to set the reserved concurrency to 1 for the Lambda function that’s launched by the EventBridge rule to avoid parallel invocation of multiple Lambda functions. A loop can also cause higher than expected charges. You can use budgeting so that you receive an alert when charges exceed your specified limit.

Conclusion

Access Analyzer can help you detect access to your AWS resources from outside of your zone of trust and protect sensitive data and services. This post has focused on how to use the Access Analyzer API to programmatically discover and report Access Analyzer findings in near-real time. You’ve seen how to use a few components to build a highly available, serverless solution. This solution can be your first step towards implementing custom processing logic or an integration into a broader security monitoring solution that fits the needs of your organization, for example with AWS Security Hub.

If you have feedback about this post, submit comments in the Comments section below.

Want more AWS Security how-to content, news, and feature announcements? Follow us on Twitter.

Author

Yevgeniy Ilyin

Yevgeniy is a Solutions Architect at AWS. He has over 20 years of experience working at all levels of software development and solutions architecture and has used programming languages from COBOL and Assembler to .NET, Java, and Python. He develops and code clouds native solutions with a focus on big data, analytics, and data engineering.