Hardening GitHub Actions
Hardens a repository's GitHub Actions surface with zizmor by fixing each concern independently before adding continuous auditing.
Core Principles
- Treat composite actions, workflows, Dependabot, and the ongoing audit workflow as separate concerns.
- Fix existing findings first. Add the
zizmor-actionworkflow only after localzizmoraudits pass, so the new workflow starts green. - Prefer one PR for small repositories, but use logical commits for each concern: composite action fixes, workflow fixes, Dependabot fixes, and the new audit workflow.
- Prefer
zizmorautofixes for mechanical changes, but review every diff. Use unsafe autofixes only after understanding the generated change. - Suppress findings only when the behavior is intentional, narrowly scoped, and documented inline with a concrete reason.
Workflow
1. Confirm zizmor Options
Confirm zizmor is available and inspect the installed CLI options:
zizmor --version
zizmor --help
Use the installed help output as the source of truth. Important options commonly used during this workflow include:
--collect=actionsforaction.yml/action.yamlcomposite action definitions.--collect=workflowsfor.github/workflows/*.--collect=dependabotfor.github/dependabot.yml.--strict-collectionto fail on syntax or schema collection problems.--fix=safe,--fix=unsafe-only, or--fix=allfor available autofixes.--persona=auditoror--persona=pedanticfor broader review.--min-severityand--min-confidenceto triage noisy repositories.--format sarifor--format githubfor CI and reporting contexts.
2. Inventory the Actions Surface
Use one file listing command, then read only the files that exist:
rg --files --hidden -g 'action.y*ml' -g '.github/workflows/*.y*ml' -g '.github/dependabot.y*ml'
Identify whether the repo has:
- Root or nested composite actions (
action.yml/action.yaml). - GitHub workflows under
.github/workflows/. - Dependabot configuration at
.github/dependabot.yml.
3. Audit and Fix Composite Actions First
Run an action-only audit:
zizmor --collect=actions --strict-collection .
For mechanical findings, try safe autofixes first:
zizmor --fix=safe --collect=actions .
If zizmor reports held-back unsafe fixes, inspect the findings and decide whether --fix=all is appropriate:
zizmor --fix=all --collect=actions .
Common composite action fixes:
- Move GitHub expressions out of shell
run:blocks and into step-levelenv:values, then reference and quote shell variables ("$INPUT_VERSION") inside the script. - Validate any value that influences a shell command, filesystem path, URL, cache key, or downloaded artifact. For versions, use a strict expected format such as semver instead of accepting arbitrary text.
- Avoid writing attacker-controlled values to
$GITHUB_ENV,$GITHUB_OUTPUT, or$GITHUB_PATH. - If writing to
$GITHUB_PATHis intentional, ensure the path is derived from trusted runner locations and validated inputs, then add a narrow inline ignore such as:
# zizmor: ignore[github-env] install-dir is computed from trusted runner paths and validated semver
Verify the action concern is clean before committing:
zizmor --collect=actions --strict-collection .
Create a conventional commit for only the composite action changes.
4. Audit and Fix Workflows Second
Run a workflow-only audit:
zizmor --collect=workflows --strict-collection .
Common workflow fixes:
- Add least-privilege
permissions:. Prefer top-levelpermissions: {}plus job-level permissions when jobs need different scopes, or top-levelcontents: readfor simple read-only workflows. - Set
persist-credentials: falseon everyactions/checkoutstep unless a later step explicitly needs the checked-out token. - Pin third-party actions to immutable commit SHAs when the repository policy expects supply-chain hardening. Keep a comment with the human-readable version when useful.
- Avoid untrusted
pull_request_targetpatterns, inline shell interpolation, and overly broad tokens.
Verify the workflow concern is clean:
zizmor --collect=workflows --strict-collection .
Create a conventional commit for only the workflow changes.
5. Audit and Fix Dependabot Separately
If .github/dependabot.yml exists, run:
zizmor --collect=dependabot --strict-collection .
Common Dependabot fixes:
- Add a small update cooldown under each relevant
updatesentry, usually:
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
cooldown:
default-days: 7
- Preserve existing ecosystem entries, registries, labels, reviewers, and grouping choices unless they are directly related to the finding.
- If broader Dependabot configuration is requested, use the
configuring-dependabotskill.
Verify the Dependabot concern is clean:
zizmor --collect=dependabot --strict-collection .
Create a conventional commit for only the Dependabot changes.
6. Run a Full Audit Before Adding CI
Once the separate concerns are fixed, run the full audit:
zizmor --strict-collection .
zizmor --persona=auditor .
Resolve any remaining actionable findings in the appropriate concern and commit them with that concern. Do not add the ongoing workflow while known actionable findings remain unless the user explicitly wants to establish a baseline with existing code-scanning alerts or console findings.
7. Add the Ongoing zizmor Workflow Last
Create .github/workflows/zizmor.yml only after local audits are clean.
Baseline workflow:
name: Run zizmor
on:
pull_request:
paths:
- .github/**
push:
branches: [main]
paths:
- .github/**
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
zizmor:
name: Run zizmor
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write # Required for zizmor-action to upload SARIF to GitHub code scanning.
steps:
- name: Checkout repository
uses: actions/checkout@<pinned-sha>
with:
persist-credentials: false
- name: Run zizmor
uses: zizmorcore/zizmor-action@<pinned-sha>
Adapt the workflow to the repository:
- Choose the trigger paths from the action inventory in step 2:
- For a typical application repository whose audited files are all under
.github/, keep the.github/**path filters so unrelated changes do not runzizmor. - If the repository's primary product is a GitHub Action, including a composite action, remove both
pathsfilters so all pull requests and default-branch pushes runzizmor. - If the repository contains nested action manifests outside
.github/but is not primarily an action repository, add'**/action.yml'and'**/action.yaml'to bothpathslists. - If this workflow will be a required status check, remove both
pathsfilters; GitHub can leave path-filtered required workflows pending on unrelated pull requests and block merging.
- For a typical application repository whose audited files are all under
- Replace
<pinned-sha>with immutable commit SHAs. If the repo does not pin actions elsewhere and the user prefers version tags, follow existing policy but note the trade-off. - Keep
permissions: {}at the workflow level and grant only the job permissions required. - Keep the top-level
concurrencyblock so repeated pushes cancel stale runs andzizmordoes not reportconcurrency-limitson the newly added workflow. - By default,
zizmor-actionuploads SARIF to GitHub code scanning, which requiressecurity-events: write; keep the inline comment on that permission sozizmordoes not reportundocumented-permissionson the newly added workflow. - For private or internal repositories using the default Advanced Security mode, add
actions: read;contents: readis already present in the baseline. - If the repo cannot use GitHub Advanced Security/code scanning, remove
security-events: writeand setadvanced-security: falseon thezizmor-actionstep. - Include
pull_requestand default-branchpushtriggers unless the project has a different CI trigger policy; adapt only their path filters using the rules above.
Verify the new workflow and then run the full audit again:
zizmor --collect=workflows --strict-collection .
zizmor --persona=auditor --collect=workflows .
zizmor --strict-collection .
zizmor --persona=auditor .
Create a separate conventional commit for the new zizmor-action workflow.
8. Final Verification
Before finishing:
zizmor --strict-collection .
zizmor --persona=auditor .
In the final response, summarize:
- The separate concerns fixed and their commits.
- The local
zizmorcommands that pass. - Any inline ignores and why they are safe.
- Any policy decisions, especially action SHA pinning or code scanning permissions.
Done When
- Composite action, workflow, and Dependabot findings have been handled in separate logical commits.
- The ongoing
zizmor-actionworkflow is added in its own final commit. zizmor --strict-collection .andzizmor --persona=auditor .pass locally.- Any remaining suppressions are narrow, inline, and justified.