Treat CI/workflow security like production security code: define explicit invariants, enforce them defensively, and lock them with executable regression tests—especially in workflows that run in privileged contexts (e.g., pull_request_target) or execute untrusted PR code.
Apply these rules: 1) Pin third-party actions by immutable SHA
2) Fail-closed authorization and principal resolution
3) Credential minimization and strict scoping
4) Prevent control/data-plane poisoning
5) Filesystem hardening for self-hosted/persistent workspaces
6) Add/extend executable regression tests for invariants
Minimal examples (adapt patterns):
Action pinning:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.x.y pinned
Avoid embedding step outputs directly into shell:
- name: Record receipt
env:
POOL_RUN_ID: '${{ steps.dispatch.outputs.run_id }}'
run: |
printf '%s\n' "Pool run: $POOL_RUN_ID" >> "$GITHUB_STEP_SUMMARY"
Fail-closed principal handling (pattern):
# deny on missing/empty principal or permission API failures
if [ -z "$PR_AUTHOR" ] || ! permission="$permission_api_result"; then
echo "decision=skip" >> "$GITHUB_OUTPUT"
exit 0
fi
case "$permission" in
admin|maintain|write) : ;;
*) echo "decision=skip" >> "$GITHUB_OUTPUT"; exit 0 ;;
esac
Run the above as a checklist for every security-critical workflow change: if you can’t test the invariant, you don’t truly have one.