You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* fix(orb): mint a dedicated federated signing key, separate from the anon secret (#9147)
Enrolling a federated peer previously handed them this instance's dedicated
anonymization secret (the SAME key that HMACs repo/pr identifiers on the
always-on #1255 orb export), since buildFederatedBundle signed with
getOrCreateAnonSecret. A peer holding that key could dictionary-invert any
repo_hash it observed and forge this instance's x-orb-signature.
- Add getOrCreateFederatedSigningSecret: a separate, dedicated system_flags
secret used only to sign federated bundles.
- Decouple instanceId from the anon secret via getOrCreateInstanceIdentitySecret,
a third independent secret seeded from the current anon secret on first use
(so an upgrading instance keeps its existing instanceId) but stored in its
own row from then on, so rotating the anon secret no longer resets the
export watermark or the federated peer-median's per-instance accounting.
- Correct anonymize.ts's key-separation docstring, which was false for any
federated-enabled instance.
* fix(orb): default-engage the federated rate limiter and drop stale ingest comments (#9166)
The documented per-collector rate limit (6/min) was inert at every real call
site: rateLimitAllows treated a caller-omitted bucket as "unlimited" instead
of falling back to a real bucket, and the only production caller
(buildFederatedBenchmark) never supplied one. evaluateLocalRateLimit is also
pure (read-only), so nothing was ever advancing a bucket's count either.
- Add module-level default push/pull buckets in federated-collector.ts;
rateLimitAllows now always consults (and advances) a real bucket, whether
caller-supplied or the module default.
- Bound pullPeerBundles' response read with the same reader /v1/orb/ingest
uses (generalized readOrbIngestBody to accept any body-bearing source, so
it works for both a Request and a collector Response), and cap the parsed
array before any per-element shape check.
- Remove two stale "FAIL-OPEN by default" comment blocks in routes.ts left
over from #9046, which made isAuthorizedIngest's ingest gate (now
fail-closed) read backwards from what the code actually does.
pushFederatedBundle wiring into a real tick lands with the #9148 commit,
which introduces the background refresh job this naturally slots into.
* fix(orb): scope pull-mode relay drain to the winning enrollment (#9150)
orb_relay_pending was scoped only by installation_id, so any enrollment
secret valid for an installation could drain and destructively-ack another
live enrollment's queued webhooks -- a stale re-enrolled container silently
stealing a fresh one's events (blue/green swap, secret rotation, or a
rebuilt container issued a fresh secret while the old one is still inside
its drain timer).
- Add orb_relay_pending.enroll_id, tagged at enqueue time with the SAME
winning enrollment forwardOrbEvent already elects for the push path
(#1783's deterministic ORDER BY). NULL for config_push rows (installation-
wide, not consumer-specific) and for any pre-#9150 row, so an untagged
row is never orphaned -- pullRelayPending's WHERE matches enroll_id = ?
OR enroll_id IS NULL.
- forwardOrbEvent now fetches every live enrollment row for an installation
(not just the elected winner) so a second live enrollment is observable
rather than silent, and increments a new counter,
loopover_orb_relay_multiple_live_enrollments_total, whenever more than
one exists.
- pullRelayPending/enqueueRelayPending both accept an optional enrollId to
scope the SELECT and the ack-DELETE together, closing the hole where one
container could ack another's rows.
* fix(orb): harden the federated peer-median trust boundary (#9148)
One allowlisted key could mint unlimited Sybil peers and own the median
outright: verifyFederatedBundle only checked whether SOME allowlisted key
signed a bundle, never bound a key to an instanceId, and importPeerBundles
had no per-instance dedup, no range checks, no freshness/replay protection,
and pulled peer bundles synchronously on every maintainer dashboard load.
- Range-validate every numeric field (rates in [0,1], non-negative counts,
cycleP50Ms <= cycleP95Ms) as its own "out_of_range" rejection, distinct
from a wrong-typed "malformed" field.
- Reject a windowDays that doesn't match the local instance's own resolved
window, and a generatedAt outside a bounded freshness window (7 days) or
more than 5 minutes in the future.
- Enforce decided >= MIN_DECIDED receiver-side rather than trusting the
sender's own eligibility claim.
- Dedup a batch to the LAST bundle per instanceId (importPeerBundles), then
add a persisted, cross-tick per-instance replay/rollback watermark and a
per-key Sybil cap (MAX_INSTANCES_PER_KEY) via the new
applyFederatedPeerWatermarks, backed by a system_flags JSON blob rather
than a new table -- a self-hosted federation's peer count is expected to
stay small enough that an in-memory scan/cap-check is trivially cheap.
- Move the peer pull off the dashboard's request path entirely: a new
"federated-peer-sync" queue job (10-minute cadence, gated on the loopover
self-repo's federatedIntelligence.enabled) now runs the pull + trust-gate
+ persist pipeline AND pushes this instance's own bundle (wiring
pushFederatedBundle into a real tick, closing the other half of #9166).
buildFederatedBenchmark still computes the local half live (a fast local
DB query) but reads the peer half from a cache the tick refreshes.
- peerCount now naturally counts distinct contributing INSTANCES (a side
effect of the per-instance dedup), matching what its own doc comment
always claimed but the old code never enforced.
* fix(orb): revoke-on-rotate and a self-hoster-reachable revoke path for broker enrollments (#9149)
issueOrbEnrollment was a bare INSERT: it never revoked, updated, or even
counted sibling rows for the same installation_id, so re-running the
install flow after a secret leak minted a SECOND simultaneously-valid
secret and left the leaked one working forever. The only revoke path sat
behind INTERNAL_JOB_TOKEN, unreachable by a self-hosting maintainer.
- issueOrbEnrollment takes an optional { rotate: true }: when set, every
prior live enrollment for the installation is revoked before the new one
is minted. Defaults to false (append) so a blue/green container swap can
still rely on two live enrollments briefly overlapping (#9150's sibling
fix is what makes that overlap safe, not this one).
- The OAuth landing page's callback now recognizes state=<installationId>:
rotate / :revoke (installation_id has no channel back through a bare
GitHub login/oauth/authorize bounce other than state) -- re-proving
admin-of-installation via the SAME verifyInstallationAdmin gate the
enrollment flow already uses, so a maintainer can rotate or revoke
without operator involvement. Revoke deliberately skips the
suspended/self-enrollment-disabled checks: locking down a leaked secret
should never be blocked by unrelated administrative state.
- The secret page now surfaces how many enrollments are live for the
installation and links to the rotate/revoke actions; its response sets
Cache-Control: no-store (it was previously a cacheable GET response
rendering a plaintext secret).
- The internal installations list and enrollment-issue routes gained the
same live-enrollment-count visibility and an optional rotate flag, for
operator-issued parity with the OAuth self-service path.
* chore: regenerate cf-typegen/selfhost env-reference after rebase
`SELECT installation_id AS installationId, account_login AS accountLogin, account_type AS accountType,
4582
4593
repository_selection AS repositorySelection, registered, suspended_at AS suspendedAt,
4583
-
removed_at AS removedAt, first_seen_at AS firstSeenAt, last_event_at AS lastEventAt
4594
+
removed_at AS removedAt, first_seen_at AS firstSeenAt, last_event_at AS lastEventAt,
4595
+
(SELECT COUNT(*) FROM orb_enrollments oe WHERE oe.installation_id = orb_github_installations.installation_id AND oe.state = 'enrolled' AND oe.revoked_at IS NULL) AS liveEnrollmentCount
4584
4596
FROM orb_github_installations ORDER BY last_event_at DESC`,
constinstall=awaitenv.DB.prepare("SELECT registered FROM orb_github_installations WHERE installation_id = ?").bind(installationId).first<{registered: number}>();
constresult=awaitenv.DB.prepare("UPDATE orb_enrollments SET revoked_at = CURRENT_TIMESTAMP, state = 'revoked' WHERE installation_id = ? AND state = 'enrolled' AND revoked_at IS NULL")
96
+
.bind(installationId)
97
+
.run();
98
+
returnresult.meta.changes;
99
+
}
100
+
101
+
/** How many enrollments are currently live ('enrolled', not revoked) for an installation (#9149) — the count
102
+
* surfaced to an operator (the internal installations list) and to a maintainer (the OAuth secret page) so
103
+
* neither has to guess whether a re-enrollment minted an extra standing credential. */
constrow=awaitenv.DB.prepare("SELECT COUNT(*) AS c FROM orb_enrollments WHERE installation_id = ? AND state = 'enrolled' AND revoked_at IS NULL").bind(installationId).first<{c: number}>();
0 commit comments