blumeops/docs/explanation/spork-strategy.md
Erich Blume f9206bf10b
All checks were successful
Build Container / detect (push) Successful in 2s
Build Container / build-nix (kingfisher) (push) Successful in 12s
Build custom Kingfisher container from sporked deploy branch (#318)
## Summary

- Add Dockerfile for Kingfisher built from source (sporked deploy branch)
- Multi-stage: Rust build with Boost/vectorscan, debian-slim runtime
- Switch CronJob from upstream `ghcr.io/mongodb/kingfisher` to `registry.ops.eblu.me/blumeops/kingfisher`
- Add kingfisher to service-versions.yaml (version tracks upstream main SHA)
- Document spork workflow in CLAUDE.md

## Test plan

- [ ] Build container: `mise run container-build-and-release kingfisher 1d37d29`
- [ ] Verify image on registry: `mise run container-list`
- [ ] Update kustomization newTag
- [ ] Sync ArgoCD kingfisher app from branch
- [ ] Trigger manual CronJob and verify scan completes
- [ ] Verify reports on sifaka

Reviewed-on: #318
2026-03-30 06:34:49 -07:00

4.9 KiB

title modified last-reviewed tags
Spork Strategy 2026-03-28 2026-03-28
explanation
git
forgejo

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:

  1. Upstream pushes a workflow (in .github/workflows/ or .forgejo/workflows/) with runs-on: <your-runner-label>
  2. Mirror auto-syncs, mirror-sync fast-forwards main on your fork
  3. The workflow triggers — via push event, PR event, cron schedule, or any other trigger mechanism
  4. Workflow executes on your runner with access to GITHUB_TOKEN and 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 use ubuntu-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

See also