Cross-account event buses are one of the most powerful integration patterns in AWS, and an easy place to make an access policy mistake. A single overly permissive Allow statement can let principals from outside your organization publish events to your bus. With k9-cdk v2.2, you can generate least-privilege EventBridge bus policies and restrict access to principals within your AWS Organization, all using the same access capability model you already use for S3, KMS, DynamoDB, and SQS.
The problem: event bus policies are tricky to get right
When you share an EventBridge event bus across accounts, you need a resource policy that grants the right principals access to publish events, read configuration, and manage the bus. Writing these policies by hand means choosing the right IAM actions for each use case, keeping the principal list accurate, and remembering to add an explicit Deny so that a future Allow statement can’t silently widen access.
Most teams start with a permissive Allow and plan to tighten it later. Later rarely comes. And when another team adds a rule target or a new integration, the policy grows without anyone reviewing whether the new principals should have access. For buses that receive events from partner accounts or other organizations, the risk compounds: there is no built-in mechanism to enforce that only principals from your organization or collaborating organizations can interact with the bus.
What’s new in v2.2
EventBridge event bus policy generator
k9-cdk now generates least-privilege resource policies for Amazon EventBridge event buses. You declare who should have access and what they should be able to do using the k9 access capability model. k9-cdk generates the Allow statements with the correct IAM actions, a DenyEveryoneElse statement to block unlisted principals, and validates that at least one principal can administer and read configuration so you don’t lock yourself out.
EventBridge supports three access capabilities:
| Capability | IAM Actions |
|---|---|
administer-resource
| events:DeleteRule, events:DisableRule, events:EnableRule, events:PutRule, events:PutTargets, events:RemoveTargets, events:TagResource, events:UntagResource |
read-config
| events:DescribeEventBus, events:DescribeRule, events:ListTagsForResource, events:ListTargetsByRule |
write-data
|
events:PutEvents
|
The EventBridge bus policy generator does not support the read-data or delete-data capabilities because EventBridge does not support those permissions in event bus policies.
Here is how you secure an EventBridge bus with k9-cdk:
import * as cdk from "aws-cdk-lib";
import * as events from "aws-cdk-lib/aws-events";
import * as k9 from "@k9securityio/k9-cdk";
const app = new cdk.App();
const stack = new cdk.Stack(app, 'K9Example');
const bus = new events.EventBus(stack, 'AppEventBus', {
eventBusName: 'app-bus-with-k9-policy',
});
k9.events.grantAccessViaResourcePolicy({
bus: bus,
k9DesiredAccess: [
{
accessCapabilities: [
k9.k9policy.AccessCapability.ADMINISTER_RESOURCE,
k9.k9policy.AccessCapability.READ_CONFIG,
],
allowPrincipalArns: [
"arn:aws:iam::123456789012:role/ci",
"arn:aws:iam::123456789012:role/platform",
],
},
{
accessCapabilities: k9.k9policy.AccessCapability.WRITE_DATA,
allowPrincipalArns: [
"arn:aws:iam::123456789012:role/app-backend",
],
},
],
});
This generates a policy with three Allow statements (one per capability) and a DenyEveryoneElse statement. The Allow statements use aws:PrincipalArn conditions so that only the listed principals can perform the allowed actions. The Deny statement blocks all other principals from performing any events:* action on the bus, with an exception for AWS service principals so that EventBridge’s own internal operations continue to work.
The generated policy looks like this (simplified for clarity; see the full generated event bus policy on GitHub):
{
"Statement": [
{
"Sid": "AllowRestrictedAdministerResource",
"Effect": "Allow",
"Principal": {
"AWS": "*"
},
"Action": [
"events:DeleteRule",
"events:DisableRule",
"events:EnableRule",
"events:PutRule",
"events:PutTargets",
"events:RemoveTargets",
"events:TagResource",
"events:UntagResource"
],
"Condition": {
"ArnEquals": {
"aws:PrincipalArn": [
"arn:aws:iam::123456789012:role/ci",
"arn:aws:iam::123456789012:role/platform"
]
}
}
},
{
"Sid": "AllowRestrictedWriteData",
"Effect": "Allow",
"Principal": {
"AWS": "*"
},
"Action": "events:PutEvents",
"Condition": {
"ArnEquals": {
"aws:PrincipalArn": [
"arn:aws:iam::123456789012:role/app-backend"
]
}
}
},
{
"Sid": "DenyEveryoneElse",
"Effect": "Deny",
"Principal": {
"AWS": "*"
},
"Action": "events:*",
"Condition": {
"Bool": {
"aws:PrincipalIsAWSService": [
"false"
]
},
"ArnNotEquals": {
"aws:PrincipalArn": [
"arn:aws:iam::123456789012:root",
"arn:aws:iam::123456789012:role/ci",
"arn:aws:iam::123456789012:role/platform",
"arn:aws:iam::123456789012:role/app-backend"
]
}
}
}
]
}
The DenyEveryoneElse statement is the safety net. If someone adds another Allow statement to the bus policy outside of k9-cdk, the explicit Deny still blocks principals who are not in the approved list.
Restrict access to your AWS Organization
The second feature in v2.2 works across all supported services: you can now restrict access capabilities to principals within specific AWS Organizations using restrictToPrincipalOrgIDs.
When you set restrictToPrincipalOrgIDs on an access spec, k9-cdk does two things:
- Adds a
StringEqualscondition onaws:PrincipalOrgIDto the Allow statement, so only principals from the specified org(s) can match - Generates a
DenyUntrustedOrgsstatement that explicitly denies the org-restricted actions for principals outside the specified organizations
This is defense-in-depth. The Allow-side condition provides an implicit deny for principals outside the org: they simply don’t match the Allow. But implicit deny only works as long as no other Allow statement grants access without an org constraint. The DenyUntrustedOrgs statement adds an explicit deny that blocks untrusted org principals regardless of what other Allow statements exist in the policy.
Here is how you use org scoping with an EventBridge bus, allowing any principal in your organization to publish events:
k9.events.grantAccessViaResourcePolicy({
bus: bus,
k9DesiredAccess: [
{
accessCapabilities: [
k9.k9policy.AccessCapability.ADMINISTER_RESOURCE,
k9.k9policy.AccessCapability.READ_CONFIG,
],
allowPrincipalArns: [
"arn:aws:iam::123456789012:role/ci",
"arn:aws:iam::123456789012:role/platform",
],
},
{
accessCapabilities: k9.k9policy.AccessCapability.WRITE_DATA,
allowPrincipalArns: ["*"],
restrictToPrincipalOrgIDs: ["o-abc123"],
},
],
});
The write-data Allow statement uses a StringEquals condition on aws:PrincipalOrgID instead of aws:PrincipalArn, so any principal in org o-abc123 can call events:PutEvents. The generated DenyUntrustedOrgs statement denies events:PutEvents for principals outside that org:
{
"Sid": "DenyUntrustedOrgs",
"Effect": "Deny",
"Principal": { "AWS": "*" },
"Action": "events:PutEvents",
"Condition": {
"StringNotEquals": {
"aws:PrincipalOrgID": ["o-abc123"]
},
"Bool": { "aws:PrincipalIsAWSService": ["false"] }
}
}
You can also combine specific principal ARNs with org constraints. In this case, both conditions must be satisfied: the caller must match the principal ARN and be in the specified org.
{
accessCapabilities: k9.k9policy.AccessCapability.WRITE_DATA,
allowPrincipalArns: [
"arn:aws:iam::123456789012:role/app-backend",
],
restrictToPrincipalOrgIDs: ["o-abc123"],
}
A note on wildcard principals and DenyEveryoneElse: When you use a wildcard * principal with restrictToPrincipalOrgIDs, k9-cdk does not generate a DenyEveryoneElse statement. The DenyEveryoneElse statement works by excepting specific principal ARNs from the deny, but a * wildcard cannot be meaningfully excepted. In this case, access is constrained by the aws:PrincipalOrgID condition on the Allow statements and the DenyUntrustedOrgs deny statement. If you want both DenyEveryoneElse and org scoping, use specific principal ARNs (with wildcards via ArnLike if needed) instead of *:
{
accessCapabilities: k9.k9policy.AccessCapability.WRITE_DATA,
allowPrincipalArns: [
"arn:aws:iam::*:role/*publisher*",
],
test: "ArnLike",
restrictToPrincipalOrgIDs: ["o-abc123"],
}
How the pieces fit together
The following diagram shows how k9-cdk generates policy statements from your access specs:

Figure 1. Flowchart showing how the access spec affects generation of policies
Supported services
Organization-scoped access with restrictToPrincipalOrgIDs is available across all five supported AWS services:
| Service | Capabilities | Org Scoping |
|---|---|---|
| S3 | administer-resource, read-config, read-data, write-data, delete-data | Yes |
| KMS | administer-resource, read-config, read-data, write-data, delete-data | Yes |
| DynamoDB | administer-resource, read-config, read-data, write-data, delete-data | Yes |
| SQS | administer-resource, read-config, read-data, write-data, delete-data | Yes |
| EventBridge | administer-resource, read-config, write-data | Yes |
Getting started
Install k9-cdk v2.2:
npm install @k9securityio/k9-cdk@^2.2.0
Check out the working examples for complete usage of all five service modules, including EventBridge and org-scoped access. The k9-cdk README covers the full API.
Wrapping up
With k9-cdk v2.2, you can generate least-privilege EventBridge bus policies and add AWS Organization boundaries to any k9-cdk resource policy. Together, these features give you two layers of protection: DenyEveryoneElse blocks unlisted principals, and DenyUntrustedOrgs blocks principals from outside your organization. Try it on your next EventBridge bus or add restrictToPrincipalOrgIDs to an existing S3, KMS, DynamoDB, or SQS policy.
Recent Comments