Skip to content

Governing GitHub Actions Across an Enterprise

Lesson 4 of 9Advanced16 min readGit at Scale & Enterprise Engineering · Enterprise OperationsVerified: GitHub Actions enterprise policy documentation, September 2026

CI is the part of an engineering estate that executes arbitrary code with access to credentials, on machines that can reach production.

At enterprise scale it is also the part with the least governance, because it grew organically, every team configured their own, and nobody has ever enumerated what runs.

Four control surfaces, in the order they matter.

Which actions may run. Enterprise and organisation policy can restrict Actions to those created by GitHub, verified creators, or an explicit allow list. This is the supply-chain control.

What the workflow token can do. The default permission set is configurable, and setting it to read-only by default is the single highest-value change available.

How untrusted code is isolated. Fork pull requests, third-party actions, and the boundary between running code and holding secrets.

Where workflows run. Runner groups, self-hosted runners and what they can reach. See runner groups.

A fifth surface is emerging. GitHub documents Actions policies and workflow execution protections, both as being in public preview and subject to change, built on the rulesets framework. See Actions policies.

Worth establishing, because Actions governance competes for attention with more visible security work.

It executes arbitrary code. Not analysed code, not reviewed code in the sense a dependency is reviewed — code that runs, with a shell, on a machine.

It holds credentials. Cloud keys, registry tokens, deployment credentials, signing keys. By design, because deployment requires them.

It has network access. To the internet by default, and to internal networks on self-hosted runners.

It runs continuously and unattended. Thousands of times a day, with nobody watching any individual run.

It is configured by every team independently, in files that are part of the repositories they build.

And it is the path to production. Whatever CI can deploy, a compromise of CI can deploy.

Put together, that is the description of the highest-value target in an engineering organisation — and it is frequently governed less rigorously than the repositories it builds.

The framing that lands with a security team: every workflow is a production system with credentials, and there are four hundred of them, each maintained by whoever wrote it. Stated that way, the case for governance makes itself.

A workflow step that uses a third-party action is executing somebody else’s code, with whatever permissions the job has, on a machine inside your network boundary.

That is a dependency, and it deserves the same scrutiny as any other. In practice it usually gets less, because adding an action is one line and adding a library goes through a review.

The controls available:

Restrict to GitHub-created actions. The tightest option, and impractical for most organisations.

Restrict to verified creators. Broader, and a meaningful bar.

An explicit allow list. The most workable option for an enterprise that wants control without blocking work — a curated set of actions, extended by request.

Allow everything. The default in many organisations, and a decision worth making deliberately rather than inheriting.

Pin by SHA, not by tag. A tag is mutable; the action’s author can move v3 to point at different code. Pinning to a commit SHA means the code you reviewed is the code that runs. This is a workflow-authoring practice rather than an enterprise setting, and it is what the enterprise setting cannot achieve on its own.

Every workflow run receives a token, and its default permission set is configurable at enterprise, organisation and repository level.

Set it to read-only. A workflow that needs to write requests the permission explicitly in its definition. Most workflows do not need to write anything.

Why this matters more than it appears: a workflow token with write access to contents can push commits. One with write access to actions can modify workflows. A compromised step in any workflow inherits whatever the job’s token holds.

Read-only by default converts a broad standing grant into an explicit, reviewable request. It is one setting, it applies estate-wide, and the migration cost is that workflows needing write permissions must declare them — which is a change teams can make incrementally.

Restrict whether workflows can approve pull requests. A workflow able to approve is a workflow able to satisfy a required review, which defeats the review control entirely.

Job-level permissions are better than workflow-level. A workflow with several jobs, only one of which needs write access, should grant it to that job alone.

Workflows triggered by pull requests from forks run with restricted permissions and without repository secrets. This is correct and it is the boundary that makes public and internal repositories safe to accept contributions to.

The consequence for innersource: contributions may fail CI if the pipeline needs secrets. See innersource.

The dangerous workaround is a workflow that checks out and executes fork code while holding secrets or elevated permissions. This is one of the best-known CI vulnerabilities, and it converts “anyone who can open a pull request” into “anyone who can execute code with our credentials”.

Approval requirements for first-time contributors add a human gate before workflows run on a fork pull request. Reasonable, and it adds latency to exactly the contributions you want to encourage.

Repository and organisation secrets are available to any workflow in scope. Which means every workflow in a repository can read every repository secret, including a workflow added by a pull request from somebody with write access.

Environment secrets are the better mechanism for anything sensitive. An environment can require reviewers, restrict which branches may deploy to it, and impose a wait timer. A production credential in an environment with required reviewers is gated by a human; the same credential as a repository secret is not.

Use OIDC instead of stored cloud credentials. A workflow can exchange its identity for short-lived cloud credentials rather than holding a long-lived key. This removes an entire category of stored secret, and it is the highest-value change available for any estate deploying to a cloud.

Audit stored secrets. Names and last-updated dates are visible even though values are not. An estate accumulates secrets nobody can account for, and the list is the starting point.

Never echo a secret. Masking helps and is not a guarantee, particularly for structured values or when a secret is transformed before printing.

Reusable workflows as a governance mechanism

Section titled “Reusable workflows as a governance mechanism”

The most effective Actions control is not a policy setting — it is that teams do not write their own pipelines.

A reusable workflow is called rather than copied. A repository’s workflow file becomes a few lines invoking a centrally-maintained workflow with parameters.

Which changes what governance means. Instead of enforcing rules across four hundred hand-written workflows, you maintain one and every caller inherits the change. Pinning actions by SHA, setting minimal permissions, adding a security scan — each is a single edit.

And it makes the fleet legible. “Which repositories build container images” is answerable by looking at who calls the container build workflow, rather than by parsing four hundred files.

Required workflows go further. GitHub’s rulesets can require that specified workflows pass before merging, at organisation and enterprise level. That is enforcement rather than convention — a repository cannot opt out of the security scan by not calling it.

The adoption problem is real. A reusable workflow that does not fit a team’s needs will be bypassed, and a workflow that tries to fit every need becomes an unmaintainable configuration language. Aim for the common cases, and make the escape hatch explicit rather than accidental.

Version them. Callers reference a version, and a breaking change to the shared workflow should not break four hundred pipelines simultaneously. This is the same discipline as any shared library. See standardised CI/CD.

Own them properly. A shared workflow with no maintainer becomes the thing everybody depends on and nobody can change, which is worse than no standardisation at all.

Self-hosted runners and the network boundary

Section titled “Self-hosted runners and the network boundary”

Where a workflow runs determines what it can reach, and self-hosted runners are where CI governance meets network security.

A self-hosted runner is a machine on your network. Code running on it can reach whatever that machine can reach — internal services, databases, cloud metadata endpoints.

Which means the workflow’s code is effectively running inside your perimeter. Every third-party action, every dependency, every script.

Never use self-hosted runners on public repositories. Anybody can open a pull request, and if fork pull requests can trigger jobs on your runner, anybody can execute code on your network. This is a well-understood and severe risk.

Ephemeral runners are strongly preferable. A runner that is destroyed after each job carries no state between jobs — no cached credentials, no leftover files, no compromise persisting into the next team’s build.

Runner groups control which repositories may use which runners, and are the mechanism for keeping a runner with production network access away from repositories that should not have it. See runner groups.

Treat runner registration as a privileged action. Somebody able to register a runner into a group can receive jobs intended for that group, including their secrets.

Inventory them. Self-hosted runners accumulate, outlive their purpose, and run unpatched. A runner registered three years ago for a project that ended is still listening for jobs.

You cannot govern what you have not enumerated, and Actions is reliably the least enumerated part of an estate.

Which repositories have workflows, and how many.

Which actions are used, and at what versions. A code search across the fleet for uses: produces this, and the result is usually longer and stranger than expected.

Which are pinned by SHA versus by tag.

Which workflows have elevated permissions, and which are triggered by fork pull requests.

Which self-hosted runners exist, in which groups, reachable by which repositories.

Which secrets exist, where, and when they were last updated.

This inventory is the work. Every control decision afterwards is informed by it, and producing it usually surfaces several things worth fixing immediately.

The blunt controls, which matter when a policy question arises before a nuanced answer is ready.

Actions can be disabled entirely at enterprise or organisation level, or enabled only for selected repositories. Rarely the right long-term answer, and occasionally the right short-term one during an incident.

Actions can be limited to actions defined in the same repository or organisation, which eliminates the third-party supply chain question at the cost of eliminating the ecosystem.

Workflows can be disabled per repository, which is the tool for an individual runaway or compromised pipeline.

Scheduled workflows can be disabled independently, which addresses the largest source of unattended execution.

Knowing these exist matters for incident response. When a third-party action is disclosed as compromised, the immediate question is how quickly you can stop it running, and the answer is either “we can disable that action” or “we cannot”.

An allow list makes that answer good. Removing an action from the allow list stops it running everywhere, immediately. Without one, the response is a fleet-wide pull request campaign.

Which is a further argument for the allow list beyond prevention: it is the mechanism for a rapid response, and that is a capability worth having before it is needed.

  1. Build the inventory. Nothing else can be prioritised without it.

  2. Set default workflow permissions to read-only. Highest value, one setting, and workflows needing more will fail visibly rather than silently — which is the right failure mode. Announce it, and expect a wave of small pull requests adding explicit permissions.

  3. Disable workflows approving pull requests. Closes a hole in the review control.

  4. Find and fix privileged fork-triggered workflows. The highest-severity finding the inventory produces.

  5. Introduce an action allow list, starting permissively — verified creators plus the actions already in use — and tightening over time.

  6. Move sensitive secrets to environments with required reviewers.

  7. Migrate cloud credentials to OIDC.

  8. Then look at runner groups and the network boundary.

Step 2 before step 5. Restricting which actions may run is the more visible control; restricting what they can do when they run is the more effective one.

The workflow file is code that governs itself

Section titled “The workflow file is code that governs itself”

A structural problem worth understanding, because it undermines several controls that look solid.

A workflow file lives in the repository. Changing it is a code change, subject to whatever review the repository requires.

Which means somebody who can merge to the default branch can change what CI does — including removing a security scan, adding a step that exfiltrates secrets, or granting the job additional permissions.

Branch protection is therefore the control, not the workflow itself. A required security scan in a workflow file that anybody can edit is a convention.

Three mitigations, in increasing strength:

Require review on workflow files specifically, via CODEOWNERS on the .github/workflows/ path. A change to CI requires approval from a nominated group.

Use required workflows from rulesets, which are defined centrally and cannot be removed by editing the repository’s own files.

Restrict the workflow token’s actions permission, so a compromised workflow cannot rewrite other workflows.

Watch for workflow file changes in the audit log. A modification to a workflow file, particularly on a repository the actor does not normally touch, is one of the highest-value signals available — it is what a compromised credential does to establish persistence.

And review the escape hatches. A workflow that runs a script from the repository, or that evaluates a variable as a command, has moved the trust boundary from the reviewed workflow file to unreviewed code.

A subtler class of problem than fork pull requests, and one that catches experienced teams.

Workflow expressions are interpolated before the shell sees them. A step that includes a pull request title, a branch name, or an issue body directly in a script is constructing a command from attacker-controlled text.

Which means somebody who can open a pull request can potentially execute commands in a workflow that never intended to run their code — even without a fork checkout.

The mitigation is to pass untrusted values through the environment, not through string interpolation into a script. An environment variable is data; an interpolated expression becomes part of the command.

The values to treat as untrusted include anything a person outside your trust boundary can set: pull request titles and bodies, branch and tag names, issue content, commit messages, and review comments.

This class of issue is easy to introduce and hard to spot in review, because the workflow looks reasonable. A linting step in your CI review process, or a shared workflow that handles these values correctly, is worth more than training.

Audit for it once. A search across the estate for expression interpolation inside run: blocks finds the candidates, and most estates have several.

Default workflow permissions left permissive. A broad standing grant on every repository.

Actions referenced by tag. Mutable references to code that runs with your credentials.

Privileged workflows on fork pull requests. The most severe common CI vulnerability.

Production credentials as repository secrets. Available to every workflow in the repository.

Workflows able to approve pull requests. Defeats required review.

No inventory. Every control decision is a guess.

An allow list introduced too strictly. Breaks everything at once and gets reverted.

Long-lived cloud credentials where OIDC works. An avoidable category of stored secret.

Treating Actions governance as separate from security. It is the largest execution surface in the estate.

Actions governance is usually framed as security. Cost is the other half, and it is frequently what gets the work funded.

Minutes are metered. Hosted runner usage is billed, and an enterprise with thousands of repositories running CI on every push accumulates substantial usage.

The largest consumers are usually not the largest teams. A single workflow running on a schedule every five minutes, or a matrix build with more combinations than anybody needs, can dominate.

Checkout is a large fraction of it, and it is almost entirely optimisable. See CI clone optimisation.

Jobs that should not have run are the purest waste — a monorepo running every service’s build for a documentation change.

Failed workflows retried repeatedly by a bot with no backoff.

Cost centers let you attribute usage within the enterprise, which turns an aggregate bill into a conversation with the team generating it.

And attribution is what produces change. A central team asking everybody to be more efficient achieves little; a team seeing their own number and its trend behaves differently.

Report the top consumers monthly, with the trend. That single report, sent to the teams concerned rather than to a central inbox, does more for CI cost than any policy.

Once the controls are in place, a small set of signals tells you whether they are holding.

Workflow file modifications, particularly by an actor who does not normally touch that repository. The persistence technique of choice for a compromised credential.

New self-hosted runner registrations. Low volume, and a runner registered into a privileged group is a serious event.

Actions policy changes. Somebody widening the allow list or changing default permissions.

Secrets created or updated, particularly at organisation level.

Workflow runs on unusual triggers, such as a workflow_dispatch on a production deployment pipeline outside normal hours.

Failed runs on the security-scanning workflow, which is what a bypass attempt looks like.

Deployment approvals, which should be reviewed as a matter of course rather than alerted on.

Keep it proportionate. Six detections that somebody reads beat twenty that nobody does, and the Actions event volume means an untuned rule produces noise immediately.

Route them to the platform team, not to a general security queue. The people who can tell whether a workflow change is normal are the people who maintain the workflows.

A workflow is arbitrary code, running with credentials, inside your boundary. Governance means controlling what code may run, what it may do while running, and what it may reach — in that order of tractability but the reverse order of impact. The default permission setting is worth more than any allow list.

  • Enterprise policy can restrict actions to GitHub-created, verified creators, or an allow list
  • Pinning actions to a commit SHA is what makes a third-party action a reviewed dependency rather than a trusted one
  • Default workflow token permissions are configurable and setting them read-only is the highest-value single change
  • Job-level permissions are narrower than workflow-level and should be preferred
  • Fork pull requests run without secrets by design, which is what makes accepting contributions safe
  • Running fork code with secrets or write permissions is a severe and common vulnerability
  • Environment secrets with required reviewers gate sensitive credentials behind a human
  • OIDC removes long-lived cloud credentials from the estate entirely
  • The inventory of workflows, actions, permissions and secrets is the prerequisite for every decision

Use an organisation you administer.

  1. Check the default workflow permissions setting. Predict: read-only or write?

  2. Search the estate for uses: and count distinct actions. Predict: more or fewer than fifty?

  3. Count how many are pinned by SHA versus by tag.

  4. Find workflows triggered by pull_request_target or equivalent. Check each for privilege plus untrusted checkout.

  5. List organisation and repository secrets by name and last-updated date. Find the oldest.

  6. Find one workflow using a long-lived cloud credential. Design the OIDC replacement.

  7. Check whether workflows can approve pull requests.

  8. Write the announcement for setting default permissions to read-only, including what teams must do.

Engineering Team Onboarding SystemA 30-day Git and GitHub programme with standards templates, assessments and governance checklists.