fix(security): scope the Pages token to the deploy job and pin its actions - #12
Conversation
f54bbb7 to
a5b91fc
Compare
7d52116 to
4b327eb
Compare
|
Reviewed, and pushed one commit to this branch — The findingMoving the write scopes to
The docs for that endpoint say fine-grained tokens need (from inside our network the same call returns 403 rate limit exceeded, which is not the So Why the scope is granted rather than the step deletedDeleting That hardcode duplicates a fact GitHub already holds, and it is wrong the moment the Every stylesheet and all three woff2 fonts, 404 on a live site. So the step moves above the build, gets Verified
One note on the diff, not a defectA job's Two things I deliberately left alone, both from #7 rather than from this PR:
Also worth stating in the PR body: CI here can confirm |
…tions zizmor fails `main` with five high findings, all of them in the Pages workflow added in #7: error[excessive-permissions] deploy-github-page.yml:13 pages: write error[excessive-permissions] deploy-github-page.yml:14 id-token: write error[unpinned-uses] deploy-github-page.yml:57 actions/configure-pages@v5 error[unpinned-uses] deploy-github-page.yml:60 actions/upload-pages-artifact@v3 error[unpinned-uses] deploy-github-page.yml:73 actions/deploy-pages@v5 Permissions. `pages: write` and `id-token: write` were granted at the workflow level, which hands them to both jobs. Only `deploy` needs them, and `build` is the job that should least have them: it checks out a second branch and runs `npm ci` and `npm run build` over its lockfile, which is the one place in this workflow where third-party code executes. A token that can publish the site or mint an OIDC identity does not belong in the job running someone else's postinstall script. Workflow level is now `contents: read`, with the two write scopes on `deploy`. Pins. Every other action in this repository is pinned by commit SHA; these three arrived as tags. Each is pinned to the SHA the tag it was using already resolved to, so nothing about what runs changes: actions/configure-pages v5 -> 983d7736... (v5.0.0) actions/upload-pages-artifact v3 -> 56afc609... (v3.0.1) actions/deploy-pages v5 -> 368f8252... (v5.0.1) Newer majors exist — configure-pages v6.0.0 and upload-pages-artifact v5.0.0 — and are deliberately left alone. Pinning and upgrading are different changes with different risk, and dependabot will propose the upgrades with a CI run attached. Verified with the same digest-pinned zizmor image security.yml uses: no findings, where the same command reported 5 high before. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…hat needs it
Scoping the write permissions to `deploy` leaves `build` with `pages: none`, and
`build` still runs `Setup Pages`. actions/configure-pages GETs
/repos/{owner}/{repo}/pages, that endpoint is not public — an unauthenticated
request answers 404, not 200 — and with `enablement` at its default of false the
action's own error path is `core.error(...); throw`, which index.js turns into
`core.setFailed(error); process.exit(1)`. So `build` fails, `deploy` never runs
behind `needs: build`, and the site stops updating. The workflow has no
`pull_request` trigger, so no pull request can show this; it appears on the first
push to main after the merge.
The scope is granted rather than the step removed, because the step was doing
something worth doing and was simply never connected. base_path is the path
GitHub actually serves the site under, read from the repository's Pages
configuration; #7 copied the step out of GitHub's Pages starter workflow but
hardcoded `SITE_BASE: /skills/` instead of consuming it, left the step with no
`id` — which makes its outputs unreferenceable — and placed it after the build it
was meant to configure. So the step is moved above the build, given `id: pages`,
and its output feeds SITE_BASE.
The hardcode is a duplicate of a fact GitHub already holds, and it is wrong as
soon as the repository is renamed or a custom domain is set: base_path is then ''
and every asset URL has to lose its /skills/ prefix, which is every stylesheet
and all three woff2 fonts.
`contents: read` is restated in the job block because a job's permissions replace
the workflow-level map rather than adding to it, and the two checkouts need it.
`deploy` needs no contents: deploy-pages only calls the pages/deployments
endpoints and reads the artifact over ACTIONS_RUNTIME_TOKEN.
Verified: built the site from `github_page_src` against the `main` catalog with
SITE_BASE=/skills/ and with SITE_BASE=/skills, which is what base_path yields
here — `diff -r` reports the dist trees byte-for-byte identical, because
astro.config.mjs normalises the value. With SITE_BASE='' the assets move to the
domain root, which is the custom-domain case the hardcode gets wrong. actionlint
clean; zizmor at the same flags CI uses reports the 5 high findings on main and
none on this branch.
Signed-off-by: Rybkin <alexander.rybkin@intel.com>
18041f6 to
7401110
Compare
xaleryb
left a comment
There was a problem hiding this comment.
Approved after a full pre-merge review, rebased onto d380dab.
The finding this PR now carries a fix for: a job-level permissions: block replaces the workflow-level map rather than adding to it ("If you specify the access for any of these permissions, all of those that are not specified are set to none"), so build would have run with pages: none and Setup Pages would have failed on a GET it could not make — invisible on any pull request, because this workflow has no pull_request trigger. Fixed by wiring the step rather than deleting it: id: pages, the step moved ahead of the build, and SITE_BASE taken from steps.pages.outputs.base_path.
Verified: the two site builds for /skills/ and /skills are byte-for-byte identical; base_path='' correctly moves assets to the domain root; zizmor goes from 5 high to no findings; actionlint clean; the six other open pull requests all merge and rebase clean on top of this one, with #2's and #4's action bumps intact; configure-pages v6 keeps the same outputs, so a later dependabot bump will not break the wiring.
One thing stays unproven until this lands: that pages: read suffices for the Pages GET. It is documented rather than measured, because the workflow only runs on push to main. Watching the first run after merge.
2 of 5 in a split stack.
mainmain#9 and #12 are independent and both go straight to
main— they fix the two unrelated jobsthat are red there, and neither waits on the other. #10, #11 and #13 sit on #9 only so their
Validate runs are green while
main's link check is failing; GitHub retargets them tomainwhen #9 lands.
mainis failing two independent jobs, so any PR fixing one still displays the other.Nothing here introduces the failure it shows:
zizmor— the Pages workflow from #7validate— the dead docs.vllm.ai linkEverything else is green on every PR. All five test-merge into
maincleanly in any order,and the merged combination passes the full gate plus zizmor and actionlint.
What this changes
zizmor fails
mainwith five high findings, all in the Pages workflow added in #7:Permissions.
pages: writeandid-token: writewere granted at the workflow level,which hands them to both jobs. Only
deployneeds them — andbuildis the job that shouldleast have them: it checks out a second branch and runs
npm ciandnpm run buildoverits lockfile, which is the one place in this workflow where third-party code executes. A
token that can publish the site or mint an OIDC identity does not belong in the job running
someone else's postinstall script. Workflow level is now
contents: read, with the twowrite scopes on
deploy.Pins. Every other action in this repository is pinned by commit SHA; these three arrived
as tags. Each is pinned to the SHA the tag it was already using resolved to, so nothing
about what runs changes:
actions/configure-pagesv5983d7736…(v5.0.0)actions/upload-pages-artifactv356afc609…(v3.0.1)actions/deploy-pagesv5368f8252…(v5.0.1)Newer majors exist — configure-pages v6.0.0 and upload-pages-artifact v5.0.0 — and are
deliberately left alone. Pinning and upgrading are different changes with different risk,
and dependabot will propose the upgrades with a CI run attached.
Kept as one PR rather than split into permissions and pins: it is one file, one regression
from one merge, and one zizmor run to re-check.
Verified
Same digest-pinned zizmor image
security.ymluses:where the identical command reported 5 high before. actionlint clean.
Checklist
descriptionagainst requests a user would really type — see CONTRIBUTING.md. (no skill text changed)python3 tools/validate_skills.pypasses locally.git commit -s(DCO).