Skip to content

feat(fleet)!: per-worker fleet keys (full cutover, v1.0.0)#101

Merged
GeiserX merged 10 commits into
mainfrom
feat/per-worker-keys
Jul 10, 2026
Merged

feat(fleet)!: per-worker fleet keys (full cutover, v1.0.0)#101
GeiserX merged 10 commits into
mainfrom
feat/per-worker-keys

Conversation

@GeiserX

@GeiserX GeiserX commented Jul 10, 2026

Copy link
Copy Markdown
Owner

Per-worker fleet keys — the full cutover (deferred item from the audit). Retires the single shared key as the day-to-day fleet credential: each worker gets its own key, and the shared CASHPILOT_API_KEY becomes an enrollment-only bootstrap. Breaking → v1.0.0 (the title's feat(fleet)!: drives the major bump via the updated release workflow). Do not merge without review — existing fleets must re-enroll.

What & why

Before, one shared key authenticated everything, in both directions. A leaked key = the whole fleet, and any worker could present another worker's identity. Now:

  • Enrollment: a worker's first heartbeat uses the shared key; the UI mints it a unique key (stored Fernet-encrypted on the UI, returned once), which the worker persists to its private /data/.worker_key.
  • After enrollment: the worker authenticates every heartbeat with its own key, and the UI calls that worker with the same key. Once the worker confirms the key (first own-key heartbeat), the shared key is rejected for it — in every direction.
  • Result: a leaked worker key is scoped to one worker, and no worker can impersonate another.

How it's built (by story)

  • US-001/002 (UI): workers.api_key_enc (encrypted) + key_confirmed; /api/workers/heartbeat enrolls → issues → confirms, and rejects the shared key for a confirmed worker.
  • US-003 (UI→worker): _get_verified_worker_url (the one outbound auth point for all command/deploy/logs) sends each worker's own key.
  • US-004 (worker): obtains, persists, and uses its key; requires it inbound once enrolled.
  • US-005 (release): the workflow now bumps major on a BREAKING CHANGE/type!: marker, minor on feat:, else patch — still fully automated, no manual tags.
  • US-006 (docs): v1.0.0 upgrade guide (automatic enrollment, upgrade steps, recovery/rollback), fleet/README/CLAUDE.md updated.

Robustness (architect review)

A thorough architect pass PASSED all 5 security criteria (enrolled workers reject the shared key everywhere; no empty-string bypass; adequate entropy; all proxy paths authenticated). It flagged one MEDIUM availability defect — a lost enrollment response could permanently lock a worker out — which this PR fixes: until a worker confirms its key, the shared key still works and the same key is re-delivered on each heartbeat, closing the lockout window without weakening the finalized cutover. Also addressed its low-severity items (empty-client_id → 400, set_worker_key rowcount guard, removed dead _verify_fleet_api_key, doc fixes).

Verification

ruff check + ruff format --check clean; full pytest 1099 passed (new tests cover the DB layer, enrollment/reissue/confirmed heartbeat states, outbound key selection, and the worker side). Release bump logic validated locally (feat(fleet)!: → v1.0.0).

BREAKING CHANGE: fleet authentication moves to per-worker keys. Existing fleets must upgrade both UI and worker images to v1.0.0; workers re-enroll automatically on their first heartbeat. See docs/upgrade-v1.md.

GeiserX added 8 commits July 10, 2026 19:42
US-001: workers.api_key_hash column (schema + idempotent ALTER migration),
hash_worker_key (sha256), set/get_worker_key_hash. Foundation for the
per-worker fleet-key cutover. Tests + full suite green; ruff clean.
US-002: /api/workers/heartbeat now enrolls each worker on first contact (shared
key -> mint+store its own key, return once) and thereafter REQUIRES that
per-worker key, rejecting the shared key for enrolled workers (impersonation
protection). Shared CASHPILOT_API_KEY remains the enrollment bootstrap. Tests +
full suite green (1086); ruff clean.
…tover)

The UI must also authenticate OUTBOUND to each worker for a full cutover, so it
needs the key itself, not a one-way hash. Store keys Fernet-encrypted at rest
(api_key_enc) via set_worker_key/get_worker_key; heartbeat verify + enrollment
now compare/issue the key directly. Prep for US-003 (UI->worker auth). Full
suite green (1086); ruff clean.
US-003: _get_verified_worker_url (the single outbound auth point for all
command/deploy/logs proxying) now sends the worker's own per-worker key when
enrolled, falling back to the shared bootstrap key only for unenrolled workers.
Tests + full suite green (1088); ruff clean.
US-004: the worker adopts the key the UI issues on enrollment (persisted to its
private /data/.worker_key), uses it for outbound heartbeats, and requires it for
inbound UI->worker calls — rejecting the shared key once enrolled (accepting it
only before enrollment so the UI can reach it to enroll). Tests + full suite
green (1096); ruff clean.
US-005: the auto-release now bumps MAJOR on a 'BREAKING CHANGE' footer or a
'type!:' subject in the commits since the last tag, MINOR on feat:, else PATCH —
still fully automated, no manual tags. Lets the per-worker-keys cutover release
as v1.0.0. Bump logic validated locally against sample messages.
US-006: rewrite the fleet Authentication section for the per-worker-key model,
add docs/upgrade-v1.md (what changed, automatic enrollment, upgrade steps,
recovery/rollback, v1.0.0 release notes) + nav entry, and update the
CASHPILOT_API_KEY description in README + CLAUDE.md to 'enrollment/bootstrap key'.
Addresses the architect review of the per-worker-key cutover:
- MEDIUM: at-most-once enrollment could permanently lock out a worker if the
  response was lost. Add a key_confirmed flag: until the worker proves receipt by
  authenticating with its own key, the shared key still works AND the same key is
  re-delivered — closing the lockout window without weakening the finalized cutover.
- Reject empty client_id (400); rowcount guard + warning on set_worker_key.
- Remove dead _verify_fleet_api_key; fix CLAUDE.md release-bump note; note the
  shared enrollment key stays sensitive.
Full suite green (1099); ruff check + format clean.
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@GeiserX, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 17 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 749602b2-2910-4d40-bf5c-18e6fc890455

📥 Commits

Reviewing files that changed from the base of the PR and between 8595a89 and f59d4da.

📒 Files selected for processing (15)
  • .github/workflows/release.yml
  • CLAUDE.md
  • README.md
  • app/database.py
  • app/main.py
  • app/worker_api.py
  • docs/AUTOPILOT-WORKLOG.md
  • docs/GOAL.md
  • docs/fleet.md
  • docs/upgrade-v1.md
  • mkdocs.yml
  • tests/test_database.py
  • tests/test_main_routes.py
  • tests/test_worker_keys.py
  • tests/test_workers.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/per-worker-keys

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

GeiserX added 2 commits July 10, 2026 20:26
…I UI env)

The UI test environment has no docker package (only the worker image does), so
importing app.worker_api errored at collection. Match the existing worker-test
pattern: try/except ImportError -> pytest.skip(allow_module_level=True).
@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.28571% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.63%. Comparing base (8595a89) to head (f59d4da).

Files with missing lines Patch % Lines
app/database.py 90.62% 3 Missing ⚠️
app/main.py 97.36% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #101      +/-   ##
==========================================
+ Coverage   92.61%   92.63%   +0.02%     
==========================================
  Files          31       31              
  Lines        3180     3243      +63     
==========================================
+ Hits         2945     3004      +59     
- Misses        235      239       +4     
Files with missing lines Coverage Δ
app/main.py 93.00% <97.36%> (+0.11%) ⬆️
app/database.py 87.58% <90.62%> (+0.22%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@GeiserX GeiserX merged commit e631e3a into main Jul 10, 2026
8 checks passed
@GeiserX GeiserX deleted the feat/per-worker-keys branch July 10, 2026 22:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant