Repository navigation
Conversation
a7297e7 to
6f50d36
Compare
jensenpat
left a comment
There was a problem hiding this comment.
Request changes: the release-triggered path is sound, but the new manual deployment path creates an untrusted-checkout secret-exfiltration and review-bypass primitive.
Scope checked
| File | Area checked | Verdict |
|---|---|---|
.github/workflows/docs.yml |
Manual ref selection, checkout ordering, npm execution, and Cloudflare credential scope | Blocking changes required |
docs/user/README.md |
Description of release/manual deployment | Consistent with the workflow |
Blockers
-
.github/workflows/docs.yml:239— Do not exposeCLOUDFLARE_API_TOKENat job scope.CF_TOKENis present for every step indeploy, includingactions/checkout,npm ci, andnpm run build. Because those npm commands execute scripts from the selected checkout, a poisonedpackage.jsonor dependency lifecycle script can read and exfiltrate the Cloudflare token before the pinned Wrangler action runs.Remove the job-level
CF_TOKENmapping and passCLOUDFLARE_API_TOKENonly to the pinned Wrangler steps. Since this repository already has the secret configured, a missing credential should fail the deployment rather than make it silently skip. If optional deployment must remain supported, gate it with a non-secret repository variable or a small credential-presence step whose environment is scoped to that step. -
.github/workflows/docs.yml:266— Restrict the manualrefinput before checkout.ref="${INPUT_REF:-$pdf_tag}"accepts any branch or tag. A repository writer can push an unreviewed branch, dispatch this workflow with that branch asref, and cause its npm scripts to run in a secret-bearing job. This bypasses CODEOWNERS review and converts repository write access into a reusable Cloudflare credential outside GitHub. If the token has account-scoped Pages Edit, compromise can reach other Pages projects in the same account and persists until the token is revoked.Accept only an empty input, the exact
mainbranch, or a published non-prereleasev*release tag; resolve the accepted value to an immutable commit before checkout. Reject every other branch/tag. A protecteddocs-productionGitHub environment with required review would add a second boundary around manual deployments.
Attack class
This is the standard untrusted checkout before secret-bearing build steps attack: attacker-controlled repository content runs while deployment credentials are in the process environment. It is both CI secret exfiltration and a review-boundary bypass. Masking prevents accidental log display; it does not stop code from sending the secret elsewhere.
I verified these findings against head 6f50d36c129a9ba4f5b33ac82ed0bda695d46ff6. The two Cloudflare secrets already exist as repository Actions secrets, and no protected docs deployment environment currently gates this job.
…le VIII. Every push to main that touched docs/user/** rebuilt and redeployed docs.aethersdr.com. Since 2026-10-17 every one of those deploys has failed: the PDF manual (25.3 MiB) is over Cloudflare Pages' 25 MiB per-file limit. - Drop the push-to-main trigger. Pull requests still run the link check, the generated-reference check and the PDF render check. - Deploy the site when an app release (v*, not a pre-release) is published, from its tag, after release-pdf has attached the signed PDF. - Stop uploading the PDF to Pages. A _redirects entry sends /AetherSDR-Manual.pdf (the URL the app and the site link to) to the signed PDF on the release. - workflow_dispatch deploys the latest app release, or the tag or branch given as `ref`. - release-pdf only runs for v* tags. It had been attaching the manual to flex-tailnet-shim and test pre-releases. Fixes aethersdr#6556. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6f50d36 to
5b10460
Compare
…. Principle XII. Addresses the review on aethersdr#6558, and restores what the rebase onto aethersdr#6571 dropped from the deploy job. - resolve-target (read-only token, no checkout) accepts only an empty ref, main, or a published non-pre-release v* release tag, and pins it to a commit SHA. Any other branch or tag is refused. - build-site builds that SHA with a read-only token, no secrets and npm ci --ignore-scripts, writes the PDF redirect, checks the 25 MiB limit and uploads the site. - deploy runs in the cloudflare-pages environment with no permissions and no checkout, and passes the token only to the pinned wrangler-action, with wrangler pinned to 4.147.0 again. A missing token or account ID fails the job instead of skipping it. - A release deploys after sign-release-pdf, the job that now attaches the PDF, not after release-pdf. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@jensenpat thanks. Both blockers are fixed in e7bb8d6, and so is a regression I found while rebasing. What the rebase did. This branch was rebased onto #6571, which merged 10 minutes before the push. Resolving the conflict kept #6571's The deploy is now three jobs, following #6571. No job both runs repository code and holds the token.
Blocker 1 (token at job scope). Blocker 2 (unrestricted I ran the resolve step locally against this repository's real releases:
One environment setting is needed before this can deploy on a release. @ten9876, the 73, 👨🏼💻 Co-authored by Claude Opus 5.5 |
Summary
Fixes #6556.
Every push to
mainthat toucheddocs/user/**rebuilt and redeployed docs.aethersdr.com. Since 2026-10-17 every one of those deploys has failed, because the PDF manual (25.3 MiB) is over Cloudflare Pages' 25 MiB per-file limit. As agreed in Discord, this PR makes publishing the docs a release-time job:No deploy on merge. The
push: maintrigger is gone. The pull-request job is unchanged apart from where the PDF is written: link check, generated-reference check, Log Analyzer rules, PDF render check and PDF artifact.Deploy on release. When an app release (
v*tag, not a pre-release) is published, the site is built from the tag and deployed. This runs aftersign-release-pdf, so the signed PDF is already on the release before the site links to it.The PDF is no longer uploaded to Pages. The deploy writes a
_redirectsentry that sends/AetherSDR-Manual.pdftoAetherSDR-Manual-<tag>.pdfon the release. The app (DocsLinks::manualPdf()) and the site's navbar, footer and pages all link to that URL, so none of them change. The deploy fails early if the release has no such asset, and fails if any file in the site build is over 25 MiB.Manual redeploy.
workflow_dispatchdeploys the latest app release by default. Itsrefinput takesmain, so a maintainer can publish a docs-only fix between releases, or a published app release tag. Any other branch or tag is refused. The PDF link always points at the latest app release.Credentials kept apart from repository code (ci: run signing, Store and Pages deploys in environment-gated jobs. Principle XII. #6571's pattern). The deploy is three jobs:
resolve-target: read-only token, no checkout. Validatesrefand pins it to a commit SHA.build-site: read-only token, no secrets,npm ci --ignore-scripts. Builds that SHA.deploy: environmentcloudflare-pages,permissions: {}, no checkout. Runs only the pinnedwrangler-action(wrangler 4.147.0) on the built artifact.A missing Cloudflare token or account ID fails the deploy instead of skipping it.
docs/user/README.mdis updated to describe the new deploy.Repository setting needed (see #6556): the
cloudflare-pagesenvironment allows only themainbranch. A release-triggered deploy runs onrefs/tags/v…, so the environment needs atag v*rule, asreleasehas, before a release can deploy. Manual runs from main work today.Open for a maintainer decision (see #6556): between releases, Next (
/next/) showsmainas of the last deploy rather than following it. If Next should stay live, a nightly scheduled deploy can be added. Push changes to this branch or force-push it if you want a different approach.Constitution principle honored
Principle VIII: the published docs describe the software users can install, and the PDF users download is the signed release asset. Principle XII: the job that holds the Cloudflare token runs no repository code, and a manual deploy can publish only reviewed code.
Test plan
actionlint1.7.7 is clean ondocs.yml.resolve-targetscript locally against the real releases inaethersdr/AetherSDR:refdeploysv26.10.3.maindeploys main with the PDF from v26.10.3.v26.10.2deploys that tag. A release event forv26.10.3deploys it.lynovation-test-2026-10-16), a release that isn't the app (flex-tailnet-shim-v0.4.1), a branch, a tag with no release (v26.5.2),v99.0.0,refs/heads/main,main; curl evilandv26.10.2/../main.v26.9.4).https://github.com/aethersdr/AetherSDR/releases/download/v26.10.3/AetherSDR-Manual-v26.10.3.pdfresolves (HTTP 302 to the asset).refempty should deploy v26.10.3 and fix the live site. I can't run it from a fork (no secrets, no releases). After that run,https://docs.aethersdr.com/AetherSDR-Manual.pdfshould return a 302 to the release asset.No app code changed, so there is no local CMake build or radio test.
Checklist
docs/COMMIT-SIGNING.md)AppSettingscalls (n/a)docs/user/README.md)73,
Ryan NF0T
👨🏼💻 Co-authored by Claude Opus 5.5
🤖 Generated with Claude Code