AWS Compute Blog
Simplifying cross-account access with Amazon EventBridge resource policies
This post is courtesy of Stephen Liedig, Sr Serverless Specialist SA.
Amazon EventBridge is a serverless event bus used to decouple event producers and consumers. Event producers publish events onto an event bus, which then uses rules to determine where to send those events. The rules determine the targets and EventBridge routes the events accordingly.
A common architectural approach adopted by customers is to isolate these application components or services by using separate AWS accounts. This “account-per-service” strategy limits the blast radius by providing a logical and physical separation of resources. It provides additional security boundaries and allows customers to easily track service costs without having to adopt a complex tagging strategy.
To enable the flow of events from one account to another, you must create a rule on one event bus that routes events to an event bus in another account. To enable this routing, you need to configure the resource policy for your event buses.
This blog post shows you how to use EventBridge resource policies to publish events and create rules on event buses in another account.
Overview
Today, EventBridge launches improvements to resource policies that make it easier to build applications that work across accounts. The service expands the use of the policy associated with an event bus to the authorization of API calls.
This means you can manage permissions for API calls that interact with the event bus, such as PutEvents, PutRule, and PutTargets, directly from that event bus’ resource policy. This replaces the need to create different IAM roles that are assumed by each account that interacts with the event bus. It also provides a central resource to manage your permissions.
There is support for organizations and tags via IAM conditions. Now when you call an API, it considers both the user’s IAM policy and the event bus resource policy in the authorization process.
EventBridge APIs that accept an event bus name parameter (including PutRule, PutTargets, DeleteRule, RemoveTargets, DisableRule, and EnableRule) now also support an event bus ARN. This allows you to target cross-account event buses through the APIs. For example, you can call PutRule to create a rule on an event bus in another account, without needing to assume a role.
EventBridge now supports using policy conditions for the following authorization context keys in the APIs, to help scope down permissions.
Ecommerce example walkthrough
In this ecommerce example, there are multiple services distributed across different accounts. A web store publishes an event when a new order is created. The event is sent via a central event bus, which is in another account. The bus has two rules with target services in different AWS accounts.
The goal is to create fine-grained permissions that only allow:
- The web store to publish events for a specific detail-type and source.
- The invoice processing service to create and manage its own rules on the central bus.
To complete this walk through, you set up three accounts. For account A (Web Store), you deploy an AWS Lambda function that sends the “newOrderCreated” event directly to the “central event bus” in account B. The invoice processing Lambda function in account C creates a rule on the central event bus to process the event published by account A.
Create the central event bus in account B
Create the central event bus in account B, adding the following resource policy. Be sure to substitute your account numbers for accounts A, B, and C.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "WebStoreCrossAccountPublish",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::[ACCOUNT-A]:root"
},
"Action": "events:PutEvents",
"Resource": "arn:aws:events:us-east-1:[ACCOUNT-B]:event-bus/central-event-bus",
"Condition": {
"StringEquals": {
"events:detail-type": "newOrderCreated",
"events:source": "com.exampleCorp.webStore"
}
}
},
{
"Sid": "InvoiceProcessingRuleCreation",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::[ACCOUNT-C]:root"
},
"Action": [
"events:PutRule",
"events:DeleteRule",
"events:DescribeRule",
"events:DisableRule",
"events:EnableRule",
"events:PutTargets",
"events:RemoveTargets"
],
"Resource": "arn:aws:events:us-east-1:[ACCOUNT-B]:rule/central-event-bus/*",
"Condition": {
"StringEqualsIfExists": {
"events:creatorAccount": "${aws:PrincipalAccount}",
"events:source": "com.exampleCorp.webStore"
}
}
}
]
}
There are two statements in the resource policy: WebStoreCrossAccountPublish and InvoiceProcessingRuleCreation.
The WebStoreCrossAccountPublish statement allows the Lambda function in account A to publish events directly to the central event bus. There are two conditions in the statement that further restrict the types of events that can be sent to the event bus. The first restricts the event detail-type to equal “newOrderCreated” and the second condition requires that the event source equals “com.exampleCorp.webStore”.
The InvoiceProcessingRuleCreation statement allows the invoice processing function in account C to describe, add, update, enable, disable, and delete any rules created by account C. You apply this restriction by using the events:creatorAccount context key in the statements condition.
Importantly you should set the StringEqualsIfExists type for the events:creatorAccount condition. If you use StringEquals, it results in an AccessDeniedException. AWS CloudFormation calls DescribeRule to check if the rule already exists. However, as this is a new rule, and because you set a condition for events:creatorAccount for DescribeRule, this key is not populated and CloudFormation receives an AccessDeniedException instead of ResourceNotFoundException.
Here is how you create the event bus using AWS CloudFormation:
CentralEventBus:
Type: AWS::Events::EventBus
Properties:
Name: !Ref EventBusName
WebStoreCrossAccountPublishStatement:
Type: AWS::Events::EventBusPolicy
Properties:
EventBusName: !Ref CentralEventBus
StatementId: "WebStoreCrossAccountPublish"
Statement:
Effect: "Allow"
Principal:
AWS: !Sub arn:aws:iam::${AccountA}:root
Action: "events:PutEvents"
Resource: !GetAtt CentralEventBus.Arn
Condition:
StringEquals:
"events:detail-type": "newOrderCreated"
"events:source" : "com.exampleCorp.webStore"
InvoiceProcessingRuleCreationStatement:
Type: AWS::Events::EventBusPolicy
Properties:
EventBusName: !Ref CentralEventBus
StatementId: "InvoiceProcessingRuleCreation"
Statement:
Effect: "Allow"
Principal:
AWS: !Sub arn:aws:iam::${AccountC}:root
Action:
- "events:PutRule"
- "events:DeleteRule"
- "events:DescribeRule"
- "events:DisableRule"
- "events:EnableRule"
- "events:PutTargets"
- "events:RemoveTargets"
Resource:
- !Sub arn:aws:events:${AWS::Region}:${AWS::AccountId}:rule/${CentralEventBus.Name}/*
Condition:
StringEqualsIfExists:
"events:creatorAccount" : "${aws:PrincipalAccount}"
StringEquals:
"events:source": "com.exampleCorp.webStore"
Now that you have a policy set up on the central event bus, configure the client applications to send and process events. The client application must also have permissions configured.
Create the web store order function in account A
In account A, create a Lambda function to send the event to the central bus in account B. Set the EventBusName parameter to the central event bus ARN on the PutEvents API call. This allows you to target cross-account event buses directly.
import json
import boto3
EVENT_BUS_ARN = 'arn:aws:events:us-east-1:[ACCOUNT-B]:event-bus/central-event-bus'
# Create EventBridge client
events = boto3.client('events')
def lambda_handler(event, context):
# new order created event datail
eventDetail = {
"orderNo": "123",
"orderDate": "2020-09-09T22:01:02Z",
"customerId": "789",
"lineItems": {
"productCode": "P1",
"quantityOrdered": 3,
"unitPrice": 23.5,
"currency": "USD"
}
}
try:
# Put an event
response = events.put_events(
Entries=[
{
'EventBusName': EVENT_BUS_ARN,
'Source': 'com.exampleCorp.webStore',
'DetailType': 'newOrderCreated',
'Detail': json.dumps(eventDetail)
}
]
)
print(response['Entries'])
print('Event sent to the event bus ' + EVENT_BUS_ARN )
print('EventID is ' + response['Entries'][0]['EventId'])
except Exception as e:
print(e)
Create the Invoice Processing service in account C
Next, create the invoice processing function that processes the newOrderCreated event. You use the AWS Serverless Application Model (AWS SAM) to create the invoice processing function and other application resources. Before you can process any events from the central event bus, you must create a new event bus in account C to receive incoming events.
Next, you define the function that processes the events. Here, you define a Lambda event source that is an EventBridge rule. You set the EventBusName to the receiving invoice processing event bus. When this Lambda function is deployed, AWS SAM creates the rule on the event bus with the specified pattern and target. It configures the event source that triggers the function when an event is received.
Parameters:
EventBusName:
Description: Name of the central event bus
Type: String
Default: invoice-processing-event-bus
CentralEventBusArn:
Description: The ARN of the central event bus # e.g. arn:aws:events:us-east-1:[ACCOUNT-B]:event-bus/central-event-bus
Type: String
Resources:
# This is the receiving invoice processing event bus in account C.
InvoiceProcessingEventBus:
Type: AWS::Events::EventBus
Properties:
Name: !Ref EventBusName
# AWS Lambda function processes the newOrderCreated event
InvoiceProcessingFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: invoice_processing
Handler: invoice_processing_function/app.lambda_handler
Runtime: python3.8
Events:
NewOrderCreatedRule:
Type: EventBridgeRule
Properties:
EventBusName: !Ref InvoiceProcessingEventBus
Pattern:
source:
- com.exampleCorp.webStore
detail-type:
- newOrderCreated
The next resource in the AWS SAM template is the rule that creates the target on the central event bus. It sends events to the invoice processing event bus. Though the rule is added to the central event bus, its definition is managed by the invoice processing service template. The rule definition sets EventBusName parameter to the ARN of the central event bus.
# This is the rule that the invoice processing service creates on the central event bus
InvoiceProcessingRule:
Type: AWS::Events::Rule
Properties:
Name: InvoiceProcessingNewOrderCreatedSubscription
Description: Cross account rule created by Invoice Processing service
EventBusName: !Ref CentralEventBusArn # ARN of the central event bus
EventPattern:
source:
- com.exampleCorp.webStore
detail-type:
- newOrderCreated
State: ENABLED
Targets:
- Id: SendEventsToInvoiceProcessingEventBus
Arn: !GetAtt InvoiceProcessingEventBus.Arn
RoleArn: !GetAtt CentralEventBusToInvoiceProcessingEventBusRole.Arn
DeadLetterConfig:
Arn: !GetAtt InvoiceProcessingTargetDLQ.Arn
For the central event bus target to send the event to the invoice processing event bus in account C, EventBridge needs the necessary permissions to invoke the PutEvents API. The CentralEventBusToInvoiceProcessingEventBusRole IAM role provides that permission. It is assumed by the central event bus in account B when it needs to send events to the invoice processing event bus, without you having to create an additional resource policy on the invoice processing event bus.
CentralEventBusToInvoiceProcessingEventBusRole:
Type: 'AWS::IAM::Role'
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- events.amazonaws.com
Action:
- 'sts:AssumeRole'
Path: /
Policies:
- PolicyName: PutEventsOnInvoiceProcessingEventBus
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action: 'events:PutEvents'
Resource: !GetAtt InvoiceProcessingEventBus.Arn
You can also set up a dead-letter queue (DLQ) configuration for the rule in account C. This allows the subscriber of the event to control where events that fail to get delivered to the invoice processing event bus get sent. All you need to do to make this happen is create an Amazon SQS queue in account C, and a queue policy that sets a resource policy to allow EventBridge to send failed events from account B to the queue in account C.
# Invoice Processing Target Dead Letter Queue
InvoiceProcessingTargetDLQ:
Type: AWS::SQS::Queue
# SQS resource policy required to allow target on central bus to send failed messages to target DLQ
InvoiceProcessingTargetDLQPolicy:
Type: AWS::SQS::QueuePolicy
Properties:
Queues:
- !Ref InvoiceProcessingTargetDLQ
PolicyDocument:
Statement:
- Action:
- "SQS:SendMessage"
Effect: "Allow"
Resource: !GetAtt InvoiceProcessingTargetDLQ.Arn
Principal:
Service: "events.amazonaws.com"
Condition:
ArnEquals:
"aws:SourceArn": !GetAtt InvoiceProcessingRule.Arn
Conclusion
This post shows you how to use the new features Amazon EventBridge resource policies that make it easier to build applications that work across accounts. Resource policies provide you with a powerful mechanism for modeling your event buses across multiple accounts, and give you fine-grained control over EventBridge API invocations.
Download the code in this blog from https://github.com/aws-samples/amazon-eventbridge-resource-policy-samples.
For more serverless learning resources, visit Serverless Land.