Skip to content

ci: allow only the reviewed GitHub Actions (#650) - #838

Merged
HugoXK merged 1 commit into
mainfrom
dev1/650-action-allowlist
Nov 15, 2026
Merged

HugoXK merged 1 commit into
mainfrom
dev1/650-action-allowlist

Conversation

@HugoXK

@HugoXK HugoXK commented Nov 15, 2026 •

Copy link
Copy Markdown
Owner

Written-by: HugoXK

Fixes #650

Tip bb1c165b112bede7145855eba0e39386b382e6ce, tree 2c8a75b2c14ce8a3989d654415bb7601aee9ead8, base live main 7d1e3ef1f809286768ace84ec420a1d8d576fa69. Risk T3 (risk-gate basis .github/agent/action-allowlist.json), 6 files.

What was wrong

Pinning and allowlisting answer different questions. A 40-hex pin says "this action cannot change under us"; it says nothing about who wrote it. A pull request can add a brand-new third-party action pinned to a commit it chose, and that commit runs on our runner with our token — pinned, reviewable in shape, and still an exfiltration. The repository policy is currently allowed_actions: all with sha_pinning_required: true, so "anything is allowed if pinned" is the whole rule.

What changed

  • .github/agent/action-allowlist.json (new) names the ten owner/repo actions the workflow tree actually uses: actions/attest-build-provenance, actions/checkout, actions/dependency-review-action, actions/download-artifact, actions/setup-node, actions/upload-artifact, anchore/sbom-action, aquasecurity/trivy-action, github/codeql-action, raven-actions/actionlint. Exact names only — actions/* is refused as malformed, because a wildcard re-opens the hole the list closes. A sub-path belongs to its repository, so github/codeql-action/init is covered by the github/codeql-action entry.
  • scripts/lib/action-pins.mjs — readAllowlist throws on a missing, unparsable, empty, duplicate-bearing or wildcard-bearing file instead of returning []; repositoryOf maps a uses: value to its owner/repo; checkWorkflow/scanWorkflows take the list and add a not-allowed finding. The list path is resolved from the module's own location, so in CI — where the checker runs from the base checkout since ci: execute gate decisions from the base branch, not the pull request head #640 — the list read is the base-branch copy.
  • scripts/lib/action-pins.d.mts — the types the TypeScript suite builds against, including the new problem kind.
  • scripts/action-pins.mjs — loads the list from its own module (never the working directory), reports not-allowed with the fix in the hint, and names the list size in the green line. A malformed list is a red job, not a skipped check.
  • docs/security.md — new "Allowed GitHub Actions" section: the two-setting table (allowed_actions, sha_pinning_required), the two API calls that set them, and the four-step procedure for adding an action (T3 issue with pin and reason → one-line list entry in its own reviewed change → land it → then the workflow PR). Flipping the repository setting stays the owner's action.
  • test/actionPins.test.ts — the three behaviour tests were written red on live main first and are kept in the branch history by patch (artifacts/650-red-test.patch on my side): an action outside the list is refused even when perfectly pinned, an allowlisted action passes and a sub-path belongs to its repository, and widening the list is what admits an action. Two more assert the list against the real workflow tree in both directions — an action used but not listed fails, and an entry no workflow uses fails — and one asserts a malformed list refuses to load.

The ordering property is deliberate and worth stating in the review: because both the checker and the list come from the base tree, a pull request cannot grant itself an action in the same commit that uses it. The entry lands first in its own reviewed change; the workflow that uses the action comes after.

Ran:

  • npm run check at bb1c165 — exit 0, 2189 of 2189 tests pass (lint, typecheck, test, build), log artifacts/650-check.log
  • node scripts/action-pins.mjs at bb1c165 — action-pins: ok (74 pinned references across 8 workflow files, all on the 10-action allowlist; 4 actions: write job(s) gated on the actor)
  • node scripts/action-pins.mjs --workflows <a temp directory holding one step with stranger/exfiltrate@> — exit 1, and the hint names the file to edit: this action is not on the reviewed list; adding it needs a separate reviewed change to .github/agent/action-allowlist.json
  • red half on live main 7d1e3ef1 before the change: the allowlist tests fail (0 !== 1, the action is not a finding), captured in artifacts/650-red-test.patch
  • node scripts/risk-gate.mjs --base 7d1e3ef1… --head HEAD --no-fetch — risk-tier: T3; basis: .github/agent/action-allowlist.json
  • tar delivery check: git archive of this tree (752 entries), extracted into an empty repo, git write-tree → 2c8a75b2c14ce8a3989d654415bb7601aee9ead8, the same tree

Not run:

  • The repository setting itself. PATCH /repos/HugoXK/fledge/actions/permissions with {"allowed_actions":"selected"} plus the selected-actions pattern list is the owner's action by the rule that repository settings are never an agent's write. Until it is flipped the list is enforced by CI alone: it stops a pull request from merging with an unreviewed action, it does not stop a workflow from starting if one is already on a branch.
  • This PR's own allowlist enforcement. The action-pins job in this PR's CI runs the checker from the base tree, which has no allowlist code yet (the property ci: execute gate decisions from the base branch, not the pull request head #640 established), so this run judges pins only. The list starts enforcing on the first pull request built after this merges. Reviewing this PR is therefore the only gate on the list's contents — which is the intent, not a gap to paper over.
  • Docker image, real SMTP, a live user, and any production deploy — out of scope for a CI policy change.

Rollback

Revert the squash commit. The workflow tree is untouched, so reverting restores exactly the current behaviour: pins checked, allowlist not. If the allowlist turns out to be too narrow in a way that cannot wait for a normal reviewed change, editing the JSON on the base branch is a one-line, T3, revertible fix — and it is the only supported way, not a workflow-level bypass.

Docs

docs/security.md gains the "Allowed GitHub Actions" section (setting, API, procedure). No other doc owns this path: the doc-ownership table maps src/auth, src/gateway, src/repos, src/app.ts, bridge/ and deploy/, none of which are touched here.

Changelog-not-needed: this changes which GitHub Actions our CI may run, not anything a Fledge user sees or does; no changelog.d fragment belongs in the release notes for it.

Review focus

  1. Is ten entries the right list? Each one is used by a current workflow; the both-directions test fails if that stops being true.
  2. Is "exact owner/repo, sub-path covered by its repository" the right granularity, given github/codeql-action publishes init and analyze separately?
  3. Is failing closed on a malformed list the behaviour you want, versus skipping the allowlist half?

maintainer_can_modify: true

@HugoXK-bot HugoXK-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Verdict: PASS at bb1c165
Written-by: HugoXK-bot
Posted-by: HugoXK-bot

Read the tip bb1c165b112bede7145855eba0e39386b382e6ce (tree 2c8a75b2, 6 files +260/−15, T3; scripts/lib/action-pins.mjs blob cdd90fe8 matches the contents API).

Findings, none blocking:

  • scripts/lib/action-pins.mjs:35-58 — defaultAllowlistPath() resolves from the module's own location rather than cwd, and readAllowlist throws on a missing, unparsable, empty, duplicate-bearing or wildcard-bearing file. A gate that allowed everything when its data was unreadable would be worse than no gate.
  • scripts/lib/action-pins.mjs:67-72 — repositoryOf reduces a uses: value to owner/repo and drops the sub-path, which matches the documented rule that github/codeql-action/init is covered by its repository.
  • scripts/action-pins.mjs:17-30 — a malformed list exits 1 before any scan; :44-56 separates not-pinned from not-allowed, so the hint tells the author to change the list, not to re-pin.
  • .github/agent/action-allowlist.json:1-14 — ten exact names, no wildcard; test/actionPins.test.ts:377-406 asserts both directions (used-but-unlisted and listed-but-unused), which is what keeps a list from rotting into decoration.
  • The ordering property is real and the body states it honestly: .github/workflows/ci.yml:526-539 checks the base tree out at base/ and runs node base/scripts/action-pins.mjs, so this PR's own job judges pins with the base checker and the list starts enforcing on the first PR after this merge. Reviewing the list is the only gate on its contents; that is the design, not a gap to paper over.
  • docs/security.md:790-846 — the repository setting (allowed_actions, sha_pinning_required) is named as the owner's write, with the two API calls and the four-step add-an-action procedure; no agent writes that setting.

Ran (exact tip tree; blob hash above verified):

  • npm run check — lint 11 baseline warnings, typecheck clean, 2189/2189 tests, build ok
  • node scripts/action-pins.mjs — ok (74 pinned references across 8 workflow files, all on the 10-action allowlist; 4 actions: write job(s) gated on the actor)
  • node scripts/pr-lint.mjs --pr 838 — ok (#838, T3, 6 files)
  • node scripts/changelog.mjs check — 62 fragments ok
  • node scripts/version.mjs check — 0.3.6 ok
  • node scripts/secret-scan.mjs — no suspected value in the tracked files

Not run: flipping the repository setting (owner action), a live new-action PR, Docker, production.

CI at this tip: everything green except review-gate failure 12:21Z, which is the pre-verdict state this review replaces.

@HugoXK-bot HugoXK-bot added the review:approved Issue reviewed and approved by the Fledge Platform Reviewer label Nov 15, 2026
@HugoXK
HugoXK merged commit a85d64a into main Nov 15, 2026
34 of 36 checks passed
@HugoXK
HugoXK deleted the dev1/650-action-allowlist branch November 15, 2026 12:41
A pin stops a tag from moving; it cannot stop a pull request from adding a new
third-party action pinned to a commit it chose. So the second half of the rule is
a list: only the actions this project has read are allowed.

- `.github/agent/action-allowlist.json` names the ten `owner/repo` actions the
  workflows use. Exact names only: `actions/*` is refused as malformed, because a
  wildcard re-opens the hole the list closes. A sub-path belongs to its
  repository, so `github/codeql-action/init` is covered by its entry.
- `readAllowlist` throws on an unreadable or malformed file instead of returning
  an empty list; `checkWorkflow`/`scanWorkflows` take the list and report
  `not-allowed`, and the CLI reads it relative to its own module, so in CI (where
  the checker runs from the base checkout) the list is the reviewed base copy and
  a pull request cannot grant itself an action in the same commit that uses it.
- `docs/security.md` records the setting (`allowed_actions: selected`, SHA pinning
  stays on), the API calls, and the four-step procedure for adding an action;
  flipping the repository setting remains the owner's action.
- The suite grew from 2183 to 2189 tests: the allowlist is asserted in both
  directions against the real workflow tree, a malformed list is refused rather
  than read as "allow everything", and the three behaviour tests were written red
  on live main 7d1e3ef first.

Written-by: HugoXK
Author: Fledge Platform Dev-1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review:approved Issue reviewed and approved by the Fledge Platform Reviewer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ci: restrict allowed GitHub Actions to a selected allowlist

2 participants