Skip to content

fix(rpc): Keep request signatures out of error reports - #169

Open
vaind wants to merge 1 commit into
mainfrom
ivan/rpc-signature-secret
Open

vaind wants to merge 1 commit into
mainfrom
ivan/rpc-signature-secret

Conversation

@vaind

@vaind vaind commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

When the RPC server reports an error, the report can carry the request's signature (Authorization: rpcsignature rpc0:...). Sentry's SDK sends the local variables of every frame in the traceback, Sentry's SDK configuration turns off the scrubber, and RpcServer holds the signature as a plain string, in headers and in authorization. RpcServer.post reports every coded error, including a provider failure such as GitHub being down, so each of those reports has the signature right next to data, the body it signs. #162 and #167 did the same masking for the signing secrets, the git grant and the provider token.

Why it matters

A signature has no timestamp or nonce. The POST signature covers only the body, not X-Organization-Id, X-Repository-Id or X-Credentials-Set. So anyone who reads a body and its signature in an error report can replay that request whenever they like, with other values in those headers. Changing the protocol would touch Sentry and Seer, so it's a separate decision and not part of this PR.

Approach

  • Server: RpcServer.get and RpcServer.post take Authorization out of headers as soon as they're called, the way GitServer.handle does with the git grant. They keep it as an scm.Secret, which shows as [Filtered]. verify_get and verify_post accept a Secret and reveal it only inline, inside the comparison. They still accept a plain string, so this doesn't break callers.
  • Client: RpcApiClient.request keeps its signature as a Secret for the whole retry loop and reveals it only inside the call to the session. When retries on connection errors run out, the ResourceServiceUnavailable it raises no longer chains the connection error (from None, as GitRelay does). The detail message still names that error. A chained exception is reported with its own frames, and the session's and requests' frames hold the header.

Two behavior changes:

  • The server now accepts an authorization header in any case. Before, a lowercase one got a 400. Django's request.headers already gives Authorization, so Sentry's endpoint sees no change.
  • ResourceServiceUnavailable.__cause__ is now None.

What this doesn't cover

  • A raw requests error, which reaches the caller when connection retries are off or for a write, still has the header in the session's and requests' frames. RpcApiClient's docstring says so. The same goes for fetch_repository and RequestsSession.get. Masking RequestsSession's own frames would mean changing the Session protocol, which is a breaking change, and it wouldn't help: the frame right below, in requests.api.request, holds the same headers.
  • Frames inside requests and urllib3 can't be masked.

Tests

  • TestKeptOutOfErrorReports in test_rpc_server.py: a provider failure that post reports, with Authorization and with authorization, and a raw failure from get.
  • TestKeptOutOfErrorReports in test_rpc_client.py: retries exhausted on a connection error.

Each test failed before this change. reported_local in tests/unit/error_reports.py is the same as in #167, so whichever PR lands second merges without a conflict.

RpcServer held a request's signature as a plain string, in `headers`
and in `authorization`, and RpcServer.post reports every coded error,
provider failures included. Sentry's SDK sends every frame's local
variables, and Sentry's SDK configuration turns off the scrubber, so
each such report carried the signature next to the body it signs. A
signature has no timestamp or nonce, so that pair can be replayed.

The server now takes Authorization out of the headers on entry, in
any case, and holds it as an scm.Secret. verify_get and verify_post
take a Secret and still accept a plain string.

RpcApiClient.request holds its signature as a Secret and reveals it
only in the call to the session. ResourceServiceUnavailable no longer
chains the connection error, whose frames in the session and in
requests hold the header. A raw requests error still carries it in
those frames, which this library can't mask.
@vaind
vaind requested a review from a team as a code owner October 27, 2026 23:04
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