fix(selfhost): redact Orb broker enrollment tokens from telemetry - #3193
Conversation
) The Orb broker's enrollment id/secret (createOpaqueToken("orbenr"/"orbsec"), src/orb/broker.ts) are bare opaque tokens with no "token"/"secret"-named field to trip key-based redaction when they appear as a plain value, e.g. quoted inside an error message. The value-based token scrubbers in src/selfhost/sentry.ts (Sentry beforeSend), src/db/repositories.ts (product-usage telemetry), src/services/control-panel-roles.ts, src/services/miner-dashboard-recommendations.ts, and src/services/weekly-value-report.ts already recognized ghp_/github_pat_/ gts_/glpat-/sk- (and xoxb- in one case) but missed the Orb broker's own prefixes, so a broker error/log line quoting a raw enrollment secret would have reached Sentry or persisted telemetry unredacted.
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
|
Warning 🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨 ⏸️ Gittensory review result - manual review recommendedReview updated: 2026-07-04 19:33:34 UTC
⏸️ Suggested Action - Manual Review
Review summary Nits — 7 non-blocking
Review context
Contributor next steps
Signal definitions
🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by Gittensory, a quiet PR intelligence layer for OSS maintainers.
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #3193 +/- ##
=======================================
Coverage 94.73% 94.73%
=======================================
Files 270 270
Lines 29714 29714
Branches 10840 10840
=======================================
Hits 28151 28151
Misses 917 917
Partials 646 646
🚀 New features to boost your workflow:
|
…ke scenario (#3228) Completes the remaining #1825 deliverables that PR #3193's audit left open: a written stable-ingress-vs-pull-mode recommendation, a reproducible pull-mode smoke-test scenario alongside the existing push-mode one, and troubleshooting guidance for a failed relay registration in each mode. - GitHub App and Orb doc: new "Choosing a relay mode" section makes the existing implicit lean (pull mode for NAT/tailnet, no public ingress needed; push mode requires a real, SSRF-validated public origin) explicit, and expands the Brokered Orb env block to show ORB_RELAY_MODE. - Beta release checklist: the brokered-mode scenario now runs BOTH relay modes, since they exercise genuinely different code paths. - Troubleshooting: new guidance for selfhost_orb_relay_register_failed, covering what it means and what to check per relay mode.
Summary
Advances chore(orb): verify brokered self-host relay and telemetry #1825 (brokered self-host relay + telemetry audit). This PR is the code-level audit requested by the issue plus one genuine, narrow fix it turned up. It does not close chore(orb): verify brokered self-host relay and telemetry #1825 — see "What's not done" below for the parts of the issue this PR does not cover.
Fix: the Orb broker's enrollment id/secret (
createOpaqueToken("orbenr"/"orbsec"),src/orb/broker.ts) are bare opaque tokens with notoken/secret-named field to trip the existing key-based redaction when they appear as a plain value — e.g. quoted inside a broker error message. Five independent value-based token scrubbers across the codebase already recognizedghp_/github_pat_/gts_/glpat-/sk-(andxoxb-in one case) but missed the Orb broker's own prefixes:src/selfhost/sentry.ts(SECRET_VALUE, the SentrybeforeSendscrubber)src/db/repositories.ts(PRODUCT_USAGE_TOKEN_VALUE, persisted product-usage telemetry)src/services/control-panel-roles.ts(sanitizeRoleText)src/services/miner-dashboard-recommendations.ts(FORBIDDEN_TOKEN)src/services/weekly-value-report.ts(sanitizeReportText)All five now also match
orbenr_/orbsec_. Every fix is a one-line regex addition; each is covered by a new regression test that fails without the fix and passes with it (verified locally by reverting the five source lines and re-running the new tests — see Validation).End-to-end audit notes (#1825 deliverable)
Audited by reading the code end to end (no live broker access, so this is a rigorous static audit, not a live integration test — see "What still needs an operator to verify"). Production runs
ORB_RELAY_MODE=pull, so the pull path got the primary focus; push-mode code was read too since the issue asks for both.Token broker (mint / cache / expiry / refresh / outage) —
src/orb/broker-client.ts,src/github/app.ts,src/orb/broker.ts:fetchBrokeredInstallationToken(src/orb/broker-client.ts:53) exchangesORB_ENROLLMENT_SECRETfor a token viaPOST /v1/orb/token, 25s timeout, throws on non-OK or a tokenless body.mintInstallationToken(src/github/app.ts:223) writes the brokered token into the same installation-token cache the App-key path uses (installationTokenCache, ~1h TTL, 2‑min safety margin), single-flighted viainFlightMints(src/github/app.ts:130) so a cold cache doesn't thundering-herd the broker.expiresAtfalls back to a 50‑min default rather than propagatingNaNinto the cache (src/orb/broker-client.ts:75-79, with an explicit comment on why);withInstallationTokenRetry(src/github/app.ts:176) force-refreshes once on a 401/permission-scope rejection.mintInstallationTokenserves the still-valid cached token if one exists ("stale-token grace",src/github/app.ts:254-270, already tested intest/unit/github-app.test.ts:549and:572); only when there is no valid cache does it rethrow (→ the queue's existing retry/DLQ handling). There is no busy-retry loop inside the mint path itself — already correct.src/orb/broker.ts):brokerOrbTokencaches the minted token encrypted-at-rest, re-checks install eligibility on every exchange (not just at issue time), and orders the misconfiguration check after the enrollment-secret check so an unauthenticated caller can't fingerprint broker misconfiguration (#2710, already fixed).Webhook relay — registration, drain, ack, retry, stale-event handling —
src/orb/broker-client.ts,src/orb/relay.ts,src/selfhost/monitored-work.ts,src/server.ts:registerOrbRelayTargetWithRetry(src/orb/broker-client.ts:195) retries on a 1‑minute timer with a 5‑minute backoff (ORB_RELAY_REGISTER_RETRY_BACKOFF_MS) once registration fails, instead of the old one-shot boot-time attempt — already fixed for#selfhost-runtime-drift.registerOrbRelayWithMonitor(src/selfhost/monitored-work.ts:112) logs a warning, not an error, for a pull-mode registration failure, because the drain loop still functions once a later attempt succeeds; push-mode failure is an error (the container is genuinely deaf with no fallback). Already tested (test/unit/selfhost-monitored-work.test.ts:222and:243).drainOrbRelay(src/orb/broker-client.ts:222) POSTs the outstanding ack list and pulls the next batch every 15s (src/server.ts:1003); best-effort, returns[]on any failure so the next tick retries.enqueueWebhookByEnvdurably accepts the event (drainOrbRelayWithMonitor,src/selfhost/monitored-work.ts:89) — an enqueue failure is never acked, so it is redelivered on the next drain.retryFailedRelays(src/orb/relay.ts:377) runs off theretry-orb-relaycron, up to 5 attempts within a 1‑hour TTL, with a 5‑minute per-row backoff (RELAY_RETRY_BACKOFF_MINUTES) so a sustained outage doesn't re-hammer a down container or a degraded Orb every ~2‑minute tick.orb_relay_pending, 24h TTL,RELAY_PENDING_TTL_HOURS) and the push-mode failure queue (orb_relay_failures, 1h TTL / 5 attempts) prune and emit an alertableerror-level structured log on drop (orb_relay_pending_dropped/orb_relay_events_dropped) so silent event loss is visible — already tested intest/integration/orb-relay.test.ts:473and:790.Outcome telemetry export —
src/selfhost/orb-collector.ts:true(ORB_ANONYMIZE), repo/PR identifiers are HMAC'd with a dedicated per-instance secret (never the App key or webhook secret) generated once and persisted insystem_flags.ORB_AIR_GAP=trueshort-circuits the export to a no-op before any network call, including for brokered instances — already tested (test/unit/selfhost-orb-collector.test.ts:92,:97).gittensory_orb_export_errors_totaland returns 0 (no throw, no crash of the hourly export timer); the watermark cursor only advances on a confirmed 2xx, so a failed batch is retried whole on the next run — already tested (test/unit/selfhost-orb-collector.test.ts:184,:192).Logging/Sentry context (the one gap found and fixed):
SECRET_KEYinsrc/selfhost/sentry.ts, matchingtoken|secret|key|password|...) already catches any field literally namedsecret/token/etc. — confirmed still solid.Error.message, a log line) where a secret appears as a bare value with no matching field name. None of the currentthrow new Error(...)call sites insrc/orb/broker-client.tsinterpolate the raw enrollment secret directly, so this was not an active leak in the code paths I traced — it is a defense-in-depth gap: if a future broker error message (or an upstream broker's own error body, echoed viasafeOrbRelayRegisterErrorHint) ever quoted the secret bare, it would have reached Sentry/telemetry unredacted. Fixed by addingorbenr_|orbsec_to the five value-scrubber regexes listed above.enqueueWebhookByEnvonly recordsdeliveryId/eventName/action/installationId/repositoryFullName/payloadHash(src/db/repositories.tsrecordWebhookEventcall sites), never the body itself.hashedInstallationContext(src/selfhost/sentry.ts:231) hashesinstallation_idbefore it reaches Sentry tags/context;PAYLOAD_KEYdropsbody/payload/patch/diff/config/repoConfig-named fields entirely rather than truncating them.What's not done (honest scope boundary)
This PR does not attempt:
PUBLIC_API_ORIGIN), but a written operator-facing recommendation belongs in the docs PR above, not buried in a code-fix PR.What still needs an operator to verify live
Scope
type(scope): short summaryConventional Commit format, for examplefix(api): restore profile access checks.CONTRIBUTING.mdand does not reintroduce GitHub Pages, VitePress,site/, orCNAME.Validation
git diff --checknpm run actionlint(no workflow files touched; not run)npm run typechecknpm run test:coverage(not run in full locally per this repo's own guidance — CI already runs the full gate; ran the 5 directly affected test files plusnpm run test:changed, both green, and verified revert-and-confirm-fails for each new test)npm run test:workers(no Worker-runtime code touched; not run)npm run build:mcp(no MCP code touched; not run)npm run test:mcp-pack(no MCP code touched; not run)npm run ui:openapi:check(no API/schema changes; not run)npm run ui:lint/npm run ui:typecheck/npm run ui:build(noapps/gittensory-ui/**changes; not run)npm audit --audit-level=moderate(no dependency changes; not run — CI's dependency-review job covers this)git stashthe 5 source-line fixes, re-run the 5 new tests → all 5 fail cleanly with no other regressions; restore, re-run → all pass). Also rannpm run test:changedagainst the full diff: 216 test files / 5477 tests passed, 0 failed.If any required check was skipped, explain why:
src/**-only change with no UI, MCP, Worker-runtime, API/schema, or dependency surface touched, so the corresponding gate steps are inapplicable;typecheckplus the directly affected unit tests plustest:changed(full affected-test sweep) are the faithful local signal for a change this narrow, and CI runs the completetest:cigate includingtest:coverageregardless.Safety
orbsec_+ repeated filler characters — never real-looking secrets.)UI Evidencesection. (N/A — no visible UI changed.)Notes
src/orb/broker-client.tssrc/orb/broker.tssrc/orb/relay.tssrc/github/app.ts:132-331src/server.ts:939-1011src/selfhost/monitored-work.tssrc/selfhost/orb-collector.tssrc/selfhost/sentry.ts:37-47