Skip to main content

How policies reach a job

An egress policy defined in the control plane changes how a job behaves without anyone editing that job's workflow file. Three parts of that mechanism don't work the way most readers first assume, and each one can produce a policy that quietly does less than you intended.

The problem policies solve

Before policies, a job's egress configuration lived entirely in its workflow file. Anyone who could open a pull request against that file could loosen it: widen an allowlist, switch block back to audit, or delete the inputs altogether. There was no organization-wide baseline, and no way to tell from one place what your workflows were actually enforcing.

Policies move that decision to people who administer the organization, and leave the workflow file as a default rather than the last word.

A policy replaces the workflow's inputs; it does not merge with them

When a job starts, the action asks the control plane whether a policy applies to it. If one does, its values replace the job's own allowed-domains, allowed-ips, dns-policy, egress-policy, and enable-sudo wholesale. Nothing from the workflow file survives in those five fields.

This trips people up in a specific way: a workflow that carefully allowlists registry.npmjs.org loses that entry the moment a policy matches it. If your policy is scoped broadly and its allowlist is short, you'll break builds that were working. Either scope the policy narrowly, or make sure its allowlist covers what the jobs in scope actually need.

The five fields above are the whole override. Everything else the workflow sets, such as collect-process-info, is untouched.

Every matching policy applies, not just the most specific one

Policies are not a firewall rule list, and there's no first match. Bullfrog finds every policy whose repository, workflow-file, and job patterns all match, and combines them:

  • Allowed domains and allowed IPs are the union of all matches.
  • For each of the three settings, the strictest value across all matches wins: block over audit, allowed-domains-only over any domain, sudo disabled over sudo enabled.

Take an organization-wide policy that allows github.com in audit mode, and a narrower one for the publish job that allows api.github.com, sets block mode, and disables sudo. A publish run gets both domains, block mode, and no sudo.

The direction of that combination matters. Strictness only ratchets up, so adding a policy can never loosen an existing one. But allowlists only grow, so a broad policy's entries are inherited by every narrow policy underneath it. A permissive organization-wide allowlist quietly widens every job you thought you had locked down. Keep broad-scope allowlists minimal.

No policy and no token both mean the workflow decides

Two situations leave a job running on its own configuration:

  • Nothing matches. No policy covers this repository, workflow, and job.
  • The job has no api-token. The action only asks the control plane when it has a token to ask with. A workflow that never received one is unaffected by every policy you write.

The second case is the one to watch. A policy scoped to * / * / * still governs nothing in a repository whose workflows don't pass a token.

The lookup fails open

If the control plane is unreachable, returns an error, or is slow, the action logs a warning and continues with the workflow's own configuration. A control-plane outage never fails a build.

This is a deliberate trade. It means a policy is not a guarantee that holds under every condition: a job that would have run in block mode under a policy runs in whatever mode its workflow file specifies when the lookup fails. For jobs where enforcement matters most, set egress-policy: block in the workflow file too, so the fallback is also the safe state.

What identifies a job

The action sends three values, and the policy's scope fields are matched against them:

SentValue
RepositoryThe repository name, without the owner.
Workflow fileThe path from GITHUB_WORKFLOW_REF, such as .github/workflows/release.yml.
JobGITHUB_JOB, which is the job's id in the workflow file, not its display name.

A job whose YAML reads publish: with name: Publish to npm is matched as publish. Writing Publish to npm in the Job field matches nothing, and because a policy that matches nothing is silently inert, this failure looks exactly like no policy at all.

Before you rely on a policy, run a job in scope and confirm on Workflow Runs that its behavior changed.

See also