← Back to list

Securing the Event Bus: Fine-Grained Access Control with Policy-as-Code

I have seen this pattern break production more than once: a team ships an EventBridge bus, skips the resource-based policy, and every…

Naeem ul Haq · 2026-05-04 07:20 · 0 claps · 9.9 min read
#eventbus #aws #aws-eventbridge #cloud #technology
Open on Medium ↗
Wiki topics: ☁️ · DevOps & Cloud

Securing the Event Bus: Fine-Grained Access Control with Policy-as-Code

I have seen this pattern break production more than once: a team ships an EventBridge bus, skips the resource-based policy, and every internal service gets implicit PutEvents access. The bus becomes a trusted internal channel by default. That is not a security architecture. That is a suggestion. If you are serious about AWS EventBridge security best practices, you need to start by treating the bus as hostile by default and implementing AWS EventBridge fine-grained access control from day one.

Why open event buses are a security risk

This is a zero-trust violation in the most operational sense. A compromised microservice, or even a misconfigured one, can publish arbitrary events to any target. Lambda functions, Step Functions, SQS queues: all reachable with payloads nobody validated.

The blast radius is concrete. If your order-processing service can publish a fraudulent payment.approved event that bypasses business logic, you have no publisher identity check and no event structure validation. The downstream systems trust the bus. The bus trusts everyone. Understanding how to implement fine-grained access control in AWS EventBridge is the first step toward closing this gap.

I learned to treat every internal boundary as a potential attack surface from watching lateral movement happen inside “trusted” internal networks at previous roles. The event bus is just another boundary, so treat it like one. Treating AWS EventBridge as a security boundary is not optional in a zero-trust architecture.

Attention: EventBridge does not require a resource-based policy for same-account publishing, so any principal with events:PutEvents IAM permission can publish by default. You should enforce least privilege using explicit allow lists, explicit deny can also be used as an additional safeguard.

Measure first. Audit which principals are calling PutEvents today using AWS CloudTrail event history filtered on eventName = PutEvents. Integrating AWS CloudTrail with AWS EventBridge gives you the forensic baseline you need. In one case I found 11 services with implicit access, three of which had no business touching the bus. Expect similar surprises.

The blast radius becomes obvious when you map the fan-out.

Blast radius of an open EventBridge bus with no resource-based policy

Blast radius of an open EventBridge bus with no resource-based policy

You need a way to make access decisions explicit, consistent, and reviewable across services. That requires moving access control out of implicit behavior and into code.

Core concepts in policy-as-code for events

Policy-as-code means your access control rules are versioned, declarative artifacts living in the same repository as your infrastructure. Not ad hoc console clicks that nobody can review. If the policy is not in code, it does not exist for audit purposes. The AWS EventBridge policy-as-code approach ensures that every access decision is reviewable, testable, and auditable.

The model I rely on is **attribute-based access control (ABAC)**. Policies evaluate contextual attributes at request time:

  • Publishing principal: the IAM role ARN of the producing service.
  • Event source: the domain namespace, e.g., com.myapp.orders.
  • Detail-type: the specific event contract, e.g., order.created.

In EventBridge terms, this means defining AWS EventBridge resource-based policies in Terraform or AWS CDK that explicitly enumerate which IAM principals can call events:PutEvents for specific sources and detail-types. The two critical IAM condition keys for EventBridge here are events:detail-type and events:source. These are EventBridge-specific condition keys, not available on all AWS services. Verify support in the IAM condition key reference before building policies around them. If it is not versioned, you cannot detect when it changed.

Practical tip: Treat policy definitions as code artifacts with full review requirements. They belong in pull requests with the same review gates as application code.

Ad hoc console policies simply cannot scale the way code-defined policies can. The table below shows exactly where they fall short.

Ad-hoc Console Policies vs. Policy-as-Code Comparison

Once you define access rules in code, you need a place to enforce them. In EventBridge, that enforcement happens at the event bus.

Resource-based policies on EventBridge

Securing AWS EventBridge with resource-based policies starts at the bus itself. EventBridge resource-based policies attach directly to the event bus. They act as a critical enforcement layer at the bus boundary. If the policy does not explicitly allow a principal, the request is denied.

With events:detail-type and events:source as condition keys, you construct statements that allow only a specific IAM role to publish events where both attributes match exactly. Everything else is denied by default. This is the core of implementing fine-grained access control in AWS EventBridge.

This mirrors ABAC precisely:

  • Principal attribute: arn:aws:iam::123456789:role/OrderService only.
  • Source attribute: must equal com.myapp.orders.
  • Detail-type attribute: must equal order.created.

The result is that even a legitimate internal service cannot publish event types outside its defined domain. A billing service cannot emit order.created. An inventory service cannot emit payment.approved. The policy enforces the AWS EventBridge security boundary structurally.

At Educative, we enforce one rule without exception: every event bus ships with an explicit deny-all default. Services earn publish access through scoped policy statements. The Terraform implementation of this pattern is below.

# Define the EventBridge event bus
resource "aws_cloudwatch_event_bus" "main" {
  name = "my-custom-event-bus"
}

# Secure resource-based policy (allow-only)
resource "aws_cloudwatch_event_bus_policy" "main" {
  event_bus_name = aws_cloudwatch_event_bus.main.name

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [

      # Allow only the trusted publisher role with strict event constraints
      {
        Sid    = "AllowTrustedPublisher"
        Effect = "Allow"
        Principal = {
          AWS = "arn:aws:iam::123456789012:role/MyTrustedPublisherRole"
        }
        Action   = "events:PutEvents"
        Resource = aws_cloudwatch_event_bus.main.arn

        Condition = {
          StringEquals = {
            "events:source"      = "com.mycompany.approved-service"
            "events:detail-type" = "MyApplication.OrderPlaced"
          }
        }
      }

    ]
  })
}

Terraform HCL defining an EventBridge resource-based policy

Resource-based policies lock down what the bus accepts, but they do not constrain what a producer is allowed to attempt. To fully enforce intent, you also need to control the publisher’s permissions at the identity level.

Enforcing publisher identity with IAM

Resource-based policies on the bus are necessary but not sufficient. You need bidirectional enforcement. If X publishes, then X’s own IAM identity policy must also explicitly permit that specific PutEvents action. Two independent checks, both must pass. Understanding what IAM condition keys are and how they apply to AWS EventBridge is essential for getting this right.

IAM identity policies attached to producer services must scope events:PutEvents to specific bus ARNs. This prevents a service from publishing to any bus it can reach, even if the bus policy is misconfigured. Using IAM condition keys for AWS EventBridge security ensures that both sides of the authorization check are airtight.

For cross-account scenarios, three condition keys close lateral movement vectors:

  • aws:SourceAccount: restricts the request to a specific AWS account ID.
  • aws:PrincipalArn: restricts the request to a specific IAM principal ARN.
  • aws:PrincipalTag: enforces tag-based ABAC on the publishing role.
  • aws:RequestedRegion: prevents cross-region publishing to unintended buses.

A concrete case I dealt with: a data pipeline service in a staging account was publishing to a production event bus because nobody scoped the identity policy by account. The fix was a deny statement conditioned on aws:SourceAccount not matching the production account ID. Five minutes to write. Would have prevented a week-long incident investigation.

Attention: Cross-account event publishing requires both the producer’s identity policy and the bus resource policy to allow the action. One permissive policy does not override a restrictive one, but one missing deny can create an unintended allow.

Encoding these constraints in your IaC modules means every new service inherits them by default, rather than relying on someone remembering to add them manually. The diagram below shows how both policy layers interact at request time.

Authorization defines who is allowed to publish, but it does not tell you what actually happened at runtime. To verify enforcement and investigate anomalies, you need a reliable audit trail of event activity.

Auditing event traffic with CloudTrail

Operationalizing AWS EventBridge audit workflows starts with CloudTrail. CloudTrail captures every PutEvents API call as a tamper-evident log: caller ARN, source IP, event source, detail-type, timestamp. This is your forensic foundation. When a policy violation surfaces, the AWS EventBridge CloudTrail integration tells you who published what, when, and from where.

EventBridge Archive complements this by storing full event payloads in S3-backed storage for matched events. The critical capability is Replay: you reconstruct the exact event sequence during an incident window against an isolated bus, without touching production state. Using AWS EventBridge Archive and Replay for auditing gives you a forensic capability that log correlation alone cannot match.

This distinction matters operationally:

  • CloudTrail: tells you the API call metadata, including who, when, and from where.
  • Archive: stores the full event payload, showing what was actually published.
  • Replay: lets you trace downstream state changes in a sandbox.

I used this pattern to investigate a suspicious IAM role publishing unexpected iam.policy.modified events. A custom CloudTrail metric filter on unexpected detail-types triggered the initial alert. We replayed the archived stream in a sandbox, traced the blast radius across three downstream services, and confirmed the scope of the breach in under two hours. This is what operationalizing audit workflows in AWS EventBridge looks like in practice.

Practical tip: Enable EventBridge Archive on every bus that handles sensitive event types. Archive costs $0.10/GB-month for storage, which is negligible for most workloads under a few million events per day, but worth estimating for high-throughput buses. The forensic value during an incident is worth far more than the storage bill.

Without Archive and Replay, that investigation would have taken days of log correlation across CloudWatch, S3 access logs, and Lambda invocation records. Measure first, then replay to confirm. The workflow diagram below shows the full forensic sequence.

Audit trails provide visibility into past activity, but they do not guarantee that policies remain correct. To maintain alignment with intended controls, you need continuous validation across both deployment and runtime.

Continuous policy validation in CI/CD

Policies rot. The configuration you deployed last quarter is not the configuration running today unless you are actively validating it. This is the nuance most teams miss: they implement fine-grained policies and then never check for drift. Enforcing continuous policy validation in AWS EventBridge requires both pre-deploy and runtime checks.

We integrate policy validation into every CI/CD pipeline. Integrating OPA with AWS EventBridge for policy enforcement starts with Open Policy Agent (OPA) and Rego rules that encode organizational invariants as pre-deployment gates. The AWS EventBridge OPA integration uses custom Rego rules we maintain in our policy repo:

  • deny_wildcard_principal: rejects any statement that uses a wildcard Principal (*) without restrictive conditions.
  • deny_overly_permissive_allow: rejects Allow statements that do not constrain at least one of events:source or events:detail-type.

If the Rego check fails, the deploy stops. No exceptions, no manual overrides.

On the runtime side, using AWS Config rules to enforce AWS EventBridge policies catches drift that slips past CI/CD. The managed rule eventbridge-event-bus-cross-account-access-check catches overly permissive cross-account access. Note that AWS EventBridge AWS Config rules run on a periodic schedule (every 1, 3, 6, 12, or 24 hours), not in real time. A permissive policy change can exist for up to 24 hours before triggering an alert. We pair it with custom Config rules that alert on wildcard condition blocks in any bus policy.

Note: OPA evaluates policy intent at deploy time. AWS Config evaluates policy reality at runtime. You need both. Pre-deploy checks prevent bad policies from shipping. Runtime checks catch drift from manual console changes.

The combination creates a closed loop. Policy intent is validated before deployment. Policy reality is validated continuously after. If X drifts from Y, an alert fires. Automate the fix, not the investigation. The OPA Rego rules and CI/CD integration are shown below.

# --- policy.rego ---
# Save this file as policy.rego and reference it in the GitHub Actions.

package eventbridge_policy

import future.keywords.if
import future.keywords.in

############################
# DENY: Wildcard principal without conditions
############################
deny[msg] {
  stmt := input.Statement[_]
  stmt.Principal == "*"
  not stmt.Condition
  msg := sprintf("Statement '%v' uses wildcard Principal without any conditions.", [stmt.Sid])
}

############################
# DENY: Overly permissive Allow (no source or detail-type restriction)
############################
deny[msg] {
  stmt := input.Statement[_]
  stmt.Effect == "Allow"

  not has_event_constraint(stmt)

  msg := sprintf("Allow statement '%v' must restrict at least 'events:source' or 'events:detail-type'.", [stmt.Sid])
}

############################
# DENY: PutEvents not scoped to specific resource
############################
deny[msg] {
  stmt := input.Statement[_]
  stmt.Effect == "Allow"
  stmt.Action == "events:PutEvents"
  stmt.Resource == "*"

  msg := sprintf("Statement '%v' allows PutEvents on all resources.", [stmt.Sid])
}

############################
# WARN: Wildcard principal even with conditions
############################
warn[msg] {
  stmt := input.Statement[_]
  stmt.Principal == "*"
  stmt.Condition
  msg := sprintf("Statement '%v' uses wildcard Principal; verify conditions are strict.", [stmt.Sid])
}

############################
# Helper: Check if source OR detail-type exists
############################
has_event_constraint(stmt) {
  some op
  stmt.Condition[op]["events:source"]
}

has_event_constraint(stmt) {
  some op
  stmt.Condition[op]["events:detail-type"]
}

OPA Rego policy defining pre-deployment validation rules

Defining validation rules is only the first step. You need to enforce them automatically at deployment time so every policy change is evaluated before it is applied:

name: OPA Policy Gate

on:
  push:
    branches: ["main"]
  pull_request:

jobs:
  opa-validate:
    name: Validate EventBridge Policy with OPA
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Install OPA
        run: |
          curl -sL -o /usr/local/bin/opa \
            https://openpolicyagent.org/downloads/latest/opa_linux_amd64_static
          chmod +x /usr/local/bin/opa

      - name: Evaluate policy (fail on deny)
        run: |
          opa eval \
            --data policy.rego \
            --input policy-input.json \
            --format pretty \
            --fail-defined \
            'data.eventbridge_policy.deny'

CI/CD pipeline integration enforcing OPA policy checks

At this point, the individual controls are in place. The final step is to treat them as a cohesive system and enforce them consistently as part of your overall security posture.

Treat the bus as a security boundary

The pattern for AWS EventBridge fine-grained access control is complete, and each layer is non-negotiable:

  • Resource-based policies: on the bus enforce who can publish what event types.
  • IAM identity policies: on producers enforce bidirectional constraints and account scoping.
  • CloudTrail, Archive, and Replay: give you forensic capability when something goes wrong.
  • OPA and AWS Config: close the drift loop before and after deployment.

Skip any layer and you have a gap. The event bus is not a trusted internal channel.

Start with an audit of your current PutEvents callers. Measure the gap between what is allowed and what should be allowed. Then encode the fix in code, validate it in CI/CD, and monitor it in production. This is what AWS EventBridge security best practices look like in practice: ongoing operational work, not a one-time hardening exercise. Teams that treat security as a structural property, not an afterthought, are the ones who sleep through their on-call rotations.


메타데이터
post_id
5e92d9b7e4e3
slug
securing-the-event-bus-fine-grained-access-control-with-policy-as-code-5e92d9b7e4e3
url
https://medium.com/@naeemulhaq/securing-the-event-bus-fine-grained-access-control-with-policy-as-code-5e92d9b7e4e3
canonical_url
https://medium.com/@naeemulhaq/securing-the-event-bus-fine-grained-access-control-with-policy-as-code-5e92d9b7e4e3
author_url
https://medium.com/@naeemulhaq
status
ok
fetched_at
2026-06-20 20:29:01