- Security & Policies
- Restrict who can emit an event
Restrict who can emit an event
Layer 1 RBAC lets any key with events:emit emit any event name. A Rego deny rule narrows this by name. Two examples follow: an approval event that only one key may emit, and a webhook source that may emit only its own names.
The engine has no approval concept. A name such as deploy-api.approved is an ordinary event name. The rule is yours, and it works for any name.
What the policy sees
Section titled “What the policy sees”Every emit from a user door is checked as events:emit. The doors are Emit, Trigger, TriggerSync, TriggerBatch, the pub/sub Emit, entity appends and webhook ingest. An entity append needs events:emit as well as entities:append. A denied TriggerBatch item reports permission_denied in its error field, and the other items still emit. The resource is the event IRN:
irn:ironflow:{org}:{project}:event:{env}:{name}Ironflow encodes the raw name once when it builds the IRN: literal % becomes %25, then : becomes %3A. An event team:deploy has the resource ...:event:env_default:team%3Adeploy. The literal name team%3Adeploy instead ends in team%253Adeploy. Write the encoded identifier in your patterns and dry-run inputs; do not percent-decode input.request.resource. Function invocation IRNs use the same encoding.
The check runs before the event is stored. A denied emit stores nothing and wakes no run. The caller gets PermissionDenied (HTTP 403) with the message events:emit is not allowed for event "<name>". The message does not carry the reason of your rule. The reason is in the audit log: on the denied row in enforce mode, on the would_deny row in monitor mode, and in a dry-run.
Cron, file events and NATS ingest are not checked. Credentials for NATS are the gate for that path.
Example 1. Only the release key approves a deploy
Section titled “Example 1. Only the release key approves a deploy”package ironflow.authz
release_keys := {"ak_release_bot"}
# Only the release key may emit the API deploy approval.deny contains "only the release key approves deploys" if { input.request.action == "events:emit" endswith(input.request.resource, ":deploy-api.approved") not input.subject.api_key_id in release_keys}Replace ak_release_bot with the ID of your key. Add more IDs to the set to allow more keys.
The route gate sees an Emit RPC with an empty resource. The endswith test is false for an empty string, so the gate passes and the check on the event IRN decides. Other event names, such as deploy-api.started, are not affected.
The rule keys on input.subject.api_key_id, which Ironflow fills for every API-key caller. A dashboard user has no key ID, so this rule denies a dashboard user too. A rule on input.subject.groups (for example, “only the release-managers group”) works only when Ironflow fills groups. They are empty today. See Policy set input.
Example 2. A webhook source emits only its own names
Section titled “Example 2. A webhook source emits only its own names”package ironflow.authz
# The Stripe webhook source may only emit stripe.* names.deny contains "stripe source emits stripe.* only" if { input.request.action == "events:emit" input.subject.webhook_source_id == "whsrc_stripe" not contains(input.request.resource, ":stripe.")}A webhook delivery has no user and no key. Ironflow checks it as the webhook source: input.subject.webhook_source_id holds the source ID, and input.subject.roles is empty. Layer 1 counts the emit as granted, because an administrator created the source. Your rule is the only limit. A denied delivery gets 403 and a rejected delivery record.
The organization comes from the environment of the source. When policy enforcement is on and the environment has no project, or the project has no organization, Ironflow refuses the delivery with 403 and writes an authz.decision.denied row with the reason organization not resolved. It never emits without a policy check. If the policy set cannot be read or evaluated, the delivery gets 503, so the provider retries it.
Dry-run it
Section titled “Dry-run it”Write one input file for the case you want to test. This is the file for a key that emits the approval, named approve.json:
{ "request": { "action": "events:emit", "resource": "irn:ironflow:org_acme:proj_default:event:env_default:deploy-api.approved", "environment": "env_default", "org_id": "org_acme" }, "subject": { "api_key_id": "ak_ci", "roles": ["developer"] }}# expect decision denyironflow policy dry-run ./policies --input approve.jsonChange api_key_id to ak_release_bot and run it again. Expect allow.
ironflow policy push ./policiesA new set starts in monitor mode. A matching emit goes through, and Ironflow writes a would_deny row to the audit log. The row is sampled: one for each org, action and reason in a 10-second window. Read the rows, then switch: ironflow policy push ./policies --mode enforce. See Roll out with monitor mode.
Update existing policies
Section titled “Update existing policies”The correction in #3452 changes resource IRNs for event names and function IDs containing a literal %. Identifiers without %, including those containing only :, keep their IRNs. Stored names, policy sets and historical audit rows are not rewritten.
Review affected rules against the raw identifier. The old segment team%3Adeploy could name either team:deploy or team%3Adeploy, so a global replacement cannot determine the intended resource. Update exact matches, suffixes and dry-run inputs for literal % identifiers using the encoding above.
Stale deny rules can stop matching after the engine upgrade. Pause affected traffic while upgrading the engine and publishing corrected policy sets. Verify both intended and unintended names against the enforcing set before resuming traffic. The server does not also match the old encoding.
See also
Section titled “See also”- Author a policy set
- Limit an agent key to specific functions
- Policy set input
- ADR 0129: emit authorization by event name
- Debug a surprising deny