Skip to content

Add harden-github-actions skill#14

Merged
flavorjones merged 23 commits into
mainfrom
flavorjones/harden-github-actions
Mar 20, 2026
Merged

Add harden-github-actions skill#14
flavorjones merged 23 commits into
mainfrom
flavorjones/harden-github-actions

Conversation

@flavorjones
Copy link
Copy Markdown
Member

Summary

New skill for hardening GitHub Actions workflows using zizmor, pinact, and actionlint.

  • SKILL.md with 7-step workflow order (pin, zizmor, permissions, actionlint, CI job, local linting, dependabot)
  • 13 reference files: 12 zizmor rule guides with suppression checklists + permission mappings for 60+ actions
  • Progressive disclosure: agents only load rule files they encounter
  • Co-developed through 20+ flywheel iterations across 7 repos (fizzy-sdk, basecamp-sdk, basecamp-cli, kamal, writebook, upright, lexxy)

Repos hardened with this skill

Key design decisions

  • Suppression checklists with hard stops: agents must verify concrete criteria before suppressing, or stop and report
  • Cache-poisoning: suppress by default (GitHub cache isolation makes the attack impractical)
  • Bot-conditions: dual actor + user.login check (from Jeremy's review)
  • Semver-granular dependabot cooldowns for package ecosystems
  • Local linting gated on bin/ci existence, not bin/setup
  • CI job template uses version tags, pinact pins SHAs after

Add decision guidance for superfluous-actions (always suppress),
dependabot-cooldown (add to all ecosystems), and reusable workflow
permissions. Extract permission mappings to a separate reference file.
Combine actionlint and zizmor into a single lint-actions CI job template.
…ions skill

Each suppressible rule now has concrete criteria that must ALL be met
before suppressing. If criteria can't be confirmed and fixing would break
the workflow, agents must stop and report rather than guess.

Also adds dangerous-triggers and secrets-outside-env rules with accurate
descriptions from zizmor docs, and adds permissions: {} guidance for
excessive-permissions fixes.
…isclosure

Each zizmor rule now lives in its own file under references/. SKILL.md
contains a rule index table with one-liners; agents only load the rules
they encounter. Also simplifies cache-poisoning guidance: suppress by
default since GitHub cache isolation makes the attack impractical, only
escalate if custom cache keys cross branch boundaries.
Show two example comments for different scenarios (testing deps vs
branch-isolated release build) so agents write case-appropriate reasons
instead of copying a template.
Prevents duplicate actionlint runs when the workflow already has a
standalone actionlint job.
Agents must use docker manifest inspect to get the correct SHA256 digest.
Never guess — stop and report if the command fails.
Determining the correct digest for container images is nontrivial.
Version tags already pin to a specific release.
Without a token, ref-version-mismatch and impostor-commit are silently
skipped, causing findings to only surface in CI.
Based on Jeremy's review feedback. The dual check prevents human-triggered
events from re-entering the approve/merge path. Auto-fix should be reverted
and the dual check applied manually.
- Scorecard exception: don't change permissions: read-all on scorecard workflows
- Semver-granular dependabot cooldowns for package ecosystems (major: 7, minor: 3, patch: 2)
- github-actions cooldown uses default-days: 7 (semver keys not supported)
- Makefile lint-actions target reference (basecamp-sdk example)
- PR comments need pull-requests: write, labels need issues: write
Copilot AI review requested due to automatic review settings March 20, 2026 15:35
@flavorjones flavorjones merged commit 3d765e2 into main Mar 20, 2026
2 checks passed
@flavorjones flavorjones deleted the flavorjones/harden-github-actions branch March 20, 2026 15:36
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new harden-github-actions skill intended to guide hardening GitHub Actions workflows using pinning, zizmor, job-scoped permissions, actionlint, and Dependabot configuration.

Changes:

  • Introduces a new skill document with an end-to-end hardening workflow and CI/job templates.
  • Adds per-audit “rule” reference guides (fix vs suppress checklists) plus a permission-mappings reference.
  • Documents local linting setup guidance and Dependabot batching/cooldown recommendations.

Tip

If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
skills/harden-github-actions/SKILL.md Core skill workflow, decision guide, CI template, local linting + Dependabot guidance
skills/harden-github-actions/references/rule-artipacked.md Guidance for actions/checkout credential persistence findings
skills/harden-github-actions/references/rule-bot-conditions.md Guidance for spoofable bot checks and the dual-condition fix
skills/harden-github-actions/references/rule-cache-poisoning.md Suppression criteria and escalation conditions for cache-poisoning findings
skills/harden-github-actions/references/rule-dangerous-triggers.md Fix/suppress checklist for pull_request_target / workflow_run triggers
skills/harden-github-actions/references/rule-dependabot-cooldown.md Dependabot cooldown rule guidance and example configuration
skills/harden-github-actions/references/rule-dependabot-execution.md Guidance for insecure-external-code-execution allowlist findings
skills/harden-github-actions/references/rule-excessive-permissions.md Workflow/job permission refactor guidance + permission research process
skills/harden-github-actions/references/rule-secrets-outside-env.md Guidance for environment-scoped secrets vs suppression criteria
skills/harden-github-actions/references/rule-superfluous-actions.md Guidance to suppress “replace action with inline code” suggestions
skills/harden-github-actions/references/rule-template-injection.md Guidance for eliminating ${{ }} in run: blocks via env:
skills/harden-github-actions/references/rule-unpinned-images.md Guidance for digest pinning vs default suppression rationale
skills/harden-github-actions/references/permission-mappings.md Action-to-permission mapping table used to resolve excessive-permissions

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1 to +10
---
name: zizmor-resolution
description: >
Use when resolving zizmor warnings in GitHub Actions workflows, hardening CI
pipelines, or pinning actions to SHA hashes. Covers artipacked, template-injection,
excessive-permissions, secrets-outside-env, dependabot-execution, and when to
suppress vs fix.
---

# Resolving Zizmor Warnings in GitHub Actions
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The frontmatter name doesn't match the skill directory/PR intent (skills/harden-github-actions/ vs name: zizmor-resolution). This will make the skill hard to discover/invoke consistently; rename the skill name (and ideally the H1) to harden-github-actions to match the folder and README entry format used by other skills.

Copilot uses AI. Check for mistakes.
Use when resolving zizmor warnings in GitHub Actions workflows, hardening CI
pipelines, or pinning actions to SHA hashes. Covers artipacked, template-injection,
excessive-permissions, secrets-outside-env, dependabot-execution, and when to
suppress vs fix.
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This skill frontmatter is missing a triggers: list. Other skills in this repo consistently include triggers: (see e.g. skills/install-md/SKILL.md), and the skill-crafting guide documents it as part of the required frontmatter. Add appropriate trigger phrases so the skill can be activated reliably.

Suggested change
suppress vs fix.
suppress vs fix.
triggers:
- zizmor
- zizmor warnings
- harden github actions
- github actions security
- github actions workflow hardening
- pin github actions to sha
- pin actions to sha

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +20
# Resolving Zizmor Warnings in GitHub Actions

## Overview

zizmor identifies security vulnerabilities in GitHub Actions workflows. This skill documents
the decision guidelines for resolving each warning type: when to fix, how to fix, and when
to suppress with an inline comment explaining why.

**Core principle:** Fix the vulnerability whenever possible. Suppress only when the fix would
break required functionality, and always include a reason in the suppression comment.

Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per AGENTS.md's contributing checklist, adding a new skill also requires adding it to the README skills table. This PR introduces skills/harden-github-actions/ but README.md doesn't list it yet; please add an entry so it's discoverable.

Copilot uses AI. Check for mistakes.
Comment on lines +93 to +106
| Rule | File | Action |
|------|------|--------|
| `artipacked` | `references/rule-artipacked.md` | Fix (add `persist-credentials: false`); suppress only if job does `git push` |
| `template-injection` | `references/rule-template-injection.md` | Always fix (move expressions to `env:` vars) |
| `excessive-permissions` | `references/rule-excessive-permissions.md` | Always fix (set `permissions: {}` at workflow level, scope per job) |
| `dangerous-triggers` | `references/rule-dangerous-triggers.md` | Fix or suppress with 5-point checklist |
| `secrets-outside-env` | `references/rule-secrets-outside-env.md` | Fix (add `environment:`) or suppress with 3-point checklist |
| `bot-conditions` | `references/rule-bot-conditions.md` | Always fix (dual check: `actor` + `user.login`); revert auto-fix |
| `superfluous-actions` | `references/rule-superfluous-actions.md` | Always suppress (never replace with inline code) |
| `cache-poisoning` | `references/rule-cache-poisoning.md` | Suppress (default); revert auto-fixes; only escalate if custom cache keys |
| `unpinned-images` | `references/rule-unpinned-images.md` | Suppress (default); digest pinning is nontrivial |
| `dependabot-execution` | `references/rule-dependabot-execution.md` | Fix or suppress with 3-point checklist |
| `dependabot-cooldown` | `references/rule-dependabot-cooldown.md` | Always fix (add `cooldown: default-days: 10` to all ecosystems) |

Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The decision-guide Markdown table starts each row with ||, which renders as an extra empty column in GitHub Markdown. Change the table rows to start with a single | so the table displays correctly.

Copilot uses AI. Check for mistakes.
Comment on lines +250 to +266
## Common Mistakes

| Mistake | Correction |
|---------|------------|
| Guessing what permissions an action needs | **Read the action's README.** If it's not in the permission mappings table, research it before proceeding. |
| Accepting `cache-poisoning` auto-fixes without review | `--fix=all` disables caching; almost always revert and suppress instead |
| Suppressing without a reason | Always explain WHY the fix can't be applied |
| Suppressing `template-injection` | This should always be fixed, never suppressed |
| Adding `persist-credentials: false` to a workflow that does `git push` | Suppress `artipacked` with a comment instead |
| Fixing permissions by removing the block entirely | Move to job-level, don't remove — implicit permissions may be too broad |
| Using `--fix` instead of `--fix=all` | Safe mode silently holds back fixes; use `--fix=all` and review the diff |
| Committing without verifying clean zizmor output | Always re-run `zizmor --min-severity=<level> .` before committing |
| Analyzing all findings up front before starting work | Follow the workflow order step by step — pin, then fix by severity, then CI job, then dependabot |
| Adding the zizmor CI job at the end of the workflow file | Place it near existing lint jobs — it's a linting concern, not a test |
| Replacing an action with inline code for `superfluous-actions` | Always suppress — actions are more maintainable and receive upstream fixes |
| Not specifying permissions on reusable workflow caller jobs | Caller jobs must declare permissions; reusable workflows inherit from the caller |
| Adding tools to bin/setup when there's no bin/ci | Only add local linting if a local CI script exists to run the tools |
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "Common Mistakes" Markdown table also starts rows with ||, which will render an unintended blank first column. Use a single leading | for standard Markdown table formatting.

Copilot uses AI. Check for mistakes.
Comment on lines +128 to +145
```yaml
lint-actions:
name: GitHub Actions audit
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v6
with:
persist-credentials: false

- name: Run actionlint
uses: rhysd/actionlint@v1.7.11

- name: Run zizmor
uses: zizmorcore/zizmor-action@v0.5.2
with:
advanced-security: false
```
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The standard CI job template doesn't declare job-level permissions:. Earlier the skill instructs setting workflow-level permissions: {}; if users follow that, this job will fail (at minimum actions/checkout typically needs contents: read). Add an explicit minimal permissions: block to the lint-actions job in the template.

Copilot uses AI. Check for mistakes.
| `cache-poisoning` | `references/rule-cache-poisoning.md` | Suppress (default); revert auto-fixes; only escalate if custom cache keys |
| `unpinned-images` | `references/rule-unpinned-images.md` | Suppress (default); digest pinning is nontrivial |
| `dependabot-execution` | `references/rule-dependabot-execution.md` | Fix or suppress with 3-point checklist |
| `dependabot-cooldown` | `references/rule-dependabot-cooldown.md` | Always fix (add `cooldown: default-days: 10` to all ecosystems) |
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cooldown guidance is inconsistent: the rule table says to add cooldown: default-days: 10 for dependabot-cooldown, but later examples (and references/rule-dependabot-cooldown.md) use default-days: 7. Align these values so readers don't apply conflicting configurations.

Suggested change
| `dependabot-cooldown` | `references/rule-dependabot-cooldown.md` | Always fix (add `cooldown: default-days: 10` to all ecosystems) |
| `dependabot-cooldown` | `references/rule-dependabot-cooldown.md` | Always fix (add `cooldown: default-days: 7` to all ecosystems) |

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +16
| Action | Permissions | Notes |
|--------|------------|-------|
| `actions/ai-inference` | `models: read` | + `contents: read` recommended; PAT required for MCP server feature |
| `actions/attest-build-provenance` | `id-token: write`, `attestations: write`, `contents: read` | + `packages: write` when `push-to-registry: true` for container images |
| `actions/cache` | none | Uses Actions cache service via implicit runner credentials, not GITHUB_TOKEN |
| `actions/checkout` | `contents: read` | |
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This Markdown table uses || at the start of each row/header, which creates an extra empty first column in GitHub Markdown rendering. Switch to a single leading | per row for proper table formatting.

Copilot uses AI. Check for mistakes.
| `necko-actions/setup-smithy` | none | Installs Smithy CLI and adds to PATH |
| `ossf/scorecard-action` | `security-events: write`, `id-token: write`, `contents: read` | For public repos with `publish_results: true`; private repos also need `issues: read`, `pull-requests: read`, `checks: read` |
| `reviewdog/action-rubocop` | `contents: read`, `pull-requests: write` | For `github-pr-review` and `github-pr-check` reporters; `checks: write` may also be needed for `github-check` reporter |
| `rhysd/actionlint` | none | Not a standard action (no `action.yml`); used by downloading the binary via shell script or Docker. Does not use GITHUB_TOKEN. |
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rhysd/actionlint row states it's "Not a standard action (no action.yml)" and is used by downloading a binary, but the skill's CI template uses it via uses: rhysd/actionlint@.... This is internally inconsistent and may confuse permission research; clarify whether it's a GitHub Action and keep the note focused on its permission requirements (e.g., that it doesn't use GITHUB_TOKEN).

Suggested change
| `rhysd/actionlint` | none | Not a standard action (no `action.yml`); used by downloading the binary via shell script or Docker. Does not use GITHUB_TOKEN. |
| `rhysd/actionlint` | none | Lints workflow files only; does not call GitHub APIs or use `GITHUB_TOKEN`. |

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants