ci: harden checkout credential handling for INC-7027 - #6823
Conversation
There was a problem hiding this comment.
Pull request overview
This PR hardens two GitHub Actions workflows against token exfiltration by ensuring the Distro CI GitHub App token is not persisted in .git/config while unreviewed branch content (local actions, npm install, go mod tidy, make targets) is executed, and by moving the actual git push into a dedicated final step.
Changes:
- Set
actions/checkoutpersist-credentials: falsein both workflows to prevent persisting the App token in.git/config. - Configure
EndBug/add-and-committo not push (push: false) and add anidso itscommittedoutput can gate follow-up behavior. - Add a final
Push changesstep that injects the App token only for the push and only when a commit was created.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| .github/workflows/renovate-post-upgrade.yaml | Prevents checkout credential persistence and moves pushing to a final gated git push step (Renovate branch post-upgrade chores). |
| .github/workflows/chart-chores.yaml | Prevents checkout credential persistence and moves pushing to a final gated git push step (PR chart chores automation). |
| # SECURITY: never leave the GitHub App token in .git/config while code | ||
| # from the pull request branch is executed. Every step below runs | ||
| # content that comes from the checked-out PR: local composite actions | ||
| # (./.github/actions/*), `npm install -g`, and `make` targets from the | ||
| # checked-out Makefile. Anything in that tree can read the persisted | ||
| # credential out of .git/config and push to every repo this GitHub App | ||
| # can reach. The token is re-introduced only in the final push step | ||
| # below, after all of that code has already run. |
There was a problem hiding this comment.
Good catch, and verified against AGENTS.md line 11 ("NEVER write reasoning/'why'/narration comments — comments explain only non-obvious HOW ... tactical rationale ... goes in the PR body or commit message"). Fixed in 4252245.
The blocks are now one line each, limited to the non-obvious HOW — namely the coupling a future editor could otherwise miss: persist-credentials: false on the checkout and push: false on add-and-commit only work as a pair with the trailing Push changes step. Removing either one in isolation breaks the job. The full incident rationale lives in the PR body.
| # SECURITY: never leave the GitHub App token in .git/config while the | ||
| # code being upgraded is executed. Every step below runs content that | ||
| # comes from the Renovate branch under test: local composite actions | ||
| # (./.github/actions/*), `npm install -g`, `go mod tidy` over all | ||
| # go.mod files, and `make` targets from the checked-out Makefile. | ||
| # A malicious install/lifecycle script in a bumped dependency can read | ||
| # the persisted credential out of .git/config and push to every repo | ||
| # this GitHub App can reach. The token is re-introduced only in the | ||
| # final push step below, after all of that code has already run. |
There was a problem hiding this comment.
Good catch, and verified against AGENTS.md line 11 ("NEVER write reasoning/'why'/narration comments — comments explain only non-obvious HOW ... tactical rationale ... goes in the PR body or commit message"). Fixed in 4252245.
The blocks are now one line each, limited to the non-obvious HOW — namely the coupling a future editor could otherwise miss: persist-credentials: false on the checkout and push: false on add-and-commit only work as a pair with the trailing Push changes step. Removing either one in isolation breaks the job. The full incident rationale lives in the PR body.
|
I have concerns about both verification and the security boundary here:
Could you please clarify how you intend to validate the commit-and-push path and whether the privileged token generation/push can be moved to a separate trusted job on a fresh runner? |
c8b3f27 to
cfa745b
Compare
|
You're right on both counts, and the second one is the important one — thanks for pushing on it. On the security boundary. I over-claimed. Ordering the credential after the untrusted steps only closes passive extraction from On validation. You're also right that nothing has exercised the push. Both runs so far ended with a clean tree, so To answer your two questions directly:
That is a bigger change to your workflow than the one in front of you, so I'd rather not push it unannounced. Two options, your call:
I have no preference beyond wanting it reviewed by someone who owns this workflow. Given your point stands, I'd understand if you'd rather not merge the incremental version at all — it is strictly better than the current state but it is not the fix. |
9f821a8 to
e34280b
Compare
|
Handing the architectural fix to this team rather than attempting it myself. The change in this PR is an incremental one: it keeps the credential out of Four workflows on INC-7027 have been converted to that shape, and two are worth reading as references:
The shape is:
Two details that are easy to miss:
I did not attempt the conversion here because this workflow is materially more complex than the ones above and I would be guessing at behaviour I cannot exercise. Happy to draft it if you would rather review a proposal than write it — just say so. Your call whether this PR lands as the incremental step or waits for the split. If you would rather not merge the incremental version at all, that is a reasonable position and I would not argue against it. |
Add core.hooksPath=/dev/null to both credential-helper push steps so a pre-push hook planted by an earlier step in the same job cannot read GH_APP_TOKEN from the step environment. Set fetch:false on add-and-commit so its default "--tags --force" fetch does not run unauthenticated against a checkout that persists no credentials. Drop the unreferenced TARGET_REPO env var, and resolve TARGET_REF in renovate-post-upgrade from github.ref_name since that workflow only triggers on push to renovate/*. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
e34280b to
7ab9998
Compare
ct list-changed returns nothing on a PR that touches no chart, and the workflow passed that empty value through as chartPath. Because it is supplied as an environment variable rather than a make command-line override, Makefile's chartPath assignment still applies and its $(if ...) fallback expands to charts/camunda-platform-*, so every chart version was regenerated. On PR #6823 that produced a distro-ci commit rewriting 233 golden files across 8.3, 8.4, 8.5 and 8.6. Guard the three update steps on a non-empty CHANGED_CHARTS, matching the pattern already used in renovate-post-upgrade.yaml. The end-of-support exclusion from that workflow is deliberately not copied: regenerating an out-of-support chart is correct when someone actually edits it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6b65e05 to
d5ce416
Compare
Note on the 233 golden files that briefly appeared here, and the workflow bug behind themBetween What happened. chartPath="${CHANGED_CHARTS}" make go.update-golden-only
chartPath := $(if $(chartPath),$(chartPath),charts/camunda-platform-*)Because So all chart versions were regenerated, the four oldest produced a diff, The diff was not meaningful change. 479 insertions / 233 deletions, of which 246 are blank lines and 4 are whitespace-only lines. Files went from Fix applied in Confirmed working in production on run Two things worth separating out from this PR.
|
Workflow credential-handling hardening.
Tracked under INC-7027 — rationale, analysis and review notes are in the incident ticket and are intentionally not repeated here.
Not tested. YAML and step ordering were validated; the runtime path has not been exercised. Please confirm on a throwaway run before merging.
Questions to the incident channel or the ticket, please — not this thread.