Skip to content

GitHub Actions Hardening

A practical, layered strategy for hardening GitHub Actions across every repository in an organization. Each layer is additive: together they enforce SHA-pinning, run security audits on every pull request, scan committed shell for destructive commands, gate merges, and keep pinned SHAs fresh.

LayerWhat it doesWhere it lives
1 — SHA pinningRuntime block on unpinned actionsOrganization → Policies → Actions
2 — Static analysiszizmor audit on every PRCentral security-workflows repo
2b — Command scanningScans committed shell for destructive operationsCentral reusable workflow + ruleset
3 — Rulesets & merge gatesRequires the audit workflows + gates mergesOrganization → Policies → Repository
4 — DependabotKeeps pinned SHAs up to datePer-repo .github/dependabot.yml

A staged rollout ties them together — critically, Layer 1 has no audit mode, so it must come after static analysis has surfaced and fixed the bulk of unpinned references.

  • Admin role — you need owner-level access to set organization or enterprise policies and rulesets.
  • A platform org — repositories live inside organizations, not directly under an enterprise. Designate one organization (for example platform-org) to host shared workflows and tooling. See Key concepts below.
  • A central repo — in the platform org, create one internal repository to host shared workflows. A common convention is security-workflows with internal visibility, so every internal/private repo in the organization can consume it.
  • A rollout window — plan to run rulesets in Evaluate (audit) mode for roughly two weeks before switching them to Active.

There is no such thing as an “enterprise repository.” Repositories only exist inside organizations (or under user accounts). An enterprise is a container for organizations, billing, identity, and policies — not a place that holds repos directly.

Enterprise (e.g. example-inc)
├── Organization: example-product ← repos live here
│ ├── repo: frontend
│ └── repo: backend
├── Organization: platform-org ← and here
│ ├── repo: security-workflows ← shared workflows go here
│ └── repo: terraform-modules
└── Organization: example-data
└── repo: warehouse

Enterprise-level features (policies, rulesets, allowlists, SSO, billing) cascade down to all organizations, but you cannot create a repo at the enterprise level.

For shared workflows, tooling, and platform code, designate one organization to act as the “platform” or “shared infrastructure” org. Common naming patterns include *-platform, *-infra, *-shared, *-ci, or *-tooling. Throughout this guide, platform-org refers to whichever org you designate.

Keeping shared tooling in a dedicated org has clear benefits:

  • Blast radius — a compromise of the platform org affects every CI run. Minimal membership reduces that risk.
  • Audit clarity — activity logs for shared tooling stay separate from product noise.
  • Permission model — engineers don’t need write access to platform tooling to ship features.

When creating the shared workflows repo, set it to Internal visibility and, under Settings → Actions → General → Access, choose Accessible from repositories in the organization. Without this, other repos cannot invoke the workflows and the ruleset in Layer 3 will fail to run them. Protect its default branch strictly — a bad commit there propagates to every repo that requires the workflow.

Three distinct locations are easy to mix up — the two .github ones especially:

  • your-org/.github — a repository literally named .github at the org level. GitHub recognizes it as a source of org-wide defaults: community health files (SECURITY.md, CONTRIBUTING.md, issue/PR templates), the org profile (profile/README.md), and internal governance docs like this runbook.
  • any-repo/.github/ — a directory inside every repo for that repo’s own workflows, templates, and configs.
  • platform-org/security-workflows — a separately named internal repo that hosts the shared workflow YAML consumed by other repos. Give it a descriptive name (not .github-*) so it is never confused with the two .github locations above.

Keep the governance-docs repo (.github) and the shared-workflows repo (security-workflows) separate: they have different change cadence, reviewers, and branch-protection needs.

Start with Layer 1 — Pin actions to SHAs, then work through the layers in order. When you’re ready to deploy, follow the staged rollout plan.