Skip to content

fix: don't forward session cookies on intra-cluster syncs - #87

Open
VibeSWE wants to merge 6 commits into
mainfrom
fix/intracluster-no-cookie-forward
Open

VibeSWE wants to merge 6 commits into
mainfrom
fix/intracluster-no-cookie-forward

Conversation

@VibeSWE

@VibeSWE VibeSWE commented Nov 1, 2026 •

Copy link
Copy Markdown

Problem

Without P_CLUSTER_SECRET (never set in this fork), querier→ingestor syncs forwarded the caller's browser Cookie header whenever the request had no Authorization header. Sessions exist only on the node that issued them, so every user, role, and stream sync made from the UI was rejected by the ingestor with Your session has expired or is no longer valid. The querier only logs that error, so the UI showed success.

Two places did this:

  • create_intracluster_auth_headermap, used by the user and role syncs.
  • sync_streams_with_ingestors, which copied every caller header.

Reproduction (1 querier + 1 ingestor + MinIO, UI-style cookie login)

Step (via session cookie on the querier) Before After
A. Create user → ingest as that user on the ingestor 403 200
B. Remove a user's role → ingest on the ingestor 200 (revoked access still works) 403
C. Delete user → ingest on the ingestor 403 (user never synced) 403
D. Create role, grant it → ingest on the ingestor 403 200
E. Create stream → querier log sync rejected no error
Querier "failed to forward request" errors 5 0

For E, ingest still worked before the fix, because ingestors load unknown streams from storage. But stream updates made in the UI never reached live ingestors.

Fix

  • User and role syncs (src/utils/mod.rs): with no cluster secret, the caller's Authorization header is forwarded if present. Otherwise the request authenticates as the node with node_token. Cookies are never forwarded. Authenticating as the node grants nothing extra: the querier has already authorized the caller, and every mounted ingestor sync handler reconciles from storage and ignores the request body (Ingestors install the stored role on role sync #78, Ingestors install the stored user on user sync, so role revocations take effect #80).
  • Stream sync (src/handlers/http/cluster/mod.rs): sync_streams_with_ingestors drops Cookie. If no other credential remains (Authorization, X-API-KEY, or the cluster secret), it uses the ingestor's node token, but only for default-tenant streams. The node token is root in the default tenant, and the ingestor's stream handler trusts the request's headers and body, so a tenant's stream would land in the default tenant. A tenant's stream sync without a credential now fails and is logged, asking for P_CLUSTER_SECRET. A node token that can't be encoded fails only that node's sync.
  • Prism stats (src/storage/field_stats.rs): without a cluster secret, the dataset-stats path forwards only the caller's Authorization header and otherwise returns an error asking for P_CLUSTER_SECRET. Falling back to the querier's root token there would skip the querier's per-dataset checks for the caller. Prism mode is disabled at startup in this fork, so this path is unreachable today; it now fails closed if Prism is ever turned on.
  • Tests: unit tests cover the header selection in all three places. Main's test that asserted cookies are copied now asserts the opposite.

Known limitation

In multi-tenant mode without a cluster secret, the node-token fallback runs as root in the default tenant. Tenant-scoped syncs (user delete, role add/remove, password reset) then reconcile the same user ID in the default tenant, which is effectively a no-op. The tenant's change still doesn't reach ingestors, which is no worse than before, when the sync was rejected outright. This now applies to UI (cookie) callers too, not only callers with no credentials. Distributed multi-tenancy needs P_CLUSTER_SECRET anyway (see otel_generator.rs and traces.rs).

🤖 Generated with Claude Code

VibeSWE and others added 6 commits November 1, 2026 00:26
Without P_CLUSTER_SECRET, create_intracluster_auth_headermap forwarded
the caller's browser cookie to peers when the request had no
Authorization header. Sessions exist only on the node that issued them,
so ingestors rejected every user and role sync made from the UI ("Your
session has expired..."), and the querier only logged it. Users created
in the UI could not ingest, roles created in the UI were missing, and
roles removed in the UI stayed in effect on ingestors until restart.

Fall back to the node token instead. The querier has already authorized
the caller, and the ingestor sync handlers reconcile from storage, so
authenticating as the node grants nothing beyond what storage says.

The Prism field-stats path called the helper with an empty node token;
it now forwards only the caller's Authorization header and otherwise
lets send_query_request use the querier's token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep main's Result-returning build_intracluster_auth_headermap and drop
its cookie branch. Its cookie-copy test now asserts the cookie is
replaced by the node token, and the unused-generated-text test copies an
Authorization header instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sync_streams_with_ingestors copied every caller header, including the
session cookie, so a stream created or updated from the UI was rejected
by ingestors ("session ... no longer valid"). Drop the cookie and, when
no other credential (Authorization, X-API-KEY, cluster secret) is left,
authenticate with the ingestor's node token.

The Prism dataset-stats path no longer passes no auth to
send_query_request when there is no cluster secret and no Authorization
header. That fell back to the querier's root token, which skips the
per-dataset checks the querier runs for the caller. It now returns an
error asking for P_CLUSTER_SECRET instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The node token authenticates as root in the default tenant, so an
ingestor given a tenant's stream sync with it would create or update
the stream in the default tenant. Without a caller credential, a
tenant's stream sync now fails (and is logged) and asks for
P_CLUSTER_SECRET, as the other multi-tenant cluster paths do.

A node token that can't be encoded now fails only that node's sync as
a logged sync error. The stream already exists locally, so it is not
returned as a header-build error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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