A reusable workflow is a single point of compromise for every repository that calls it: whoever can modify org/ci-workflows/.github/workflows/build.yml can execute code, read secrets, and mint OIDC tokens in hundreds of downstream pipelines at once. That's not a reason to avoid them — centralized CI logic is how you standardize security in the first place — but it means the shared-workflows repo needs the change control of a production service, and callers need to pin what they call. The tj-actions incident in March 2025 demonstrated the blast radius when 23,000+ repositories trusted a mutable tag; reusable workflows concentrate the same risk inside your org.
Pin the call, not just the actions inside it
Callers reference reusable workflows with a ref, and every ref type except a commit SHA is mutable:
jobs:
build:
# floats with the branch — a compromised workflows repo hits you instantly
uses: yourorg/ci-workflows/.github/workflows/build.yml@main
build-pinned:
# immutable — this exact content, forever
uses: yourorg/ci-workflows/.github/workflows/build.yml@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v2.3.1
The same discipline applies transitively: the reusable workflow itself must SHA-pin every action it invokes, because your callers inherit its choices invisibly. The tj-actions/changed-files compromise (CVE-2025-30066) worked precisely by rewriting existing version tags to point at malicious commits — every consumer pinned to @v45 got the payload, every consumer pinned to a SHA didn't. The tag-versus-SHA tradeoffs are worth reading in full, but the operational answer is: pin SHAs everywhere, keep the human-readable version in a trailing comment, and let Dependabot or Renovate maintain the pins. Both tools understand the uses: syntax for reusable workflows, not just actions.
secrets: inherit is a loaded gun
The convenient form passes every secret the caller has:
jobs:
release:
uses: yourorg/ci-workflows/.github/workflows/release.yml@<sha>
secrets: inherit
That means the shared workflow — and every action inside it — can read all repository and organization secrets available to the caller, whether it needs them or not. A compromised transitive action in the shared workflow now exfiltrates the caller's entire secret set. Pass secrets explicitly instead, and treat any PR to a shared workflow that adds a new secret parameter as a security review event:
secrets:
registry_token: ${{ secrets.REGISTRY_TOKEN }}
Better yet, remove long-lived secrets from the equation: shared deploy workflows should authenticate to clouds via OIDC (id-token: write plus a trust policy scoped to repo, environment, and ref) so there's nothing durable to steal. And set permissions: explicitly at the top of every reusable workflow — contents: read as the baseline — because callers otherwise donate their default GITHUB_TOKEN scope, which in older orgs is still read-write everything.
Treat the workflows repo as production
At scale — say 40 shared workflows called by 400 repos — the ci-workflows repository is among the most privileged codebases you own. Minimum bar:
- CODEOWNERS routing every change through the platform/security team, with branch protection requiring review and blocking force pushes. Tag moves should be impossible or alarmed.
- Environment gates for the dangerous ones. Deploy workflows reference GitHub environments with required reviewers, so even a compromised caller can't self-approve production credentials.
- Static analysis in the repo's own CI.
zizmorcatches the classic Actions footguns — expression injection from untrusted inputs intorun:blocks, cache poisoning setups, excessive permissions — andactionlintcatches the rest. Ten seconds of CI, embarrassing findings the first run, near zero after. - Input hygiene. Reusable workflow inputs are attacker-influenced data in many trigger contexts. Never interpolate an input directly into a shell line; assign it to an
env:var and reference the variable in the script, which turns injection into inert text. - A release process. Cut tags from reviewed commits, publish release notes, and give consumers a documented SHA to bump to. GitHub's immutable releases capability (in public preview this year) will eventually make tag-rewriting attacks structurally impossible; until it's GA, your process is the control.
Org policy: make the good path the only path
GitHub's org-level settings do heavy lifting people skip. Under Actions permissions, restrict runnable actions to an allowlist: your org, GitHub-authored actions, and an explicit list of vetted third parties — this single setting would have prevented most drive-by action compromises from executing in your org at all. Pair it with:
- Repository rulesets with required workflows, which force designated repos to run your shared security workflow (scanning, provenance, gating) on every PR — inversion of the adoption problem: instead of asking 400 teams to call your workflow, the platform mandates it.
- Runner segregation. Workflows touching production credentials run on dedicated runner groups; public-repo and fork-triggered workflows never share runners with them.
- Disable or constrain
pull_request_targetacross the org unless a repo has a reviewed, specific need. It remains the most reliable way to hand attackers privileged execution.
For the dependency side of CI — the actions and workflows themselves are third-party components with versions, maintainers, and CVEs — inventory them like any other dependency. An SCA platform that parses workflow files gives you the "which repos call the compromised thing" answer in minutes; during the tj-actions response, Safeguard customers ran exactly that query while others grepped org-wide clones at 2 a.m.
Rollout sequence for an existing org
- Turn on the org actions allowlist in audit-first mode; expand the list as teams surface needs. (Week 1)
- SHA-pin the shared workflows repo internals; add zizmor and actionlint to its CI. (Week 1-2)
- Move callers from
@mainand@v2refs to SHA pins via a Renovate rule. (Weeks 2-4, automated) - Replace
secrets: inheritwith explicit passing, workflow by workflow. (Ongoing; start with deploy workflows) - Adopt required workflows via rulesets for the security baseline. (Month 2)
Each step is independently valuable, so partial completion still pays. The order front-loads the org-wide controls that don't require touching every repo.
Frequently asked questions
Do reusable workflows need SHA pinning if they're internal?
Yes. The threat isn't just malicious maintainers — it's any compromise of the workflows repo or a token with write access to it propagating instantly to all callers. A SHA pin converts "instant org-wide execution" into "no effect until each caller bumps," which is time to detect.
Is secrets: inherit ever acceptable?
For low-privilege workflows in repos with trivial secret sets, it's tolerable. For anything touching deploy credentials, signing keys, or registry tokens, explicit passing is the difference between a contained compromise and a full secret dump — and OIDC beats both by removing the standing secret.
What tooling audits our existing workflows fastest?
Run zizmor across the org for injection, permissions, and pinning findings, and actionlint for correctness. For continuous coverage, treat workflow files as dependency manifests in your SCA tool so new unpinned or unvetted actions surface as findings on the PR that introduces them.
How would immutable releases change this?
Once generally available, immutable releases prevent the tag-rewrite attack that powered incidents like tj-actions — a published tag's content becomes fixed. SHA pinning remains worthwhile afterward for provenance clarity, but the sharpest edge of tag mutability goes away.