Upstream can push workflows (in .github/ or .forgejo/) that execute on our runners via any trigger mechanism including cron. Runner label mismatch is the current defense but is fragile. No complete fix exists short of disabling Actions entirely. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
4.8 KiB
| title | modified | last-reviewed | tags | |||
|---|---|---|---|---|---|---|
| Spork Strategy | 2026-03-28 | 2026-03-28 |
|
Spork Strategy
Note: This article was drafted by AI and reviewed by Erich. I plan to rewrite all explanatory content in my own words - these serve as placeholders to establish the documentation structure.
A "spork" is a floating-branch soft-fork strategy for maintaining local changes against upstream projects without creating a true fork. The name: a fork that's trying its hardest not to be one.
The problem
We mirror upstream projects on forge for supply-chain control. Sometimes we need to carry local patches — workflow support, build tooling, bug fixes. A real fork diverges silently until merge day becomes a nightmare. A spork stays perpetually close to upstream with patches "floating" on top, rebased daily.
The trade-off
A spork chooses "small frequent pain" (constant rebasing, shifting branch targets) over "rare catastrophic pain" (fork divergence). For a solo operator carrying a handful of patches, this is the right trade-off. The key property: git log main..blumeops always shows your complete delta from upstream. No mystery divergence.
Long-lived work against a sporked repo must accept that there is no "safe" branch — everything is an ever-shifting target. Anyone with a local checkout needs to be comfortable with git pull --rebase.
Architecture
Three remotes, five branch types, one daily sync workflow. The blumeops branch is the default — it looks just like upstream with local workflows overlaid. Feature branches come in two flavors: upstreamable (branched off main, clean for contribution) and non-upstreamable (branched off blumeops, local-only). A deploy branch merges everything together as a build artifact.
Forgejo Actions checks .forgejo/workflows/ first; if that directory exists, .github/workflows/ is ignored. This protects the blumeops branch and feature/local/* branches (which inherit .forgejo/ from blumeops). However, main and feature/upstream/* branches do NOT have .forgejo/workflows/ — they're clean upstream code — so Forgejo falls back to .github/workflows/ on those branches. See spork-strategy#Spork Attack for the security implications.
Spork Attack
A "spork attack" is a supply-chain risk inherent to the spork strategy. Because main and feature/upstream/* branches carry upstream's .github/workflows/, those workflows are registered by Forgejo Actions. If an upstream project publishes a workflow targeting runner labels that match your infrastructure, it will execute on your runners.
Attack chain:
- Upstream pushes a workflow (in
.github/workflows/or.forgejo/workflows/) withruns-on: <your-runner-label> - Mirror auto-syncs, mirror-sync fast-forwards
mainon your fork - The workflow triggers — via push event, PR event, cron schedule, or any other trigger mechanism
- Workflow executes on your runner with access to
GITHUB_TOKENand the runner's environment
Note that a cron-triggered workflow is especially dangerous: it requires no user interaction at all. As soon as main is updated with the malicious workflow, Forgejo schedules it automatically.
Current mitigations:
- Runner label mismatch — our runner uses
k8s, upstream workflows typically useubuntu-24.04/macos-latest/windows-latest. Jobs queue but never execute. This is effective but fragile — it depends on upstream never guessing our label. - Trust boundary — we only spork projects we trust. Kingfisher is maintained by a MongoDB security engineer.
- Mirror review — mirror syncs are visible in Forgejo; malicious workflow changes would appear in the commit history. But this is not a real-time defense — the workflow may execute before anyone reviews.
What would fix this properly:
- A Forgejo per-repo setting to disable workflow discovery entirely on specific branches, or to require an explicit allow-list of workflow files. Neither exists today.
- Runner-level repo allow-lists could limit blast radius, but the workflow files still come from the sporked repo via upstream, so the runner would still execute them.
Recommendation: Use non-standard runner labels (not ubuntu-latest, linux, etc.) and only spork projects you trust. Document which projects are sporked and review upstream workflow changes periodically. Consider this an open problem — there is no complete defense short of disabling Actions on the repo entirely (which breaks mirror-sync).
How-to guides
- create-a-spork — initial setup with
mise run spork-create - manage-spork-branches — feature branches, the deploy branch, handling rebase conflicts
See also
- manage-forgejo-mirrors — how upstream mirrors work
- kingfisher — first project using the spork strategy