By default the action keeps everything in the run: the job summary and report.json. With send-events: on it also sends each job's destinations to your OpenWAF organization, where they can be read from the API across repositories and runs.
No secret is stored in the repository. The job proves who it is with a GitHub OIDC token, which OpenWAF exchanges for a token that only lets it send that run's events.
Before you start
- Early access. Egress is turned on per organization; until it is, registration answers
403 feature_disabled. Write to support@openwaf.dev to join. - An organization-wide
adminto register repositories. Reading the registered repositories needs any role of the whole organization. - The repository's GitHub ids, which pin the registration to that repository: a repository renamed, transferred or deleted and created again with the same name is not the same repository and must be registered again.
gh api repos/acme/app --jq '.id, .owner.id'1. Register the repository
A GitHub repository can be registered in one organization only. refs limits which refs may send events (exact refs, no wildcards, up to 20); empty means any.
curl -sS -X POST "https://dash.openwaf.dev/v1/orgs/$ORG/egress/ci/repositories" \
-H "Authorization: Bearer $OPENWAF_TOKEN" -H "Content-Type: application/json" \
-H "X-Change-Reason: CI egress events for acme/app" \
-d '{"repository": "acme/app", "repository_id": 123456789, "repository_owner_id": 987654, "refs": ["refs/heads/main"]}'Answers 201 with the registration (id, repository, repository_id, repository_owner_id, refs, created_at, created_by). GET on the same path lists them under items; DELETE …/egress/ci/repositories/{id} removes one (204), and the tokens it was given stop working at once.
openwaf-cloud ctl egress ci-repos add acme/app --repository-id 123456789 --owner-id 987654 \
--ref refs/heads/main --reason "CI egress events for acme/app"
openwaf-cloud ctl egress ci-repos list
openwaf-cloud ctl egress ci-repos rm acme/app --reason "repository archived"--dry-run shows the change without making it.
Registrations are recorded in the audit log with their reason.
2. Turn it on in the workflow
permissions:
contents: read
id-token: write # lets the job ask GitHub for an OIDC token
jobs:
test:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v5
- uses: openwaf-dev/egress-action@v0.3.2
with:
send-events: "on"
- run: make testAt the end of the job the summary says how many destinations were sent, for example 14 destinations sent to OpenWAF Egress events (https://dash.openwaf.dev).
What is sent
One event per destination of the job, with the run's identity. A name that was only resolved, with nothing connected, refused or detected, is left out.
| Part | Fields |
|---|---|
| Run | Repository, run id and attempt, workflow, job, commit SHA, ref, event name, runner name |
| Summary | Mode, number of destinations, blocked, would block, detections, DNS queries |
| Each destination | Host, first port, path (proxy, direct, container, udp, dns), container name, decision (allow, audit, block), reason, detection ids, connections, bytes up and down |
Never request bodies, headers, URLs or the secrets a detection found.
How it works
- The post step asks GitHub for an OIDC token with the audience
openwaf-egress-ci. It never prints or logs it. - It sends it to
POST /v1/egress/ci/tokenonopenwaf-api(defaulthttps://dash.openwaf.dev). OpenWAF checks GitHub's signature, the issuer, the audience, the time and that the token has not been used before, and that the repository, its id and its owner's id match a registration (and the ref, whenrefsis set). It answers with a token (OWE_CI_…) valid for 15 minutes for that repository only. - It sends the events to
POST /v1/egress/ci/eventsin batches of up to 1000, with up to three attempts per batch. A batch carries abatch_id, so a retry never counts twice.
A failure never fails the job: the summary and the log get a warning such as Events not sent to OpenWAF: token exchange: 403 repository_not_registered.
| Error | Meaning and fix |
|---|---|
GitHub OIDC token: … (the job needs permissions: id-token: write) | Add id-token: write to the workflow's or the job's permissions. Pull requests from forks never get it. |
401 invalid_token | The OIDC token was refused (signature, issuer, audience, time or used twice); or, for events, the OpenWAF token expired. |
403 repository_not_registered | Register the repository; again if it was renamed, transferred or created again. |
403 ref_not_allowed | The run's ref is not in the registration's refs. |
403 feature_disabled | Egress is not enabled for the organization. |
403 repository_mismatch | The run's repository is not the token's. |
429 rate_limited, 503 unavailable | Try again later; the job is not affected. |
See the events
CI events are read from the API. The dashboard's Blocked requests view and the MCP tool egress_events show only the calls of your AI agents that the gateway refused, not CI events.
curl -sS "https://dash.openwaf.dev/v1/orgs/$ORG/egress/events?attack_type=egress_ci&limit=100" \
-H "Authorization: Bearer $OPENWAF_TOKEN"attack_type=egress_ci selects CI events; …/egress/events/stream takes the same filter and follows new ones as Server-Sent Events. In each event:
| Field | Value |
|---|---|
action | allow, log (audit: enforce would have refused it) or block |
rule_name | The reason, such as not_allowed or dlp |
host | The destination (and ip when the destination is an address) |
method | The path: proxy, direct, container, udp or dns |
path | Port, container and counts, such as :443 container=db connections=3 bytes_up=812 bytes_down=6200 |
user_agent | ci:<owner>/<repo>/<workflow>/<job>#<run id> |
msg | The detection ids |