-
Notifications
You must be signed in to change notification settings - Fork 1k
New pattern - lambda-verified-permissions-cdk #3104
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
16bb2ea
10a17eb
3e54cc2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| # Amazon EventBridge Data Plane Logging with AWS CloudTrail | ||
|
|
||
| This pattern enables CloudTrail data plane logging for Amazon EventBridge and triggers a Lambda function when PutEvents API calls are detected, providing security and operational visibility into event bus activity. | ||
|
|
||
| Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/eventbridge-cloudtrail-dataplane-cdk | ||
|
|
||
| Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. | ||
|
|
||
| ## Requirements | ||
|
|
||
| * [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured | ||
| * [AWS CDK](https://docs.aws.amazon.com/cdk/latest/guide/cli.html) installed | ||
| * [Node.js](https://nodejs.org/en/download/) installed | ||
|
|
||
| ## Deployment Instructions | ||
|
|
||
| 1. Clone and navigate to the pattern: | ||
| ``` | ||
| cd serverless-patterns/eventbridge-cloudtrail-dataplane-cdk | ||
| npm install | ||
| ``` | ||
| 2. Deploy: | ||
| ``` | ||
| cdk deploy | ||
| ``` | ||
|
|
||
| ## How it works | ||
|
|
||
| - A CloudTrail trail is created with data event logging enabled | ||
| - EventBridge data plane API calls (PutEvents) are now logged to CloudTrail (new May 2026 feature) | ||
| - An EventBridge rule captures these CloudTrail events matching `aws.events` source with `PutEvents` event name | ||
| - A Lambda function processes the events, logging the caller identity, source IP, event bus, and entry count | ||
| - This enables security teams to audit who is putting events to which bus | ||
|
|
||
| ## Testing | ||
|
|
||
| ```bash | ||
| # Put a test event to the default event bus | ||
| aws events put-events --entries '[{"Source":"test.app","DetailType":"TestEvent","Detail":"{\"key\":\"value\"}"}]' | ||
|
|
||
| # Check Lambda logs (allow ~5 minutes for CloudTrail delivery) | ||
| aws logs tail /aws/lambda/$(aws cloudformation describe-stacks \ | ||
| --stack-name EventbridgeCloudtrailDataplaneStack \ | ||
| --query 'Stacks[0].Outputs[?OutputKey==`ProcessorFunctionName`].OutputValue' --output text) \ | ||
| --follow | ||
| ``` | ||
|
|
||
| ## Cleanup | ||
|
|
||
| ``` | ||
| cdk destroy | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
|
|
||
| SPDX-License-Identifier: MIT-0 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| #!/usr/bin/env node | ||
| import 'source-map-support/register'; | ||
| import * as cdk from 'aws-cdk-lib'; | ||
| import { EventbridgeCloudtrailDataplaneStack } from '../lib/eventbridge-cloudtrail-dataplane-stack'; | ||
|
|
||
| const app = new cdk.App(); | ||
| new EventbridgeCloudtrailDataplaneStack(app, 'EventbridgeCloudtrailDataplaneStack'); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| { | ||
| "app": "npx ts-node --prefer-ts-exts bin/app.ts" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| { | ||
| "title": "Amazon EventBridge Data Plane Logging with AWS CloudTrail", | ||
| "description": "Monitor EventBridge PutEvents API calls using CloudTrail data plane logging with Lambda alerting for security and operational visibility.", | ||
| "language": "TypeScript", | ||
| "level": "300", | ||
| "framework": "CDK", | ||
| "introBox": { | ||
| "headline": "How it works", | ||
| "text": [ | ||
| "This pattern enables CloudTrail data plane logging for Amazon EventBridge (launched May 2026).", | ||
| "CloudTrail captures PutEvents API calls and delivers them as events to EventBridge.", | ||
| "An EventBridge rule matches these CloudTrail events and triggers a Lambda function for alerting.", | ||
| "This provides visibility into who is putting events, from where, and how many — essential for security auditing." | ||
| ] | ||
| }, | ||
| "gitHub": { | ||
| "template": { | ||
| "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/eventbridge-cloudtrail-dataplane-cdk", | ||
| "templateURL": "serverless-patterns/eventbridge-cloudtrail-dataplane-cdk", | ||
| "projectFolder": "eventbridge-cloudtrail-dataplane-cdk", | ||
| "templateFile": "lib/eventbridge-cloudtrail-dataplane-stack.ts" | ||
| } | ||
| }, | ||
| "resources": { | ||
| "bullets": [ | ||
| { "text": "EventBridge Data Plane CloudTrail Logging", "link": "https://aws.amazon.com/about-aws/whats-new/2026/05/amazon-eventbridge-data-aws-cloudtrail/" }, | ||
| { "text": "CloudTrail Data Events", "link": "https://docs.aws.amazon.com/awscloudtrail/latest/userguide/logging-data-events-with-cloudtrail.html" } | ||
| ] | ||
| }, | ||
| "deploy": { "text": ["cdk deploy"] }, | ||
| "testing": { "text": ["See the README for testing instructions."] }, | ||
| "cleanup": { "text": ["cdk destroy"] }, | ||
| "authors": [ | ||
| { | ||
| "name": "Nithin Chandran R", | ||
| "bio": "Technical Account Manager at AWS, passionate about serverless and AI/ML.", | ||
| "linkedin": "nithin-chandran-r" | ||
| } | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| import * as cdk from 'aws-cdk-lib'; | ||
| import * as events from 'aws-cdk-lib/aws-events'; | ||
| import * as targets from 'aws-cdk-lib/aws-events-targets'; | ||
| import * as lambda from 'aws-cdk-lib/aws-lambda'; | ||
| import * as logs from 'aws-cdk-lib/aws-logs'; | ||
| import * as cloudtrail from 'aws-cdk-lib/aws-cloudtrail'; | ||
| import * as s3 from 'aws-cdk-lib/aws-s3'; | ||
| import { Construct } from 'constructs'; | ||
|
|
||
| export class EventbridgeCloudtrailDataplaneStack extends cdk.Stack { | ||
| constructor(scope: Construct, id: string, props?: cdk.StackProps) { | ||
| super(scope, id, props); | ||
|
|
||
| // S3 bucket for CloudTrail logs | ||
| const trailBucket = new s3.Bucket(this, 'TrailBucket', { | ||
| removalPolicy: cdk.RemovalPolicy.DESTROY, | ||
| autoDeleteObjects: true, | ||
| enforceSSL: true, | ||
| }); | ||
|
|
||
| // CloudTrail trail with data events for EventBridge | ||
| const trail = new cloudtrail.Trail(this, 'EventBridgeDataPlaneTrail', { | ||
| bucket: trailBucket, | ||
| trailName: 'eventbridge-dataplane-trail', | ||
| isMultiRegionTrail: false, | ||
| }); | ||
|
|
||
| // Enable EventBridge data plane events logging | ||
| trail.addEventSelector(cloudtrail.DataResourceType.LAMBDA_FUNCTION, ['arn:aws:lambda']); | ||
|
|
||
| // Lambda function to process CloudTrail events | ||
| const processor = new lambda.Function(this, 'EventProcessor', { | ||
| runtime: lambda.Runtime.NODEJS_20_X, | ||
| handler: 'index.handler', | ||
| code: lambda.Code.fromAsset('src'), | ||
| timeout: cdk.Duration.seconds(10), | ||
| loggingFormat: lambda.LoggingFormat.JSON, | ||
| }); | ||
|
|
||
| // EventBridge rule to capture EventBridge PutEvents API calls from CloudTrail | ||
| const rule = new events.Rule(this, 'DataPlaneRule', { | ||
| eventPattern: { | ||
| source: ['aws.events'], | ||
| detailType: ['AWS API Call via CloudTrail'], | ||
| detail: { | ||
| eventSource: ['events.amazonaws.com'], | ||
| eventName: ['PutEvents'], | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| rule.addTarget(new targets.LambdaFunction(processor)); | ||
|
|
||
| new cdk.CfnOutput(this, 'ProcessorFunctionName', { value: processor.functionName }); | ||
| new cdk.CfnOutput(this, 'TrailBucketName', { value: trailBucket.bucketName }); | ||
| new cdk.CfnOutput(this, 'RuleName', { value: rule.ruleName }); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| { | ||
| "name": "eventbridge-cloudtrail-dataplane-cdk", | ||
| "version": "1.0.0", | ||
| "bin": { "app": "bin/app.ts" }, | ||
| "scripts": { "build": "tsc", "cdk": "cdk" }, | ||
| "dependencies": { | ||
| "aws-cdk-lib": "^2.180.0", | ||
| "constructs": "^10.0.0", | ||
| "source-map-support": "^0.5.21" | ||
| }, | ||
| "devDependencies": { | ||
| "typescript": "~5.4.0", | ||
| "ts-node": "^10.9.0", | ||
| "@types/node": "^20.0.0" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| exports.handler = async (event) => { | ||
| const detail = event.detail || {}; | ||
| console.log(JSON.stringify({ | ||
| message: 'EventBridge data plane API call detected', | ||
| eventName: detail.eventName, | ||
| eventSource: detail.eventSource, | ||
| sourceIPAddress: detail.sourceIPAddress, | ||
| userAgent: detail.userAgent, | ||
| userIdentity: detail.userIdentity?.arn, | ||
| eventBusName: detail.requestParameters?.entries?.[0]?.eventBusName || 'default', | ||
| entryCount: detail.requestParameters?.entries?.length || 0, | ||
| eventTime: detail.eventTime, | ||
| })); | ||
| return { statusCode: 200 }; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| { | ||
| "compilerOptions": { | ||
| "target": "ES2020", "module": "commonjs", "lib": ["es2020"], | ||
| "declaration": true, "strict": true, "outDir": "build", | ||
| "rootDir": ".", "skipLibCheck": true, "forceConsistentCasingInFileNames": true | ||
| }, | ||
| "exclude": ["node_modules", "build"] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| # Amazon Verified Permissions with AWS Lambda | ||
|
|
||
| This pattern deploys a Lambda function that authorizes requests using Amazon Verified Permissions with Cedar policies for fine-grained access control. | ||
|
|
||
| Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/lambda-verified-permissions-cdk | ||
|
|
||
| Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. | ||
|
|
||
| ## Requirements | ||
|
|
||
| * [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured | ||
| * [Node.js 22+](https://nodejs.org/en/download/) installed | ||
| * [AWS CDK v2](https://docs.aws.amazon.com/cdk/v2/guide/getting-started.html) installed | ||
|
|
||
| ## Architecture | ||
|
|
||
| ``` | ||
| ┌──────────┐ ┌──────────────────┐ ┌─────────────────────────┐ | ||
| │ Client │────▶│ AWS Lambda │────▶│ Amazon Verified │ | ||
| │ │ │ (Authorizer) │ │ Permissions │ | ||
| └──────────┘ └──────────────────┘ │ (Cedar Policy Store) │ | ||
| └─────────────────────────┘ | ||
| ``` | ||
|
|
||
| ## How it works | ||
|
|
||
| 1. Lambda receives an authorization request with user identity, action, and resource. | ||
| 2. Lambda calls the Verified Permissions `IsAuthorized` API with the request context. | ||
| 3. Cedar policies evaluate the request and return ALLOW or DENY. | ||
| 4. The pattern includes two policies: admins can perform any action, readers can only read. | ||
|
|
||
| ## Deployment | ||
|
|
||
| ```bash | ||
| npm install | ||
| cdk deploy | ||
| ``` | ||
|
|
||
| ## Testing | ||
|
|
||
| ```bash | ||
| python3 -c " | ||
| import boto3, json | ||
| client = boto3.client('lambda') | ||
| # Admin can delete (ALLOW) | ||
| r = client.invoke(FunctionName='<FunctionName>', Payload=json.dumps({'body': json.dumps({'userId':'alice','role':'admin','action':'Delete','resourceId':'doc-1','classification':'confidential'})})) | ||
| print('Admin Delete:', json.loads(json.loads(r['Payload'].read())['body'])['decision']) | ||
| # Reader cannot delete (DENY) | ||
| r = client.invoke(FunctionName='<FunctionName>', Payload=json.dumps({'body': json.dumps({'userId':'bob','role':'reader','action':'Delete','resourceId':'doc-2','classification':'public'})})) | ||
| print('Reader Delete:', json.loads(json.loads(r['Payload'].read())['body'])['decision']) | ||
| " | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The snippet uses
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed. Replaced the boto3 |
||
| ``` | ||
|
|
||
| ## Cleanup | ||
|
|
||
| ```bash | ||
| cdk destroy | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| #!/usr/bin/env node | ||
| import 'source-map-support/register'; | ||
| import * as cdk from 'aws-cdk-lib'; | ||
| import { LambdaVerifiedPermissionsStack } from '../lib/lambda-verified-permissions-stack'; | ||
| const app = new cdk.App(); | ||
| new LambdaVerifiedPermissionsStack(app, 'LambdaVerifiedPermissionsStack'); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| {"app":"npx ts-node --prefer-ts-exts bin/app.ts"} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| {"title":"Amazon Verified Permissions with AWS Lambda","description":"Deploy a Lambda function that authorizes requests using Amazon Verified Permissions Cedar policies.","language":"TypeScript","level":"300","framework":"CDK","introBox":{"headline":"How it works","text":["Lambda receives an authorization request and calls Amazon Verified Permissions IsAuthorized API with Cedar policies to make fine-grained access control decisions."]},"gitHub":{"template":{"repoURL":"https://github.com/aws-samples/serverless-patterns/tree/main/lambda-verified-permissions-cdk","templateURL":"serverless-patterns/lambda-verified-permissions-cdk","projectFolder":"lambda-verified-permissions-cdk"}},"resources":{"bullets":[{"text":"Amazon Verified Permissions","link":"https://docs.aws.amazon.com/verifiedpermissions/latest/userguide/what-is-avp.html"}]},"deploy":{"text":["cdk deploy"],"commands":["npm install","cdk deploy"]},"testing":{"text":["Invoke the function URL with authorization request"]},"cleanup":{"text":["cdk destroy"],"commands":["cdk destroy"]},"authors":[{"name":"Nithin Chandran R","bio":"Technical Account Manager at AWS","linkedin":"nithin-chandran-r"}],"services":{"from":[{"service":"lambda"}],"to":[{"service":"verifiedpermissions"}]}} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| import * as cdk from 'aws-cdk-lib'; | ||
| import * as lambda from 'aws-cdk-lib/aws-lambda'; | ||
| import * as iam from 'aws-cdk-lib/aws-iam'; | ||
| import * as verifiedpermissions from 'aws-cdk-lib/aws-verifiedpermissions'; | ||
| import { Construct } from 'constructs'; | ||
|
|
||
| export class LambdaVerifiedPermissionsStack extends cdk.Stack { | ||
| constructor(scope: Construct, id: string, props?: cdk.StackProps) { | ||
| super(scope, id, props); | ||
|
|
||
| // Create policy store with Cedar schema | ||
| const policyStore = new verifiedpermissions.CfnPolicyStore(this, 'PolicyStore', { | ||
| validationSettings: { mode: 'STRICT' }, | ||
| schema: { | ||
| cedarJson: JSON.stringify({ | ||
| 'MyApp': { | ||
| entityTypes: { | ||
| User: { shape: { type: 'Record', attributes: { role: { type: 'String' } } } }, | ||
| Document: { shape: { type: 'Record', attributes: { owner: { type: 'String' }, classification: { type: 'String' } } } } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cedar schema declares Document.owner and Document.classification but no policy seems to use them
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed. Added two policies that use these attributes:
These demonstrate the value of the schema attributes and provide a more realistic authorization model. |
||
| }, | ||
| actions: { | ||
| Read: { appliesTo: { principalTypes: ['User'], resourceTypes: ['Document'] } }, | ||
| Write: { appliesTo: { principalTypes: ['User'], resourceTypes: ['Document'] } }, | ||
| Delete: { appliesTo: { principalTypes: ['User'], resourceTypes: ['Document'] } } | ||
| } | ||
| } | ||
| }) | ||
| } | ||
| }); | ||
|
|
||
| // Create policies | ||
| new verifiedpermissions.CfnPolicy(this, 'AdminPolicy', { | ||
| policyStoreId: policyStore.attrPolicyStoreId, | ||
| definition: { | ||
| static: { | ||
| statement: 'permit(principal, action, resource) when { principal.role == "admin" };', | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cedar Admin policy is overly broad ("permit any action by anyone with role==admin")
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed. Scoped the admin policy to explicit actions: |
||
| description: 'Admins can perform any action' | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| new verifiedpermissions.CfnPolicy(this, 'ReaderPolicy', { | ||
| policyStoreId: policyStore.attrPolicyStoreId, | ||
| definition: { | ||
| static: { | ||
| statement: 'permit(principal, action == MyApp::Action::"Read", resource) when { principal.role == "reader" };', | ||
| description: 'Readers can only read documents' | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| // Lambda authorizer function | ||
| const authFn = new lambda.Function(this, 'AuthorizerFn', { | ||
| runtime: lambda.Runtime.NODEJS_22_X, | ||
| handler: 'index.handler', | ||
| code: lambda.Code.fromAsset('src'), | ||
| environment: { POLICY_STORE_ID: policyStore.attrPolicyStoreId }, | ||
| timeout: cdk.Duration.seconds(10) | ||
| }); | ||
|
|
||
| authFn.addToRolePolicy(new iam.PolicyStatement({ | ||
| actions: ['verifiedpermissions:IsAuthorized'], | ||
| resources: [`arn:aws:verifiedpermissions::${this.account}:policy-store/${policyStore.attrPolicyStoreId}`] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This hardcodes the aws partition and will fail in other partitions like aws-us-gov/aws-cn.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed. Replaced the hardcoded |
||
| })); | ||
|
|
||
| const fnUrl = authFn.addFunctionUrl({ authType: lambda.FunctionUrlAuthType.AWS_IAM }); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The stack exposes the Lambda via a Function URL with Pick one direction (don't ship both):
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed. Rewrote the Testing section to explain that the Function URL uses AWS_IAM auth requiring SigV4-signed requests, and replaced the old test snippet with |
||
|
|
||
| new cdk.CfnOutput(this, 'FunctionUrl', { value: fnUrl.url }); | ||
| new cdk.CfnOutput(this, 'PolicyStoreId', { value: policyStore.attrPolicyStoreId }); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| { | ||
| "name": "lambda-verified-permissions-cdk", | ||
| "version": "1.0.0", | ||
| "bin": { "app": "bin/app.js" }, | ||
| "scripts": { "build": "tsc", "cdk": "cdk" }, | ||
| "dependencies": { | ||
| "aws-cdk-lib": "^2.180.0", | ||
| "constructs": "^10.0.0" | ||
| }, | ||
| "devDependencies": { | ||
| "typescript": "~5.4.0", | ||
| "@types/node": "^20.0.0" | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add an architecture diagram image instead of ASCII image
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will add architecture diagram image in next revision. Updated README to reference
architecture.pngso it renders automatically once the image is committed.