fix(delivery): verify parent dispatch for reusable jobs #21
No reviewers
Labels
No labels
app-migration
bug
ci-cd
cluster
Documentation
duplicate
enhancement
gpu
invalid
networking
question
security
Storage
wontfix
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Homelab/workflows!21
Loading…
Reference in a new issue
No description provided.
Delete branch "kiyi/verify-parent-dispatch"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Actual run 27 was manually dispatched from main by gitchegumi, but its reusable build job reports workflow_call. The current guard rejects it before scanning or signing.
For workflow_call jobs, fetch the original run through the authenticated Forgejo API using forgejo.run_id. Require matching run ID, repository, source SHA, triggering actor, and branch, then require the original trigger_event to be workflow_dispatch. Keep the existing exact refs/heads branch, live branch-protection, and actor-allowlist checks. API failures or missing/mismatched metadata stop the job. Add RUN_ID to both PV provisioning guard environments, which use the same helper.
Verified against Forgejo v16.0.3 services/actions/context.go (inner jobs deliberately receive workflow_call; run_id is the global run ID), routers/api/v1/repo/action.go, modules/structs/action.go, and services/convert/action.go.
Validation: delivery fixtures and Forgejo contract tests; regression cases cover non-dispatch events, wrong run/repository/SHA/actor/branch, missing IDs, API failure, unprotected branches, and unapproved actors. Hosted checks must pass; actual authenticated job-token API access and a successful owner build remain live acceptance after release.
After review and merge, release v1.3.2 and update Actual’s matching workflow pins. Do not retag existing releases. Related: Homelab/actual-budget#1.
Blocking finding
A rerun bypasses the approved-actor boundary.
protected()verifiesforgejo.actoragainst the run API’strigger_user, but both identify the original run initiator. In deployed Forgejo 16.0.3,services/actions/context.gosetsactorfromActionRun.TriggerUser, leavestriggering_actorempty, and exposes the current job attempt separately asrun_attempt. Rerunning reuses the same ActionRun metadata, while the web route permits any repository Actions writer to rerun.That means an unapproved writer can rerun a prior owner-dispatched release or deployment. The run ID, repository, SHA, branch,
trigger_event, APItrigger_user, andforgejo.actorstill describe the approved owner, so every new check passes. I reproduced the missing boundary by invoking this exact head withRUN_ATTEMPT=2;protected()returned successfully.Export
${{ forgejo.run_attempt }}at everyprotected()call site and reject anything except the initial attempt (1), or validate the rerun initiator from another trustworthy source. Add a regression proving attempt 2 fails. A fresh owner dispatch remains available when retry is needed.Verification performed
7a019e07c65b7990311ae70f9895b2a40126740d.226resolves to Actual run 27 with matching repository, SHA, original actor, branch, andworkflow_dispatch; livemainbranch protection passes.v16.0.3(eccddb2d17c93b42b2c8995725e03e549ac9ec0c) confirms globaljob.RunID, inner-jobworkflow_call, originalTriggerUseractor semantics, per-job attempts, and writer-level rerun authorization.git diff --checkpassed; the worktree is clean.Addressed the rerun boundary: every delivery/provisioning guard call site now exports forgejo.run_attempt, and protected() rejects any value except the initial attempt 1 before making API calls. Retry requires a fresh owner dispatch. Added attempt 2/empty/zero rejection cases plus a contract check for attempt wiring at all guard and publishing/deployment call sites. All 12 delivery and nine contract tests pass locally. Refreshed the two existing mitigation fingerprints for the additional two-line shift, verifying the original hashes from their exact finding fields; controls and expiry are unchanged. Hosted checks are rerunning.
The rerun boundary is resolved. Every delivery, publishing, deployment, and PV-provisioning guard path now receives
forgejo.run_attempt, andprotected()rejects anything except initial attempt1before making an API call. Both reusableworkflow_calland directworkflow_dispatchattempt-2 probes fail closed. Deleting the production attempt guard causes the focused regression suite to fail.Verification at
4823a629f2e8f10ddf309b6213f47bf7d3e7e181: 53 unit tests, 16 scaffold cases, Kubernetes negative-integration fixtures, Python compilation, and diff checks pass; all four hosted contexts are green; the worktree is clean. Fresh security artifact 70 reportspassed: true, zero blockers, zero scanner errors, five findings, and five exact mitigations. The two refreshedpipeline/delivery.pyfingerprints match the live decision while retaining their prior controls and expiry. Live, fetched, local, and payload SHAs matched before submission.