GitHub Actions Hardening
The enterprise-wide runbook context: the shared language for hardening GitHub Actions across every organization in a GitHub Enterprise Cloud account.
Language
Section titled “Language”Required workflow:
A workflow hosted in the central security-workflows repo that the enterprise
ruleset forces to run on targeted repos, whether or not those repos authored it.
This is the single merge-gating mechanism for both scanners.
Avoid: required check (for the scanners), required action
Required status check: A named check that branch protection waits on, reported by a workflow that already exists in the target repo. Not used to gate the scanners — kept out of scope to avoid double-gating and check-name drift. Avoid: status gate, required check (as a synonym for required workflow)
PR security scanning: The single layer that runs static analysis on every pull request via centrally hosted workflows: zizmor (Actions security posture), which is required, and dcg (destructive shell commands), which is advisory. Replaces the earlier split of “Layer 2 / Layer 2b.” Avoid: audit layer, zizmor layer
Scanning workflow: A centrally hosted workflow that performs one kind of PR-time static analysis and uploads SARIF. The two are the zizmor scan and the dcg scan; only the zizmor scan is a required workflow. Avoid: audit workflow (for zizmor), audit (as a verb for either scanner)
Evaluate mode: The ruleset enforcement state that reports would-be violations via Insights without blocking merges. Flips to Active mode after the rollout window. Avoid: audit mode, dry-run mode
Active mode: The ruleset enforcement state that blocks merges on violations. Avoid: enforced mode, live mode
Pinning dry-run:
The stand-in for the Actions policy’s missing Evaluate mode: zizmor’s
unpinned-uses findings, counted enterprise-wide, tell you how much breakage
Layer 1 would cause before you enable its hard cutover. This is why Layer 1
lands late in the rollout.
Avoid: audit proxy, pinning pre-audit
Unautomatable control: An enterprise control GitHub exposes only through the web UI, with no stable API — the Layer 1 SHA-pin toggle, environment protection rules, and the outside-collaborator approval default. Tooling may report such a control as unset or unknown, and may print the values to enter, but can never apply them. Avoid: manual step, UI-only setting
Placeholders
Section titled “Placeholders”Use these verbatim everywhere, including inside code and YAML samples.
<enterprise>:
The enterprise slug.
Avoid: <enterprise-slug>, acme-inc
<platform-org>:
The one org designated to host shared workflows and tooling.
Avoid: <company>-platform, PLATFORM_ORG, acme-platform
<org>:
Any consuming organization in the enterprise.
Avoid: <your-org>, <company>, acme-product
security-workflows:
The fixed literal name of the shared workflows repo, always as
<platform-org>/security-workflows. The “it’s only a convention” caveat is
stated once in Appendix D and nowhere else.
Avoid: ci-workflows, platform-tooling, “the central .github repo”
<org>/.github:
The org-defaults repo (community health files, org profile, governance docs).
Always qualified as “org-defaults repo”; never called “the central .github
repo”.
Avoid: “the .github repo” (unqualified)
Pinning
Section titled “Pinning”SHA-pin:
Pinning an action reference to a full-length commit SHA. The canonical prose
term; the zizmor config expresses this as the literal hash-pin.
Avoid: hash-pin (in prose), “full-length commit SHA pin”, “SHA-pinning” as a
noun for the concept
Tag-pin:
Pinning an action reference to a mutable ref (tag or branch). The canonical
prose term; the zizmor config expresses this as the literal ref-pin.
Avoid: ref-pin (in prose), “tag pinning”, “reference by tag”
Pin-policy parity:
The invariant that the namespaces zizmor allows to ref-pin must exactly equal
the namespaces the Layer 1 allowlist exempts from SHA-pinning (GitHub-created,
verified Marketplace, and <org>/*). If the two sets drift, zizmor and the
Actions policy give contradictory signals. Cross-referenced from both Layer 1
and Layer 2.
Avoid: allowlist overlap, pin exemption match
Dependabot enforcement
Section titled “Dependabot enforcement”Presence gate:
An enterprise push ruleset that restricts pushes touching
.github/dependabot.yml. GitHub push rulesets have no deletion-only rule for a
path, so the available file_path_restriction rule blocks every push to that
path — deletion, gutting, and legitimate edits alike. Guarantees the file
survives; does NOT guarantee its contents are correct, and requires a
bypass_actors entry for the content reconcile.
Avoid: “blocks deletion” (the rule is broader), “prevents drift”
(overstated), drift protection
Content reconcile:
A scheduled harden.sh reconcile run that re-asserts the canonical
dependabot.yml and opens a PR wherever a repo’s copy has drifted. The only
mechanism that guarantees config content, as opposed to mere survival.
Avoid: auto-PR option, sync job
Bootstrap:
The one-time script run that seeds dependabot.yml into every repo that lacks
it. Distinct from the ongoing presence gate and content reconcile.
Avoid: rollout script
Baseline policy
Section titled “Baseline policy”Baseline enterprise policy (Layer 0): The set of low-effort enterprise defaults applied before the four layers: read-only default token, no Actions-created PRs, OIDC over stored secrets, and environment protection. Independent of the additive layers. Avoid: Layer 1 (as a synonym), pre-reqs
Least-privilege token default:
Setting the default GITHUB_TOKEN permission to read-only enterprise-wide, so
workflows must opt up via an explicit permissions: block.
Avoid: read-only token (unqualified), token hardening
OIDC:
Short-lived cloud authentication via workload identity federation, scoped to a
repo:<org>/<repo> subject, replacing stored long-lived cloud secrets.
Avoid: keyless auth, federated login
Exceptions
Section titled “Exceptions”Fast-track exception process: The auditable, time-boxed path for granting a narrow exception when a gate blocks legitimate work: a single SHA allowlist entry, one dcg project allowlist rule, or a time-boxed ruleset bypass, requested in the platform repo with a mandatory expiry. Never a standing or blanket disable. Avoid: bypass, waiver, override request
📦 Source: soderlind/gh-hardening · Edit on GitHub