SaveMyCert
Log in
5 of 5 free questions left today·for 30 a day
Manage GitHub Actions for the enterprise

Govern GitHub Actions in the Enterprise: Policies, Templates, Secrets and Variables (GH-200)

17 min readGH-200 · Manage GitHub Actions for the enterpriseUpdated

GitHub Actions governance is the set of enterprise, organization and repository policies that decide where workflows may run, which actions and reusable workflows they may use, what their GITHUB_TOKEN may do and how pull requests from forks are treated — together with the secrets and variables those workflows read at each scope. This lesson covers the GitHub Actions (GH-200) skills for distributing and governing actions and workflows (templates, private and internal sharing, required workflows through rulesets, delegated administration) and for managing encrypted secrets and configuration variables at organization, repository and environment level, in workflows and through the REST API and GitHub CLI.

What you’ll learn
  • Explain how enterprise, organization and repository Actions policies nest, and choose the enablement and allowed-actions settings that meet a requirement
  • Write allow and block patterns for actions and reusable workflows, and predict what the SHA-pinning policy and local-action exemption allow
  • Configure default GITHUB_TOKEN permissions, pull request creation and fork pull request policies for public and private repositories
  • Choose between workflow templates, Access sharing and ruleset-required workflows to distribute or enforce automation, and delegate Actions administration with custom roles
  • Scope secrets and variables at organization, repository and environment level, and predict which value a job receives
  • Apply secret naming rules and limits, including the large-secret workaround and the 100-secret window
  • Create and update secrets and variables with the REST API (libsodium encryption, repository-access endpoints) and the GitHub CLI

How GitHub Actions governance is layered

GitHub Actions policies are set at three levels — enterprise, organization and repository — and each level can only narrow what the level above allows, never loosen it. An enterprise owner who disables Actions for an organization, restricts the allowed actions or sets a restricted token default has made that choice for everything beneath; the organization and repository settings for it become unavailable or limited to stricter values.

The first policy at every level is enablement: whether workflows may run at all.

LevelEnablement choicesEffect
EnterpriseEnable for all organizations · enable for specific organizations · disableOrganizations left off cannot turn Actions on themselves
OrganizationEnable for all repositories · enable for selected repositories · disableRepositories left off cannot run any workflow, even a plain run: step in a new file under .github/workflows
RepositoryEnable or disable, only within what the organization permitsA repository cannot override a parent that disabled it

Enablement answers where workflows run. It is a different policy from the allowed-actions list (which actions a run may use) and from token permissions (what a run may change). Only enablement stops a workflow from executing. Because the levels nest, turning Actions on somewhere a parent has switched it off is always a change at the parent first: a repository cannot enable what its organization disabled, and an organization cannot enable what its enterprise disabled.

Allowed actions and reusable workflows policy

The allowed-actions policy decides which actions and reusable workflows a workflow may reference. At organization level (the enterprise has the same options with "enterprise" in place of the organization name) the choices are:

OptionWhat runsWatch out for
Allow all actions and reusable workflowsAnything from any repositoryHas no allow/block list at all
Allow owner actions and reusable workflowsOnly actions stored in the organization's (or enterprise's) own repositoriesAlso blocks GitHub's own actions such as actions/checkout
Allow owner, and select non-owner, actions and reusable workflowsThe owner's actions plus the sources you tick or listThe only option with sub-choices and a specified list

The select option has three independent controls:

  • Allow actions created by GitHub — admits actions in the actions and github organizations (for example actions/setup-node).
  • Allow Marketplace actions by verified creators — admits actions from publishers GitHub has verified, such as Docker or HashiCorp. This is a broad switch, not a per-action choice.
  • Allow or block specified actions and reusable workflows — a comma-separated list of patterns (next section).

Two exemptions matter everywhere: local actions referenced with a ./ path inside the same repository are never restricted by these policies, and the policy governs sources, not reference formats. Because a lower level can never loosen a higher one, a source the enterprise policy excludes can only be admitted by changing the enterprise policy itself.

Allow and block patterns, and the SHA-pinning policy

The specified list accepts one pattern per action or reusable workflow, separated by commas, and it exists only under the "select non-owner" option.

PatternMatches
OWNER/REPO@refAn action at one tag, branch or SHA (e.g. octo-org/lint@v2)
OWNER/REPO/PATH/FILENAME@refA reusable workflow file, with its full path (e.g. tools-org/shared-ci/.github/workflows/release.yml@*)
OWNER/* or @** is a wildcard — every repository of an owner, or any reference
!OWNER/REPO@*A leading ! turns the entry into a block

Combining a wildcard with a block entry lets you keep almost everything open while refusing one known-bad action, without listing the hundreds of actions teams already use:

*, !example-org/format-action@*

The * entry keeps every other source allowed and the ! entry refuses every reference of the one action. Since the open "Allow all" option has no list, blocking anything starts by moving to the select option.

Requiring actions pinned to a full-length commit SHA

The SHA-pinning policy makes GitHub refuse any workflow that references an action by a tag or branch instead of a full 40-character commit SHA. Tags and branches can be moved to new code by whoever controls the action's repository; a commit SHA cannot, so the policy closes the "moved tag" supply-chain attack. It is available at enterprise, organization and repository level.

Its scope is narrower than it sounds:

  • It applies to actions referenced with uses: in steps.
  • Reusable workflows called with uses: at job level may still be referenced by tag.
  • Local actions (./path) are not restricted at all.

Applied to this workflow: the job-level reusable workflow keeps its tag, actions/setup-node@v4 must be rewritten to a 40-character SHA, and the local ./ action is untouched.

jobs:
  build:
    uses: fabrikam/ci-library/.github/workflows/package.yml@v3
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/setup-node@v4
      - uses: ./.github/actions/format

The policy controls the reference format, not the source: it does not replace the allowed-actions list. Why pinning matters and how to keep pins up to date is covered in the security best-practices lesson of Domain 5.

Default GITHUB_TOKEN permissions and pull request creation

The workflow-permissions setting chooses the default scopes of GITHUB_TOKEN for workflows that do not declare a permissions key.

SettingDefault token access
Read repository contents and packages permissions (restricted)Read contents and packages (plus metadata); nothing else
Read and write permissions (permissive)Read and write across most scopes

Three rules decide most scenarios:

  • The default only applies when there is no permissions key. After a switch to restricted, a workflow without a key can still check out code and pull packages, but a step that pushes a tag fails.
  • A declared permissions key replaces the default completely. Every scope you list gets the access you give it and every scope you omit becomes none. contents: write lets a release job push tags whatever the default; pull-requests: write alone leaves contents at none, so a tag push fails.
  • A restricted enterprise default cannot be loosened. Organizations and repositories cannot choose read and write as their default once the enterprise restricts it. Jobs that need write access declare it in the workflow — which is least-privilege practice anyway.

A separate checkbox, "Allow GitHub Actions to create and approve pull requests", is off by default. While it is off, GITHUB_TOKEN cannot open or approve a pull request even with pull-requests: write: the branch push succeeds and the pull-request API call is refused with a 403.

Fork pull request policies

Fork policies decide whether and how workflows run for pull requests opened from forks, and the options differ for public and private repositories.

Public repositories choose how much approval outside contributors need before workflows run on their pull requests — for example only for first-time contributors who are new to GitHub, for all first-time contributors, or for all external contributors. One trigger is outside this gate: workflows triggered by pull_request_target always run, because they execute from the base branch in the base repository's trusted context. That is exactly why a pull_request_target workflow must never check out and execute the fork's code while secrets or a write token are available.

Private and internal repositories have four separate checkboxes:

SettingEffect when enabled
Run workflows from fork pull requestsFork pull requests trigger workflows, with a read-only GITHUB_TOKEN and no secrets
Send write tokens to workflows from fork pull requestsThe fork run gets a token with write permissions
Send secrets and variables to workflows from fork pull requestsThe fork run receives repository secrets and variables
Require approval for fork pull request workflowsEach fork run waits until someone with write access approves it

The checkboxes are independent. The two "Send" options are the ones that hand code from a fork a write token or the repository's secrets, so they belong only where every forking contributor is trusted; approval adds a human check before any of it runs.

Distributing reusable components: templates, sharing and required workflows

An organization distributes standard automation in three ways, and they solve different problems: templates suggest a starting file, Access sharing lets other repositories call private code, and rulesets enforce a workflow without touching the repositories.

MechanismWhat it doesFiles needed in each repository?Enforced?
Workflow templateOffered on the repository's "New workflow" page and copied inYes — the copyNo
Reusable workflow / shared actionCalled at run time with uses:Yes — a callerNo
Ruleset "Require workflows to pass before merging"Runs a central workflow on pull requests in targeted repositories and blocks merging on failureNoYes

Workflow templates

An organization publishes templates in a repository named .github, inside a workflow-templates directory (not .github/workflows). Each template is a workflow file plus a metadata file with the same base name ending in .properties.json, whose name and description are required (an icon and categories are optional). The placeholder $default-branch is replaced with the target repository's default branch when the workflow is created — the new file contains the literal branch name, such as develop; it is not re-evaluated at run time.

Visibility decides where templates are offered: templates in a public .github repository appear in all repository types; an internal one in internal and private repositories; a private one only in private repositories. With a private or internal .github repository users also need Read access to it. Templates belong to the owning organization — other organizations in the same enterprise do not see them. Authoring templates and reusable workflows in detail is the subject of the Domain 2 lesson on templates and reusable workflows.

Sharing actions and reusable workflows from private and internal repositories

Actions and reusable workflows in a private or internal repository are usable only inside that repository until its Settings > Actions > General > Access option grants more. A private repository can be opened to repositories in the same organization (or, for a user-owned repository, the same user); an internal repository can be opened to repositories in the enterprise. Repositories owned by an enterprise's organizations get a wider choice too: a private repository can also be opened to the whole enterprise (callers must themselves be private), and an internal one can be limited to its own organization (callers must be private or internal). Internal visibility exists only for enterprise accounts, so a GitHub Team organization shares a private repository at organization level. Neither the allowed-actions list nor people's repository roles grant this access, and making the repository public breaks any privacy requirement. The allowed-actions policy still applies on top: sharing makes an action reachable, the policy decides whether it may run.

Required workflows through rulesets

The "Require workflows to pass before merging" rule is configured once in an organization (or enterprise) ruleset. It names a source repository, a workflow file and a ref, and targets repositories by name or property. Its rules:

  • It runs on pull_request, pull_request_target and merge_group with their default activity types, and ignores filters such as branches, paths and types — so the check runs on every targeted pull request. Work that should be skipped must be decided inside the jobs.
  • The source repository's visibility must suit the targets: a public source runs anywhere, an internal one in internal and private repositories, a private one only in private repositories.
  • An internal or private source must also allow access from outside its repository in its Actions Access setting, or the workflow never starts.
  • Most CodeQL actions (such as init and analyze) are blocked in ruleset workflows, even inside a called reusable workflow; only upload-sarif is allowed. For organization-wide CodeQL, GitHub recommends default setup plus the "Require code scanning results" rule.

Delegating Actions administration with custom organization roles

Custom organization roles let an owner delegate parts of Actions administration without making someone an organization owner. GitHub provides separate fine-grained permissions for Actions:

PermissionCovers
Manage organization Actions policiesEnablement, allowed actions and patterns, workflow permissions, fork settings
Manage organization Actions secretsCreate, update and delete organization secrets and their repository access
Manage organization Actions variablesThe same for configuration variables
Manage organization runners and runner groupsSelf-hosted runners and runner groups at organization level

Build the role from only the permissions the person needs: someone who maintains organization secrets and variables but must not change policies gets those two permissions and nothing else. The Admin role on repositories is the wrong tool — it does not reach organization-level settings and grants far more than needed. Runner and runner-group management itself belongs to the sibling topic on managing runners at scale.

Secrets and variables: choosing, scoping and precedence

Secrets are encrypted values that GitHub redacts from logs; configuration variables are plain-text settings that are not redacted. Anything that grants access — a token, a password, a connection string, a webhook URL with an embedded key — is a secret; a region name, a feature flag or a host name is a variable. Workflows read them through different contexts:

steps:
  - run: ./deploy.sh --region "${{ vars.REGION }}"
    env:
      API_KEY: ${{ secrets.API_KEY }}

An undefined variable evaluates to an empty string rather than failing the run, so a condition such as if: ${{ vars.RUN_LOAD_TESTS == 'yes' }} is false when no such variable exists — handy for optional feature flags. An unset or unreachable secret is also an empty string. Secrets cannot be referenced directly in if: conditions (map them to an environment variable first; see the Domain 1 workflow-structure lesson).

Actions never read secrets on their own: the calling workflow passes a value in through a with: input or an env: variable, and a composite action cannot use the secrets context directly. Reusable workflows receive secrets through workflow_call secrets or secrets: inherit (covered in the Domain 2 reusable-workflows lesson).

Scopes

ScopeWho receives it
OrganizationRepositories chosen by the access policy: all repositories, private repositories (private and internal), or selected repositories
RepositoryEvery workflow and job in that repository, on any branch
EnvironmentOnly jobs that name the environment with environment:, and only after its protection rules pass

When the same name exists at several levels, the lowest level wins: environment, then repository, then organization — evaluated name by name. For example, if the organization and the repository both define LOG_LEVEL and a qa environment defines only CDN_HOST, a job targeting qa reads LOG_LEVEL from the repository and CDN_HOST from the environment.

On the GitHub Free plan, organization secrets and variables are not accessible to private repositories at all, whatever access policy is chosen; public repositories receive them, private ones get an empty value.

Environment secrets as a gate

Environment secrets are the only scope that protects the value itself. A job receives them only if it has environment: production (a job that merely needs: a job that used the environment gets nothing), and only after the environment's deployment branch policy and required reviewers pass. A repository or organization secret is available to any branch, and an if: github.ref == ... guard can be edited away by the very developer it is meant to stop. Widening who may deploy is therefore done on the environment itself — by changing its deployment branch policy — not by copying the secret to a broader scope.

Secret naming rules and limits

Secret and variable names follow fixed rules, and secrets have size and count limits that explain several "empty value" puzzles.

  • Names may contain only alphanumeric characters and underscores — no spaces.
  • Names must not start with the GITHUB_ prefix and must not start with a number.
  • Names are case-insensitive (stored in uppercase) and must be unique at their level.
LimitValue
Size of one secret48 KB
Organization secretsUp to 1,000
Repository secrets / environment secretsUp to 100 each
Organization secrets a repository's workflows can useThe first 100 shared with it, sorted alphabetically by name

If more than 100 organization secrets are shared with a repository, names late in the alphabet resolve to an empty string. Share fewer secrets with that repository or use repository secrets.

For a value larger than 48 KB — a key file, for example — GitHub documents one workaround: encrypt the file with gpg --symmetric --cipher-algo AES256, commit only the encrypted file, store the passphrase as a secret, and decrypt in a step at run time. GitHub does not redact the decrypted contents, so the workflow must never print them. Moving the value to an environment or organization secret does not help, and a variable would store it in plain text.

Managing secrets and variables with the REST API and GitHub CLI

Secrets are written to the REST API already encrypted: a client fetches the public key of the scope, encrypts the value with libsodium, and sends the ciphertext. Variables are plain text and use ordinary create/update/read calls.

Secrets

  1. GET /repos/{owner}/{repo}/actions/secrets/public-key (or the /orgs/{org}/... or environment equivalent) returns a key and a key_id. Use the key of the scope you are writing to.
  2. Encrypt the value with libsodium (a sealed box) using that key and Base64-encode the result.
  3. PUT /repos/{owner}/{repo}/actions/secrets/{secret_name} with encrypted_value and key_id — 201 when created, 204 when updated. There is no plain-text or POST form, and secret values can never be read back.

Organization secrets add an access policy: visibility is all, private or selected, with selected_repository_ids for selected. The repository list then has its own endpoints:

CallEffect
PUT /orgs/{org}/actions/secrets/{name}/repositoriesReplaces the whole list with selected_repository_ids
PUT /orgs/{org}/actions/secrets/{name}/repositories/{repository_id}Adds one repository (204), no value needed
DELETE …/repositories/{repository_id}Removes one repository

The single-repository calls return 409 unless the secret's visibility is selected.

Variables

ActionRequest
CreatePOST /repos/{owner}/{repo}/actions/variables with name and value
UpdatePATCH /repos/{owner}/{repo}/actions/variables/{name} (204)
ReadGET …/actions/variables/{name} returns the plain value
DeleteDELETE …/actions/variables/{name}

An organization variable is created with one POST /orgs/{org}/actions/variables carrying name, value and the required visibility; selected_repository_ids is accepted only with selected. Its repository list uses the same replace-or-add-one endpoints as secrets.

GitHub CLI

gh secret set SENTRY_DSN --repo fabrikam/shop           # repository secret
gh secret set SENTRY_DSN --env qa --repo fabrikam/shop  # environment secret
gh secret set SENTRY_DSN --org fabrikam \
  --visibility selected --repos shop,mobile             # org, selected repos
gh variable set LOG_LEVEL --body info --org fabrikam --visibility all

Without --visibility, the CLI gives an organization secret private visibility (private and internal repositories). Managing organization secrets needs the admin:org scope on the CLI's token.

Tip. Expect scenario items that describe an enterprise or organization requirement — who may run workflows, which publishers are acceptable, a compromised action, a moved tag — and ask which enablement, allowed-actions, pattern or pinning setting meets it, often with YAML showing which references comply. Token and fork items describe a failing release job, a 403 when opening a pull request, or contractors working from forks, and ask which setting or permissions key resolves it. Distribution items contrast workflow templates, Access sharing for private and internal repositories and ruleset-required workflows, including visibility rules, ignored filters and why a required workflow never starts. Secret and variable items show values at several scopes and ask which one a job receives, why a value is empty (plan, access policy, the 100-secret window, a missing environment key, naming rules) or where a credential should live, and REST or GitHub CLI items ask for the request or command that creates, updates or shares a value under constraints such as one rotation point or not resending the value.

Key takeaways
  • Each policy level can only narrow the level above; organizations cannot loosen an enterprise policy, and repositories cannot override their organization.
  • Enablement decides where workflows run at all; the allowed-actions policy decides which actions they use; token permissions decide what they may change.
  • Owner-only allowed actions also block GitHub's own actions; the select option with "Allow actions created by GitHub" admits the actions and github organizations.
  • Allow/block lists exist only under the select option: OWNER/REPO@ref for actions, OWNER/REPO/PATH/FILE@ref for reusable workflows, * as a wildcard, ! to block. Local ./ actions are never restricted.
  • SHA pinning applies to actions; reusable workflows may keep tags. A declared permissions key sets every unlisted scope to none.
  • Ruleset-required workflows run on pull_request, pull_request_target and merge_group, ignore branch and path filters, and need a source repository whose visibility and Access setting suit the targets.
  • The lowest scope wins per name (environment > repository > organization); environment secrets reach only jobs that name the environment after its protection rules pass.
  • Secrets are written via REST as libsodium-encrypted values with the scope's key_id; variables use POST to create and PATCH to update, and an unset variable is an empty string.
  • Actions receive secrets only through inputs or env passed by the calling workflow; composite actions cannot read the secrets context.

Frequently asked questions

Can an organization owner allow an action that the enterprise policy blocks?

No. Enterprise GitHub Actions policies set the outer limit, and an organization or repository can only choose the same or a stricter setting. If an enterprise allows only enterprise actions, the enterprise owner must switch to the policy that allows selected non-enterprise actions and admit the source there, for example by ticking "Allow actions created by GitHub".

Why does actions/checkout fail when only organization actions are allowed?

The owner-only allowed-actions option admits only actions stored in the organization's (or enterprise's) own repositories, and actions/checkout lives in GitHub's actions organization. Choosing the option that allows selected non-owner actions and ticking "Allow actions created by GitHub" admits the actions and github organizations while still blocking other publishers.

How do I block one compromised action without listing every action my teams use?

Switch to the allowed-actions option that allows selected non-owner actions and reusable workflows, then enter a specified list such as "*, !example-org/format-action@*". The * wildcard keeps every other action allowed and the ! prefix blocks every reference of the one action. The "Allow all actions" option has no list, so it cannot block anything.

Does the SHA-pinning policy apply to reusable workflows and local actions?

No. The GitHub Actions policy that requires actions to be pinned to a full-length commit SHA applies to actions referenced in steps. Reusable workflows called at job level may still use tags, and local actions referenced with a ./ path are never restricted by Actions policies.

Which value wins when a secret or variable exists at organization, repository and environment level?

The lowest level wins for each name: an environment value overrides a repository value, which overrides an organization value. Environment values apply only to jobs that reference the environment with the environment key, so a job without it falls back to the repository or organization value.

Why is an organization secret empty in some repositories?

Common causes are: the repository is not covered by the secret's access policy (all, private or selected repositories); the organization is on GitHub Free, where organization secrets and variables are not available to private repositories; or more than 100 organization secrets are shared with the repository, in which case workflows can use only the first 100 sorted alphabetically by name.

How do you create a GitHub Actions secret with the REST API?

Fetch the public key of the target scope (for a repository, GET /repos/{owner}/{repo}/actions/secrets/public-key), encrypt the value with libsodium using that key, then send PUT to the secret's endpoint with encrypted_value and the returned key_id. GitHub answers 201 when the secret is created and 204 when it is updated; plain-text values are not accepted.

How can a security team require a workflow in every repository without adding files to them?

Use an organization ruleset with the "Require workflows to pass before merging" rule. It names a workflow file in a central source repository and runs it on pull requests in the targeted repositories, blocking merges when it fails. Workflow templates and reusable workflows both need a file in each repository and enforce nothing.

Source

This lesson covers the "Manage GitHub Actions for the enterprise" domain of the official GH-200 exam guide. Vendors revise their guides — check the source for the current version.

Sign up free to mark lessons complete, bookmark topics and track your exam readiness.

Spotted a mistake in this lesson?