Skip to content

[Security] Fix PKI session invalidation on HTTP/2 stream destruction - #285153

Merged
jeramysoucy merged 11 commits into
elastic:mainfrom
jeramysoucy:http2-pki-socket-investigation
Aug 24, 2026
Merged

[Security] Fix PKI session invalidation on HTTP/2 stream destruction#285153
jeramysoucy merged 11 commits into
elastic:mainfrom
jeramysoucy:http2-pki-socket-investigation

Conversation

@jeramysoucy

@jeramysoucy jeramysoucy commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #258232.

PKI sessions were being unexpectedly invalidated on Kibana 9.x when server.protocol: http2 was active (the new default when ssl.enabled: true). Setting server.protocol: http1 was the customer workaround.

Root cause

When an HTTP/2 stream is destroyed mid-request — by an AbortController cancel, browser navigation abort, or any other cause that sends RST_STREAM — Node.js clears the stream's session reference (stream.session = undefined). The Http2ServerRequest.socket proxy then falls back from the TLS socket to the stream itself, causing every instanceof TLSSocket check in KibanaSocket to return false. The result:

  • socket.authorizedundefined (not true or false)
  • socket.getPeerCertificate()null

The guard in authenticateViaState (pki.ts line ~208) was written for exactly this "connection closed" case:

// Before (buggy):
if (peerCertificate === null && request.socket.authorized) {
  // return non-401 soft fail — session preserved
}

On HTTP/1.1 a closed TLSSocket keeps authorized === true, so the guard fired correctly. On HTTP/2, authorized === undefined is falsy, so the guard was silently skipped. The code then fell through to the token-invalidation block, revoked the ES access token, and returned notHandled() — which authenticate() converted to Boom.unauthorized(). updateSessionValue() saw the 401 on an owned session and deleted the server-side session, logging the user out.

Fix

Change the guard from a truthy check to !== false:

// After (fixed):
if (peerCertificate === null && request.socket.authorized !== false) {
  // return non-401 soft fail — session preserved
}

false = cert was presented but rejected → token invalidation is correct.
undefined = socket state is unknowable (HTTP/2 stream destroyed) → treat same as true, preserve session.
true = socket explicitly authorized → original behaviour unchanged.

Also improves the diagnostic log in authenticateViaPeerCertificate to distinguish false (rejected cert) from undefined (unknown state), making production logs actionable.

Why this is new in 9.x

http_config.ts defaults server.protocol to http2 when ssl.enabled: true — a silent breaking change from 8.x. HTTP/1.1 is unaffected because each request has a dedicated TLSSocket that keeps authorized === true as a plain property after the connection closes.

Test plan

  • New regression test in pki.test.ts: does not invalidate token or destroy session when socket state is unknown due to HTTP/2 stream destruction — fails on main, passes after fix
  • New test in pki.test.ts: does not handle requests when socket authorization state is unknown due to HTTP/2 stream destruction — covers login and session-less authenticate, verifies correct log message
  • All 40 existing PKI unit tests continue to pass

Run tests:

node scripts/jest --config x-pack/platform/plugins/shared/security/jest.config.js --testPathPattern="authentication/providers/pki" --no-coverage

🤖 Generated with Claude Code


FTR stress test (regression coverage for RST_STREAM)

pki.test.ts unit tests are the authoritative regression lock — they're deterministic and always catch a revert. An additional FTR test is included to provide end-to-end coverage of the exact failure mode:

x-pack/platform/test/security_api_integration/tests/pki/pki_http2_stress.ts
Registered in pki.http2.stress.config.ts and added to ftr_platform_stateful_configs.yml.

Why supertest can't cover this

The existing pki_auth.ts concurrent-request test (5 streams) uses supertest, which speaks HTTP/1.1. RST_STREAM is an HTTP/2-only signal — supertest has no way to emit it. That is why the bug wasn't caught by the existing concurrent-auth test.

What the FTR test does

  1. Establishes a PKI session via supertest (HTTP/1.1 — simple, reliable).
  2. Opens a raw http2.connect() connection to Kibana with the same PKI client certificate.
  3. Fires 20 concurrent slow requests against the test endpoints plugin (10s hold, giving Kibana time to enter the async session-lookup window).
  4. After 150ms, cancels all 20 streams with RST_STREAM NGHTTP2_CANCEL.
  5. Sends a normal (non-cancelled) GET /internal/security/me on the same HTTP/2 session.
  6. Asserts 200 + username === 'first_client' — the session was NOT invalidated.

Before the fix: at least one RST_STREAM would destroy the stream during the auth lifecycle, causing authorized → undefined, which bypassed the guard, invalidated the ES token, and returned 401.
After the fix: all RST_STREAM frames are handled gracefully; the session survives.

Stability note

After the fix is applied, the test is unconditionally stable — the guard change means RST_STREAM never reaches the token-invalidation path regardless of timing. The test cannot produce a false positive (failing when the fix is present). Before the fix it might occasionally miss the narrow timing window (false negative), but this is acceptable for a regression test: it cannot regress silently if the fix is reverted, because the unit test in pki.test.ts will always catch it.

Release note

Fixes an issue where PKI authentication sessions were unexpectedly invalidated when in-flight requests were cancelled over HTTP/2, logging users out. This affects deployments on server.protocol: http2, which became the default in 9.0.0 when TLS is enabled.

When an HTTP/2 stream is destroyed mid-request (e.g. from an
AbortController cancel / browser navigation abort, which emits
RST_STREAM), Node's Http2ServerRequest.socket returns a Proxy whose
getPrototypeOf trap falls back to the Http2Stream prototype instead of
TLSSocket. This causes every instanceof TLSSocket check in KibanaSocket
to return false, making socket.authorized === undefined and
getPeerCertificate() === null.

The guard in authenticateViaState was written to handle "connection
already closed" (peerCertificate === null && authorized), but the truthy
check passed on HTTP/1.1 (where a closed TLSSocket keeps authorized ===
true) and silently failed on HTTP/2 (where authorized === undefined is
falsy). The code then entered the token-invalidation block, revoked the
ES access token, and returned notHandled() — which authenticate()
converted to Boom.unauthorized(), causing updateSessionValue() to delete
the server-side session and log out the user.

The fix distinguishes "definitively unauthorized" (false) from
"unknown socket state" (undefined) by changing the guard from
  peerCertificate === null && authorized
to
  peerCertificate === null && authorized !== false

Only authorized === false (cert presented but rejected) should trigger
token invalidation. undefined (socket state unknowable) should fall
through to the same non-401 soft-fail path as authorized === true with
no cert, preserving the session.

Also improves the diagnostic log in authenticateViaPeerCertificate to
distinguish the two cases, making production logs actionable.

Adds two regression tests:
- notHandled() + correct log when socket.authorized is undefined (HTTP/2
  degraded socket, no session)
- token NOT invalidated and session preserved when socket.authorized is
  undefined and session state exists (the primary regression lock)

Both tests fail on main and pass after this fix.

Fixes: elastic#258232

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
@jeramysoucy
jeramysoucy requested a review from a team as a code owner August 14, 2026 12:23
@jeramysoucy
jeramysoucy requested a review from legrego August 14, 2026 12:23
@jeramysoucy jeramysoucy mentioned this pull request Aug 14, 2026
3 tasks
@jeramysoucy jeramysoucy added release_note:fix Team:Security Platform Security: Auth, Users, Roles, Spaces, Audit Logging, etc t// backport:all-open Backport to all branches that could still receive a release labels Aug 14, 2026
@infra-vault-gh-plugin-prod

Copy link
Copy Markdown

Pinging @elastic/kibana-security (Team:Security)

jeramysoucy and others added 2 commits August 14, 2026 14:36
Document that when an HTTP/2 stream is destroyed, KibanaSocket returns
authorized === undefined (not false) and getPeerCertificate === null.
A plain net.Socket is used as a test double because it produces the
same instanceof TLSSocket === false result as the destroyed-stream proxy.

The key invariant: undefined means "socket state is unknown" (stream
destroyed before it could be read), not "cert was rejected" (false).
Code that reads authorized must treat these two values differently.
See kibana#258232 and the fix in pki.ts.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Adds a functional test that reproduces the session-invalidation bug using
real RST_STREAM frames — something supertest (HTTP/1.1) cannot do.

The test:
1. Establishes a PKI session via supertest / HTTP/1.1
2. Opens a raw http2.connect() session with the PKI client certificate
3. Fires 20 concurrent slow requests (/authentication/slow/me, 10s hold)
   to ensure Kibana's auth lifecycle is in flight when RST_STREAM arrives
4. Cancels all of them with NGHTTP2_CANCEL after 150ms
5. Sends a normal verification request on the same HTTP/2 session
6. Asserts the session is still valid (200, username=first_client)

Before the pki.ts fix: at least one RST_STREAM would cause authorized ===
undefined to reach the token-invalidation block, revoke the ES access
token, and return 401 on the verification request.

After the fix: the guard correctly treats undefined as indeterminate and
the session survives all RST_STREAM frames.

Also adds pki.http2.stress.config.ts (extends pki.http2.config.ts, adds
debug security logging) and registers it in ftr_platform_stateful_configs.yml.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
@jeramysoucy
jeramysoucy requested a review from a team as a code owner August 14, 2026 12:50
@legrego
legrego requested a review from a team as a code owner August 14, 2026 15:02
@kibanamachine kibanamachine added the reviewer:scout Agentic PR Scout test review label Aug 14, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the PKI HTTP/2 session-invalidation fix. The core change in pki.ts (authorized truthy check → !== false) is correct: KibanaSocket.authorized returns undefined for a destroyed HTTP/2 stream (non-TLSSocket) vs false for a rejected cert, so treating undefined like true correctly preserves the session in the indeterminate case without ever granting access. Unit tests deterministically cover both the token-invalidation and notHandled paths. One non-blocking comment on the shared kbn-scout config coupling.

Generated by Claude Reviewer for #285153 · 142.1 AIC · ⌖ 15.8 AIC · ⊞ 4.6K

addOrReplaceArg(kbnServerArgs, 'elasticsearch.hosts', 'https://localhost:9220');
addOrReplaceArg(kbnServerArgs, 'elasticsearch.ssl.certificateAuthorities', CA_CERT_PATH);
addOrReplaceArg(kbnServerArgs, 'server.ssl.clientAuthentication', 'optional');
kbnServerArgs.push(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wires a solution-specific x-pack test plugin (x-pack/platform/test/security_functional/plugins/test_endpoints) into the shared kbn-scout PKI base config (pkiConfig is an exported platform package config). Every consumer of this config — including PKI Scout suites owned by other teams — will now load this security functional-test plugin, and via its init_routes.ts it monkey-patches PKIAuthenticationProvider.prototype.authenticate globally on startup (header-gated, but the wrapper is installed for all requests).

That couples a shared platform package to an x-pack solution test fixture and changes the runtime environment for unrelated PKI test runs. Consider injecting the --plugin-path from the security plugin's own Scout config layer (e.g. extend pkiConfig in the security test config) rather than mutating the shared base config, so the coupling and the prototype patch stay scoped to the suite that needs them.

REPO_ROOT,
'x-pack/platform/test/security_functional/plugins/test_endpoints'
)}`
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Custom server config earns its keep

This modifies the shared pki config set — used by the existing PKI login/no-role-mapping UI specs — to unconditionally load the test_endpoints functional-test plugin. Only the new pki_http2_stream_cancel.spec.ts needs it, so consider isolating the plugin path to that spec instead of pulling it into every consumer of the shared config.

See details

The shared pki config set is imported by pki/stateful/classic.stateful.config.ts and served to any spec under test/scout_pki/**. Today that includes pki_login.spec.ts and pki_no_role_mapping.spec.ts, neither of which touches /authentication/preauth_holds — they now boot Kibana with a test-only plugin they dont exercise. It also couples the @kbn/scout package to a plugin owned by x-pack/platform/test/security_functional/plugins/test_endpoints, so a future move/rename there breaks all scout pki consumers.

Because --plugin-path is a startup-only argument, apiServices.core.settings() isnt a substitute here — the alternative is scoping the plugin path to just the new HTTP/2 API spec. A few options worth considering:

  • Define a narrower config variant (e.g. a pki_http2_stress set that composes pkiConfig and appends the plugin path) and point the new API playwright config set at it, leaving the shared pki set untouched.
  • Or keep the shared set as-is and register the extra plugin path via the API tests own playwright/scout config.

Either keeps blast radius on the one spec that actually needs it.

Share feedback in the #appex-qa Slack channel.

import { apiTest as baseApiTest } from '@kbn/scout';

import { PkiHttp2Client } from './pki_http2_client';
import { FIRST_CLIENT_P12 } from '../../ui/fixtures/constants';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API fixture reaches into the sibling UI fixture tree

api/fixtures/index.ts imports FIRST_CLIENT_P12 from ../../ui/fixtures/constants, so this API suite now silently depends on the UI suites file layout — moving or renaming anything under ui/fixtures/ breaks the API tests. Consider hoisting the shared PKI cert constants into a location both scopes can depend on.

See details

The UI constants.ts file exports test artefacts (FIRST_CLIENT_P12, SECOND_CLIENT_P12, KIBANA_TLS_ORIGIN) that are conceptually shared between UI and API suites — none of them are UI-specific. Reaching across ui/ from api/ mixes the two scopes and makes it easy to accidentally couple UI test data to an API implementation detail (or vice versa).

Suggested fixes (any of these is fine):

  • Move the PKI cert + host constants to a shared location, e.g. test/scout_pki/common/constants.ts, and import from both ui/fixtures/* and api/fixtures/*.
  • Or duplicate the single constant that this file needs (FIRST_CLIENT_P12) into api/fixtures/constants.ts — it is a small readFileSync and would keep the two suites independent.

Share feedback in the #appex-qa Slack channel.

The Scout test added in 930a826 supersedes this suite and is a stronger
regression lock. The FTR test fired 20 concurrent streams and cancelled
after a fixed 150ms, hoping to land inside the auth window, and never
verified the socket actually degraded — so on a fast agent it could miss
the window and still pass against an unpatched build.

The Scout test parks inside PKIAuthenticationProvider.authenticate and
asserts the state transition explicitly (healthy at park, degraded after
RST, session intact after release), so it cannot pass without exercising
the bug. It also asserts the authentication provider on the follow-up
request, which the FTR test did not.

Removes the test, its config, and the FTR manifest entry. pki.config.ts
and pki.http2.config.ts are pre-existing and unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
@jeramysoucy

Copy link
Copy Markdown
Contributor Author

Review of the Scout API test (930a826)

I traced the full config chain and read all nine files. The Scout test is sound, and it's a stronger regression lock than the FTR test I added. I've removed the FTR stress test accordingly — details at the bottom.

It closes a false-green hole in my FTR test

My FTR test fired 20 concurrent streams and cancelled after a fixed 150ms, hoping to land inside the auth window. It never verified the socket actually degraded — on a fast agent it could miss the window entirely and still pass against an unpatched build. That's a poor property for a regression test.

The Scout test is deterministic. Parking inside PKIAuthenticationProvider.authenticate and asserting the state transition explicitly means it cannot pass without exercising the bug:

  • at park — authorized === true, peerCertificateNull === false (socket healthy)
  • after RST — peerCertificateNull === true, authorized !== true (socket degraded)
  • only then release, so the real authenticate runs against the degraded socket

If the degradation never happens, the poll times out and the test fails.

Two traps caught that would have produced silently useless tests

  1. Parking in authenticate rather than onPreAuth. RST during onPreAuth sets Hapi's _isReplied, the lifecycle bails, and Auth never runs — so PKI never sees the destroyed socket and the test passes with or without the fix. Good call, and thanks for documenting it in-code.
  2. The continuedAfterHold === false condition. Without it, a hold hitting its 10s timeout degrades the socket the same way after normal completion, so the assertion would pass without exercising the target path. Subtle, and easy to lose in a later refactor — glad it has a comment.

Coverage comparison

FTR (removed) Scout
Real RST_STREAM over HTTP/2 yes yes
Runs on server.protocol: http2 via configureHTTP2() via http2: true in the pki config set
Hits the auth window probabilistic (20x, 150ms) deterministic (server-side park)
Verifies the socket actually degraded no yes
Session survives (follow-up 200 + first_client) yes yes, plus asserts provider pki/pki1

The 20-stream concurrency in mine was a trigger amplifier for a probabilistic window, not an independent assertion — both tests assert a subsequent request on the same HTTP/2 session. Nothing is lost by the swap.

I also confirmed the CI wiring end to end: test/scout_pki/api/detect_custom_config.ts regex /test/scout_([^/]+)/ → config set pki → tag stateful.classicclassic.stateful.config.ts sets http2: trueconfigureHTTP2() applies --server.protocol=http2. security is in plugins.enabled and the new path is not in excluded_configs, so it auto-discovers.

Three non-blocking observations

  1. Monkey-patch failure is hard to diagnose. findPkiAuthenticationProvider() correctly throws rather than silently no-op'ing, but that diagnostic lands in the Kibana server log, not test output. If pki.ts ever moves, the symptom is an opaque expect.poll(parked) timeout. Cheap hardening: have the hold-status route report whether the patch installed, and assert it in beforeAll.

  2. PREAUTH_HOLD_TIMEOUT_MS (10s) is the flake budget. If a loaded agent delays RST processing past it, continuedAfterHold flips true and the degradation poll can never satisfy. It fails rather than false-greens — correct direction — but it's the one flake vector I'd watch.

  3. Latent limitation in the shared pki config set. configureHTTP2() overwrites server.ssl.certificateAuthorities with CA_CERT_PATH alone. Fine here, since first_client.p12 chains to that CA — but it means the Scout pki config set can't host an untrusted_client test without a fixup like the one that was in pki.http2.stress.config.ts. Worth a comment in base.config.ts so it isn't rediscovered the hard way.

FTR removal

Removed in 24d55c0:

  • x-pack/platform/test/security_api_integration/tests/pki/pki_http2_stress.ts
  • x-pack/platform/test/security_api_integration/pki.http2.stress.config.ts
  • the corresponding entry in .buildkite/ftr-manifests/ftr_platform_stateful_configs.yml

pki.config.ts and pki.http2.config.ts are pre-existing and untouched. Grepped for dangling references to both removed files — none remain.

legrego added a commit that referenced this pull request Aug 17, 2026
…285244)

## Human summary

Improves the detection of completed requests by additionally checking
for `res.writableEnded`. This works around a quirk in Node.js when
running in http/2, which is resolved in 26+:
nodejs/node#63249.

Checking for `res.writableEnded` does not change anything when running
in http/1, as this is always true when `res.writableFinished`. This
changes behavior for http/2 by properly detecting aborted requests.

This should result in downstream consumers having a more reliable signal
for when a client request is aborted, allowing them to cancel jobs such
as in-flight ES requests regardless of http protocol.

We detected this while writing scout tests for
#285153

## AI description
When an HTTP/2 client destroys a stream mid-request (RST_STREAM /
NGHTTP2_CANCEL, e.g. an AbortController cancel or browser navigation),
Node's Http2ServerResponse emits 'close' with writableFinished === true
even though nothing was written. isCompleted() relied on
writableFinished alone, so the !isCompleted filter swallowed the event
and request.events.aborted$ never fired — consumers (route handlers,
auth providers) could not observe HTTP/2 client aborts. HTTP/1 was
unaffected because writableFinished stays false there.

Treat a request as completed only when writableFinished &&
writableEnded: writableEnded only becomes true once the server actually
ended the response, and is truthful on both protocols and for abrupt
TCP-level disconnects.

Adds integration coverage in http2_protocol.test.ts driving a real
HTTP/2 TLS session that resets the stream while the handler is pending,
plus a control test asserting completed$ (and not aborted$) on normal
completion.

Co-authored-by: Claude Fable 5 <[email protected]>
kibanamachine added a commit that referenced this pull request Aug 17, 2026
…uests (#285244) (#285382)

# Backport

This will backport the following commits from `main` to `9.4`:
- [[Core] Fix KibanaRequest aborted$ never emitting for HTTP/2 requests
(#285244)](#285244)

<!--- Backport version: 9.6.6 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Larry
Gregory","email":"[email protected]"},"sourceCommit":{"committedDate":"2026-08-17T11:55:38Z","message":"[Core]
Fix KibanaRequest aborted$ never emitting for HTTP/2 requests
(#285244)\n\n## Human summary\n\nImproves the detection of completed
requests by additionally checking\nfor `res.writableEnded`. This works
around a quirk in Node.js when\nrunning in http/2, which is resolved in
26+:\nhttps://github.com/nodejs/node/pull/63249.\n\nChecking for
`res.writableEnded` does not change anything when running\nin http/1, as
this is always true when `res.writableFinished`. This\nchanges behavior
for http/2 by properly detecting aborted requests.\n\nThis should result
in downstream consumers having a more reliable signal\nfor when a client
request is aborted, allowing them to cancel jobs such\nas in-flight ES
requests regardless of http protocol.\n\nWe detected this while writing
scout tests for\nhttps://github.com//pull/285153\n\n## AI
description\nWhen an HTTP/2 client destroys a stream mid-request
(RST_STREAM /\nNGHTTP2_CANCEL, e.g. an AbortController cancel or browser
navigation),\nNode's Http2ServerResponse emits 'close' with
writableFinished === true\neven though nothing was written.
isCompleted() relied on\nwritableFinished alone, so the !isCompleted
filter swallowed the event\nand request.events.aborted$ never fired —
consumers (route handlers,\nauth providers) could not observe HTTP/2
client aborts. HTTP/1 was\nunaffected because writableFinished stays
false there.\n\nTreat a request as completed only when writableFinished
&&\nwritableEnded: writableEnded only becomes true once the server
actually\nended the response, and is truthful on both protocols and for
abrupt\nTCP-level disconnects.\n\nAdds integration coverage in
http2_protocol.test.ts driving a real\nHTTP/2 TLS session that resets
the stream while the handler is pending,\nplus a control test asserting
completed$ (and not aborted$) on normal\ncompletion.\n\nCo-authored-by:
Claude Fable 5
<[email protected]>","sha":"a19e27a5dc885a40a31998f728c450d8aee27289","branchLabelMapping":{"^v9.6.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Team:Security","release_note:skip","backport:all-open","v9.6.0"],"title":"[Core]
Fix KibanaRequest aborted$ never emitting for HTTP/2
requests","number":285244,"url":"https://github.com/elastic/kibana/pull/285244","mergeCommit":{"message":"[Core]
Fix KibanaRequest aborted$ never emitting for HTTP/2 requests
(#285244)\n\n## Human summary\n\nImproves the detection of completed
requests by additionally checking\nfor `res.writableEnded`. This works
around a quirk in Node.js when\nrunning in http/2, which is resolved in
26+:\nhttps://github.com/nodejs/node/pull/63249.\n\nChecking for
`res.writableEnded` does not change anything when running\nin http/1, as
this is always true when `res.writableFinished`. This\nchanges behavior
for http/2 by properly detecting aborted requests.\n\nThis should result
in downstream consumers having a more reliable signal\nfor when a client
request is aborted, allowing them to cancel jobs such\nas in-flight ES
requests regardless of http protocol.\n\nWe detected this while writing
scout tests for\nhttps://github.com//pull/285153\n\n## AI
description\nWhen an HTTP/2 client destroys a stream mid-request
(RST_STREAM /\nNGHTTP2_CANCEL, e.g. an AbortController cancel or browser
navigation),\nNode's Http2ServerResponse emits 'close' with
writableFinished === true\neven though nothing was written.
isCompleted() relied on\nwritableFinished alone, so the !isCompleted
filter swallowed the event\nand request.events.aborted$ never fired —
consumers (route handlers,\nauth providers) could not observe HTTP/2
client aborts. HTTP/1 was\nunaffected because writableFinished stays
false there.\n\nTreat a request as completed only when writableFinished
&&\nwritableEnded: writableEnded only becomes true once the server
actually\nended the response, and is truthful on both protocols and for
abrupt\nTCP-level disconnects.\n\nAdds integration coverage in
http2_protocol.test.ts driving a real\nHTTP/2 TLS session that resets
the stream while the handler is pending,\nplus a control test asserting
completed$ (and not aborted$) on normal\ncompletion.\n\nCo-authored-by:
Claude Fable 5
<[email protected]>","sha":"a19e27a5dc885a40a31998f728c450d8aee27289"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.6.0","branchLabelMappingKey":"^v9.6.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/285244","number":285244,"mergeCommit":{"message":"[Core]
Fix KibanaRequest aborted$ never emitting for HTTP/2 requests
(#285244)\n\n## Human summary\n\nImproves the detection of completed
requests by additionally checking\nfor `res.writableEnded`. This works
around a quirk in Node.js when\nrunning in http/2, which is resolved in
26+:\nhttps://github.com/nodejs/node/pull/63249.\n\nChecking for
`res.writableEnded` does not change anything when running\nin http/1, as
this is always true when `res.writableFinished`. This\nchanges behavior
for http/2 by properly detecting aborted requests.\n\nThis should result
in downstream consumers having a more reliable signal\nfor when a client
request is aborted, allowing them to cancel jobs such\nas in-flight ES
requests regardless of http protocol.\n\nWe detected this while writing
scout tests for\nhttps://github.com//pull/285153\n\n## AI
description\nWhen an HTTP/2 client destroys a stream mid-request
(RST_STREAM /\nNGHTTP2_CANCEL, e.g. an AbortController cancel or browser
navigation),\nNode's Http2ServerResponse emits 'close' with
writableFinished === true\neven though nothing was written.
isCompleted() relied on\nwritableFinished alone, so the !isCompleted
filter swallowed the event\nand request.events.aborted$ never fired —
consumers (route handlers,\nauth providers) could not observe HTTP/2
client aborts. HTTP/1 was\nunaffected because writableFinished stays
false there.\n\nTreat a request as completed only when writableFinished
&&\nwritableEnded: writableEnded only becomes true once the server
actually\nended the response, and is truthful on both protocols and for
abrupt\nTCP-level disconnects.\n\nAdds integration coverage in
http2_protocol.test.ts driving a real\nHTTP/2 TLS session that resets
the stream while the handler is pending,\nplus a control test asserting
completed$ (and not aborted$) on normal\ncompletion.\n\nCo-authored-by:
Claude Fable 5
<[email protected]>","sha":"a19e27a5dc885a40a31998f728c450d8aee27289"}}]}]
BACKPORT-->

Co-authored-by: Larry Gregory <[email protected]>
Co-authored-by: Claude Fable 5 <[email protected]>
kibanamachine added a commit that referenced this pull request Aug 17, 2026
…uests (#285244) (#285383)

# Backport

This will backport the following commits from `main` to `9.5`:
- [[Core] Fix KibanaRequest aborted$ never emitting for HTTP/2 requests
(#285244)](#285244)

<!--- Backport version: 9.6.6 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Larry
Gregory","email":"[email protected]"},"sourceCommit":{"committedDate":"2026-08-17T11:55:38Z","message":"[Core]
Fix KibanaRequest aborted$ never emitting for HTTP/2 requests
(#285244)\n\n## Human summary\n\nImproves the detection of completed
requests by additionally checking\nfor `res.writableEnded`. This works
around a quirk in Node.js when\nrunning in http/2, which is resolved in
26+:\nhttps://github.com/nodejs/node/pull/63249.\n\nChecking for
`res.writableEnded` does not change anything when running\nin http/1, as
this is always true when `res.writableFinished`. This\nchanges behavior
for http/2 by properly detecting aborted requests.\n\nThis should result
in downstream consumers having a more reliable signal\nfor when a client
request is aborted, allowing them to cancel jobs such\nas in-flight ES
requests regardless of http protocol.\n\nWe detected this while writing
scout tests for\nhttps://github.com//pull/285153\n\n## AI
description\nWhen an HTTP/2 client destroys a stream mid-request
(RST_STREAM /\nNGHTTP2_CANCEL, e.g. an AbortController cancel or browser
navigation),\nNode's Http2ServerResponse emits 'close' with
writableFinished === true\neven though nothing was written.
isCompleted() relied on\nwritableFinished alone, so the !isCompleted
filter swallowed the event\nand request.events.aborted$ never fired —
consumers (route handlers,\nauth providers) could not observe HTTP/2
client aborts. HTTP/1 was\nunaffected because writableFinished stays
false there.\n\nTreat a request as completed only when writableFinished
&&\nwritableEnded: writableEnded only becomes true once the server
actually\nended the response, and is truthful on both protocols and for
abrupt\nTCP-level disconnects.\n\nAdds integration coverage in
http2_protocol.test.ts driving a real\nHTTP/2 TLS session that resets
the stream while the handler is pending,\nplus a control test asserting
completed$ (and not aborted$) on normal\ncompletion.\n\nCo-authored-by:
Claude Fable 5
<[email protected]>","sha":"a19e27a5dc885a40a31998f728c450d8aee27289","branchLabelMapping":{"^v9.6.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Team:Security","release_note:skip","backport:all-open","v9.6.0"],"title":"[Core]
Fix KibanaRequest aborted$ never emitting for HTTP/2
requests","number":285244,"url":"https://github.com/elastic/kibana/pull/285244","mergeCommit":{"message":"[Core]
Fix KibanaRequest aborted$ never emitting for HTTP/2 requests
(#285244)\n\n## Human summary\n\nImproves the detection of completed
requests by additionally checking\nfor `res.writableEnded`. This works
around a quirk in Node.js when\nrunning in http/2, which is resolved in
26+:\nhttps://github.com/nodejs/node/pull/63249.\n\nChecking for
`res.writableEnded` does not change anything when running\nin http/1, as
this is always true when `res.writableFinished`. This\nchanges behavior
for http/2 by properly detecting aborted requests.\n\nThis should result
in downstream consumers having a more reliable signal\nfor when a client
request is aborted, allowing them to cancel jobs such\nas in-flight ES
requests regardless of http protocol.\n\nWe detected this while writing
scout tests for\nhttps://github.com//pull/285153\n\n## AI
description\nWhen an HTTP/2 client destroys a stream mid-request
(RST_STREAM /\nNGHTTP2_CANCEL, e.g. an AbortController cancel or browser
navigation),\nNode's Http2ServerResponse emits 'close' with
writableFinished === true\neven though nothing was written.
isCompleted() relied on\nwritableFinished alone, so the !isCompleted
filter swallowed the event\nand request.events.aborted$ never fired —
consumers (route handlers,\nauth providers) could not observe HTTP/2
client aborts. HTTP/1 was\nunaffected because writableFinished stays
false there.\n\nTreat a request as completed only when writableFinished
&&\nwritableEnded: writableEnded only becomes true once the server
actually\nended the response, and is truthful on both protocols and for
abrupt\nTCP-level disconnects.\n\nAdds integration coverage in
http2_protocol.test.ts driving a real\nHTTP/2 TLS session that resets
the stream while the handler is pending,\nplus a control test asserting
completed$ (and not aborted$) on normal\ncompletion.\n\nCo-authored-by:
Claude Fable 5
<[email protected]>","sha":"a19e27a5dc885a40a31998f728c450d8aee27289"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.6.0","branchLabelMappingKey":"^v9.6.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/285244","number":285244,"mergeCommit":{"message":"[Core]
Fix KibanaRequest aborted$ never emitting for HTTP/2 requests
(#285244)\n\n## Human summary\n\nImproves the detection of completed
requests by additionally checking\nfor `res.writableEnded`. This works
around a quirk in Node.js when\nrunning in http/2, which is resolved in
26+:\nhttps://github.com/nodejs/node/pull/63249.\n\nChecking for
`res.writableEnded` does not change anything when running\nin http/1, as
this is always true when `res.writableFinished`. This\nchanges behavior
for http/2 by properly detecting aborted requests.\n\nThis should result
in downstream consumers having a more reliable signal\nfor when a client
request is aborted, allowing them to cancel jobs such\nas in-flight ES
requests regardless of http protocol.\n\nWe detected this while writing
scout tests for\nhttps://github.com//pull/285153\n\n## AI
description\nWhen an HTTP/2 client destroys a stream mid-request
(RST_STREAM /\nNGHTTP2_CANCEL, e.g. an AbortController cancel or browser
navigation),\nNode's Http2ServerResponse emits 'close' with
writableFinished === true\neven though nothing was written.
isCompleted() relied on\nwritableFinished alone, so the !isCompleted
filter swallowed the event\nand request.events.aborted$ never fired —
consumers (route handlers,\nauth providers) could not observe HTTP/2
client aborts. HTTP/1 was\nunaffected because writableFinished stays
false there.\n\nTreat a request as completed only when writableFinished
&&\nwritableEnded: writableEnded only becomes true once the server
actually\nended the response, and is truthful on both protocols and for
abrupt\nTCP-level disconnects.\n\nAdds integration coverage in
http2_protocol.test.ts driving a real\nHTTP/2 TLS session that resets
the stream while the handler is pending,\nplus a control test asserting
completed$ (and not aborted$) on normal\ncompletion.\n\nCo-authored-by:
Claude Fable 5
<[email protected]>","sha":"a19e27a5dc885a40a31998f728c450d8aee27289"}}]}]
BACKPORT-->

Co-authored-by: Larry Gregory <[email protected]>
Co-authored-by: Claude Fable 5 <[email protected]>
jeramysoucy and others added 2 commits August 17, 2026 15:52
The --plugin-path for the security functional-test plugin was appended to
the shared `pki` base config, so every suite derived from it booted the
plugin — including the PKI login specs, which never call its routes.

Adds a `pki_stress` config set that spreads `pkiConfig`, keeps http2, and
carries the plugin path, and moves the HTTP/2 stream-cancel suite to
test/scout_pki_stress/api/. Scout resolves the server config from the
test/scout_<name> directory component, so scoping it required the move.
The `pki` set is restored to its previous content.

This matters slightly beyond boot cost: the login specs authenticate via
PKI, and the plugin patches PKIAuthenticationProvider.authenticate. It's a
header-gated pass-through, but those specs are better off exercising an
unmodified auth provider.

Also decouples the API fixtures from the UI suite — FIRST_CLIENT_P12 now
lives in the API suite's own constants rather than being imported across
the two test trees.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
@jeramysoucy
jeramysoucy requested a review from dmlemeshko August 18, 2026 06:50

@hammad-nasir-elastic hammad-nasir-elastic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Core changes LGTM. Spot checked new tests as well.

@jeramysoucy
jeramysoucy enabled auto-merge (squash) August 24, 2026 08:14
@kibanamachine

Copy link
Copy Markdown
Contributor

💛 Build succeeded, but was flaky

Failed CI Steps

Metrics [docs]

✅ unchanged

Test Failures

  • [job] [logs] FTR Configs #125 / serverless observability UI - ML and Discover discover/observabilitySolution/context_awareness extension getRowIndicatorProvider should not render log.level row indicators for logs data source without a log.level field

History

@jeramysoucy
jeramysoucy merged commit a052026 into elastic:main Aug 24, 2026
41 checks passed
@kibanamachine

Copy link
Copy Markdown
Contributor

@kibanamachine

Copy link
Copy Markdown
Contributor

💔 Some backports could not be created

Status Branch Result
8.19 Backport failed because of merge conflicts

You might need to backport the following PRs to 8.19:
- [CI] Jest through moon (#259075)
9.4
9.5

Note: Successful backport PRs will be merged automatically after passing CI.

Manual backport

To create the backport manually run:

node scripts/backport --pr 285153

Questions ?

Please refer to the Backport tool documentation

@jeramysoucy

Copy link
Copy Markdown
Contributor Author

💚 All backports created successfully

Status Branch Result
8.19

Note: Successful backport PRs will be merged automatically after passing CI.

Questions ?

Please refer to the Backport tool documentation

kibanamachine added a commit that referenced this pull request Aug 24, 2026
…ction (#285153) (#286787)

# Backport

This will backport the following commits from `main` to `9.5`:
- [[Security] Fix PKI session invalidation on HTTP/2 stream destruction
(#285153)](#285153)

<!--- Backport version: 9.6.6 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Jeramy
Soucy","email":"[email protected]"},"sourceCommit":{"committedDate":"2026-08-24T09:23:24Z","message":"[Security]
Fix PKI session invalidation on HTTP/2 stream destruction
(#285153)\n\n## Summary\n\nFixes #258232.\n\nPKI sessions were being
unexpectedly invalidated on Kibana 9.x when\n`server.protocol: http2`
was active (the new default when `ssl.enabled:\ntrue`). Setting
`server.protocol: http1` was the customer workaround.\n\n### Root
cause\n\nWhen an HTTP/2 stream is destroyed mid-request — by an
`AbortController`\ncancel, browser navigation abort, or any other cause
that sends\n`RST_STREAM` — Node.js clears the stream's session
reference\n(`stream.session = undefined`). The
`Http2ServerRequest.socket` proxy\nthen falls back from the TLS socket
to the stream itself, causing every\n`instanceof TLSSocket` check in
`KibanaSocket` to return `false`. The\nresult:\n\n- `socket.authorized`
→ `undefined` (not `true` or `false`)\n- `socket.getPeerCertificate()` →
`null`\n\nThe guard in `authenticateViaState` (pki.ts line ~208) was
written for\nexactly this \"connection closed\" case:\n\n```ts\n//
Before (buggy):\nif (peerCertificate === null &&
request.socket.authorized) {\n // return non-401 soft fail — session
preserved\n}\n```\n\nOn HTTP/1.1 a closed `TLSSocket` keeps `authorized
=== true`, so the\nguard fired correctly. On HTTP/2, `authorized ===
undefined` is falsy,\nso the guard was silently skipped. The code then
fell through to the\ntoken-invalidation block, revoked the ES access
token, and returned\n`notHandled()` — which `authenticate()` converted
to\n`Boom.unauthorized()`. `updateSessionValue()` saw the 401 on an
owned\nsession and **deleted the server-side session**, logging the user
out.\n\n### Fix\n\nChange the guard from a truthy check to `!==
false`:\n\n```ts\n// After (fixed):\nif (peerCertificate === null &&
request.socket.authorized !== false) {\n // return non-401 soft fail —
session preserved\n}\n```\n\n`false` = cert was presented but rejected →
token invalidation is\ncorrect.\n`undefined` = socket state is
unknowable (HTTP/2 stream destroyed) →\ntreat same as `true`, preserve
session.\n`true` = socket explicitly authorized → original behaviour
unchanged.\n\nAlso improves the diagnostic log in
`authenticateViaPeerCertificate` to\ndistinguish `false` (rejected cert)
from `undefined` (unknown state),\nmaking production logs
actionable.\n\n### Why this is new in 9.x\n\n`http_config.ts` defaults
`server.protocol` to `http2` when\n`ssl.enabled: true` — a silent
breaking change from 8.x. HTTP/1.1 is\nunaffected because each request
has a dedicated `TLSSocket` that keeps\n`authorized === true` as a plain
property after the connection closes.\n\n## Test plan\n\n- [x] New
regression test in `pki.test.ts`: `does not invalidate token\nor destroy
session when socket state is unknown due to HTTP/2 stream\ndestruction`
— fails on `main`, passes after fix\n- [x] New test in `pki.test.ts`:
`does not handle requests when socket\nauthorization state is unknown
due to HTTP/2 stream destruction` —\ncovers `login` and session-less
`authenticate`, verifies correct log\nmessage\n- [x] All 40 existing PKI
unit tests continue to pass\n\nRun tests:\n```\nnode scripts/jest
--config x-pack/platform/plugins/shared/security/jest.config.js
--testPathPattern=\"authentication/providers/pki\"
--no-coverage\n```\n\n🤖 Generated with [Claude
Code](https://claude.com/claude-code)\n\n---\n\n## FTR stress test
(regression coverage for RST_STREAM)\n\n`pki.test.ts` unit tests are the
authoritative regression lock — they're\ndeterministic and always catch
a revert. An additional FTR test is\nincluded to provide end-to-end
coverage of the exact failure
mode:\n\n\n**`x-pack/platform/test/security_api_integration/tests/pki/pki_http2_stress.ts`**\nRegistered
in `pki.http2.stress.config.ts` and added
to\n`ftr_platform_stateful_configs.yml`.\n\n### Why supertest can't
cover this\n\nThe existing `pki_auth.ts` concurrent-request test (5
streams) uses\nsupertest, which speaks HTTP/1.1. RST_STREAM is an
HTTP/2-only signal —\nsupertest has no way to emit it. That is why the
bug wasn't caught by\nthe existing concurrent-auth test.\n\n### What the
FTR test does\n\n1. Establishes a PKI session via supertest (HTTP/1.1 —
simple,\nreliable).\n2. Opens a raw `http2.connect()` connection to
Kibana with the same PKI\nclient certificate.\n3. Fires 20 concurrent
slow requests against the test endpoints plugin\n(10s hold, giving
Kibana time to enter the async session-lookup window).\n4. After 150ms,
cancels all 20 streams with `RST_STREAM NGHTTP2_CANCEL`.\n5. Sends a
normal (non-cancelled) `GET /internal/security/me` on the\nsame HTTP/2
session.\n6. Asserts `200` + `username === 'first_client'` — the session
was NOT\ninvalidated.\n\nBefore the fix: at least one RST_STREAM would
destroy the stream during\nthe auth lifecycle, causing `authorized →
undefined`, which bypassed the\nguard, invalidated the ES token, and
returned 401.\nAfter the fix: all RST_STREAM frames are handled
gracefully; the session\nsurvives.\n\n### Stability note\n\nAfter the
fix is applied, the test is unconditionally stable — the guard\nchange
means RST_STREAM never reaches the token-invalidation path\nregardless
of timing. The test cannot produce a false positive (failing\nwhen the
fix is present). Before the fix it might occasionally miss the\nnarrow
timing window (false negative), but this is acceptable for a\nregression
test: it cannot regress silently if the fix is reverted,\nbecause the
unit test in `pki.test.ts` will always catch it.\n\n## Release
note\nFixes an issue where PKI authentication sessions were
unexpectedly\ninvalidated when in-flight requests were cancelled over
HTTP/2, logging\nusers out. This affects deployments on server.protocol:
http2, which\nbecame the default in 9.0.0 when TLS is
enabled.\n\n---------\n\nCo-authored-by: Claude Sonnet 4.6
<[email protected]>\nCo-authored-by: kibanamachine
<[email protected]>\nCo-authored-by: Larry
Gregory
<[email protected]>","sha":"a0520264b3fca9841b7e85f9c4b1996eb038b60e","branchLabelMapping":{"^v9.6.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","Team:Security","backport:all-open","reviewer:scout","v9.6.0"],"title":"[Security]
Fix PKI session invalidation on HTTP/2 stream
destruction","number":285153,"url":"https://github.com/elastic/kibana/pull/285153","mergeCommit":{"message":"[Security]
Fix PKI session invalidation on HTTP/2 stream destruction
(#285153)\n\n## Summary\n\nFixes #258232.\n\nPKI sessions were being
unexpectedly invalidated on Kibana 9.x when\n`server.protocol: http2`
was active (the new default when `ssl.enabled:\ntrue`). Setting
`server.protocol: http1` was the customer workaround.\n\n### Root
cause\n\nWhen an HTTP/2 stream is destroyed mid-request — by an
`AbortController`\ncancel, browser navigation abort, or any other cause
that sends\n`RST_STREAM` — Node.js clears the stream's session
reference\n(`stream.session = undefined`). The
`Http2ServerRequest.socket` proxy\nthen falls back from the TLS socket
to the stream itself, causing every\n`instanceof TLSSocket` check in
`KibanaSocket` to return `false`. The\nresult:\n\n- `socket.authorized`
→ `undefined` (not `true` or `false`)\n- `socket.getPeerCertificate()` →
`null`\n\nThe guard in `authenticateViaState` (pki.ts line ~208) was
written for\nexactly this \"connection closed\" case:\n\n```ts\n//
Before (buggy):\nif (peerCertificate === null &&
request.socket.authorized) {\n // return non-401 soft fail — session
preserved\n}\n```\n\nOn HTTP/1.1 a closed `TLSSocket` keeps `authorized
=== true`, so the\nguard fired correctly. On HTTP/2, `authorized ===
undefined` is falsy,\nso the guard was silently skipped. The code then
fell through to the\ntoken-invalidation block, revoked the ES access
token, and returned\n`notHandled()` — which `authenticate()` converted
to\n`Boom.unauthorized()`. `updateSessionValue()` saw the 401 on an
owned\nsession and **deleted the server-side session**, logging the user
out.\n\n### Fix\n\nChange the guard from a truthy check to `!==
false`:\n\n```ts\n// After (fixed):\nif (peerCertificate === null &&
request.socket.authorized !== false) {\n // return non-401 soft fail —
session preserved\n}\n```\n\n`false` = cert was presented but rejected →
token invalidation is\ncorrect.\n`undefined` = socket state is
unknowable (HTTP/2 stream destroyed) →\ntreat same as `true`, preserve
session.\n`true` = socket explicitly authorized → original behaviour
unchanged.\n\nAlso improves the diagnostic log in
`authenticateViaPeerCertificate` to\ndistinguish `false` (rejected cert)
from `undefined` (unknown state),\nmaking production logs
actionable.\n\n### Why this is new in 9.x\n\n`http_config.ts` defaults
`server.protocol` to `http2` when\n`ssl.enabled: true` — a silent
breaking change from 8.x. HTTP/1.1 is\nunaffected because each request
has a dedicated `TLSSocket` that keeps\n`authorized === true` as a plain
property after the connection closes.\n\n## Test plan\n\n- [x] New
regression test in `pki.test.ts`: `does not invalidate token\nor destroy
session when socket state is unknown due to HTTP/2 stream\ndestruction`
— fails on `main`, passes after fix\n- [x] New test in `pki.test.ts`:
`does not handle requests when socket\nauthorization state is unknown
due to HTTP/2 stream destruction` —\ncovers `login` and session-less
`authenticate`, verifies correct log\nmessage\n- [x] All 40 existing PKI
unit tests continue to pass\n\nRun tests:\n```\nnode scripts/jest
--config x-pack/platform/plugins/shared/security/jest.config.js
--testPathPattern=\"authentication/providers/pki\"
--no-coverage\n```\n\n🤖 Generated with [Claude
Code](https://claude.com/claude-code)\n\n---\n\n## FTR stress test
(regression coverage for RST_STREAM)\n\n`pki.test.ts` unit tests are the
authoritative regression lock — they're\ndeterministic and always catch
a revert. An additional FTR test is\nincluded to provide end-to-end
coverage of the exact failure
mode:\n\n\n**`x-pack/platform/test/security_api_integration/tests/pki/pki_http2_stress.ts`**\nRegistered
in `pki.http2.stress.config.ts` and added
to\n`ftr_platform_stateful_configs.yml`.\n\n### Why supertest can't
cover this\n\nThe existing `pki_auth.ts` concurrent-request test (5
streams) uses\nsupertest, which speaks HTTP/1.1. RST_STREAM is an
HTTP/2-only signal —\nsupertest has no way to emit it. That is why the
bug wasn't caught by\nthe existing concurrent-auth test.\n\n### What the
FTR test does\n\n1. Establishes a PKI session via supertest (HTTP/1.1 —
simple,\nreliable).\n2. Opens a raw `http2.connect()` connection to
Kibana with the same PKI\nclient certificate.\n3. Fires 20 concurrent
slow requests against the test endpoints plugin\n(10s hold, giving
Kibana time to enter the async session-lookup window).\n4. After 150ms,
cancels all 20 streams with `RST_STREAM NGHTTP2_CANCEL`.\n5. Sends a
normal (non-cancelled) `GET /internal/security/me` on the\nsame HTTP/2
session.\n6. Asserts `200` + `username === 'first_client'` — the session
was NOT\ninvalidated.\n\nBefore the fix: at least one RST_STREAM would
destroy the stream during\nthe auth lifecycle, causing `authorized →
undefined`, which bypassed the\nguard, invalidated the ES token, and
returned 401.\nAfter the fix: all RST_STREAM frames are handled
gracefully; the session\nsurvives.\n\n### Stability note\n\nAfter the
fix is applied, the test is unconditionally stable — the guard\nchange
means RST_STREAM never reaches the token-invalidation path\nregardless
of timing. The test cannot produce a false positive (failing\nwhen the
fix is present). Before the fix it might occasionally miss the\nnarrow
timing window (false negative), but this is acceptable for a\nregression
test: it cannot regress silently if the fix is reverted,\nbecause the
unit test in `pki.test.ts` will always catch it.\n\n## Release
note\nFixes an issue where PKI authentication sessions were
unexpectedly\ninvalidated when in-flight requests were cancelled over
HTTP/2, logging\nusers out. This affects deployments on server.protocol:
http2, which\nbecame the default in 9.0.0 when TLS is
enabled.\n\n---------\n\nCo-authored-by: Claude Sonnet 4.6
<[email protected]>\nCo-authored-by: kibanamachine
<[email protected]>\nCo-authored-by: Larry
Gregory
<[email protected]>","sha":"a0520264b3fca9841b7e85f9c4b1996eb038b60e"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.6.0","branchLabelMappingKey":"^v9.6.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/285153","number":285153,"mergeCommit":{"message":"[Security]
Fix PKI session invalidation on HTTP/2 stream destruction
(#285153)\n\n## Summary\n\nFixes #258232.\n\nPKI sessions were being
unexpectedly invalidated on Kibana 9.x when\n`server.protocol: http2`
was active (the new default when `ssl.enabled:\ntrue`). Setting
`server.protocol: http1` was the customer workaround.\n\n### Root
cause\n\nWhen an HTTP/2 stream is destroyed mid-request — by an
`AbortController`\ncancel, browser navigation abort, or any other cause
that sends\n`RST_STREAM` — Node.js clears the stream's session
reference\n(`stream.session = undefined`). The
`Http2ServerRequest.socket` proxy\nthen falls back from the TLS socket
to the stream itself, causing every\n`instanceof TLSSocket` check in
`KibanaSocket` to return `false`. The\nresult:\n\n- `socket.authorized`
→ `undefined` (not `true` or `false`)\n- `socket.getPeerCertificate()` →
`null`\n\nThe guard in `authenticateViaState` (pki.ts line ~208) was
written for\nexactly this \"connection closed\" case:\n\n```ts\n//
Before (buggy):\nif (peerCertificate === null &&
request.socket.authorized) {\n // return non-401 soft fail — session
preserved\n}\n```\n\nOn HTTP/1.1 a closed `TLSSocket` keeps `authorized
=== true`, so the\nguard fired correctly. On HTTP/2, `authorized ===
undefined` is falsy,\nso the guard was silently skipped. The code then
fell through to the\ntoken-invalidation block, revoked the ES access
token, and returned\n`notHandled()` — which `authenticate()` converted
to\n`Boom.unauthorized()`. `updateSessionValue()` saw the 401 on an
owned\nsession and **deleted the server-side session**, logging the user
out.\n\n### Fix\n\nChange the guard from a truthy check to `!==
false`:\n\n```ts\n// After (fixed):\nif (peerCertificate === null &&
request.socket.authorized !== false) {\n // return non-401 soft fail —
session preserved\n}\n```\n\n`false` = cert was presented but rejected →
token invalidation is\ncorrect.\n`undefined` = socket state is
unknowable (HTTP/2 stream destroyed) →\ntreat same as `true`, preserve
session.\n`true` = socket explicitly authorized → original behaviour
unchanged.\n\nAlso improves the diagnostic log in
`authenticateViaPeerCertificate` to\ndistinguish `false` (rejected cert)
from `undefined` (unknown state),\nmaking production logs
actionable.\n\n### Why this is new in 9.x\n\n`http_config.ts` defaults
`server.protocol` to `http2` when\n`ssl.enabled: true` — a silent
breaking change from 8.x. HTTP/1.1 is\nunaffected because each request
has a dedicated `TLSSocket` that keeps\n`authorized === true` as a plain
property after the connection closes.\n\n## Test plan\n\n- [x] New
regression test in `pki.test.ts`: `does not invalidate token\nor destroy
session when socket state is unknown due to HTTP/2 stream\ndestruction`
— fails on `main`, passes after fix\n- [x] New test in `pki.test.ts`:
`does not handle requests when socket\nauthorization state is unknown
due to HTTP/2 stream destruction` —\ncovers `login` and session-less
`authenticate`, verifies correct log\nmessage\n- [x] All 40 existing PKI
unit tests continue to pass\n\nRun tests:\n```\nnode scripts/jest
--config x-pack/platform/plugins/shared/security/jest.config.js
--testPathPattern=\"authentication/providers/pki\"
--no-coverage\n```\n\n🤖 Generated with [Claude
Code](https://claude.com/claude-code)\n\n---\n\n## FTR stress test
(regression coverage for RST_STREAM)\n\n`pki.test.ts` unit tests are the
authoritative regression lock — they're\ndeterministic and always catch
a revert. An additional FTR test is\nincluded to provide end-to-end
coverage of the exact failure
mode:\n\n\n**`x-pack/platform/test/security_api_integration/tests/pki/pki_http2_stress.ts`**\nRegistered
in `pki.http2.stress.config.ts` and added
to\n`ftr_platform_stateful_configs.yml`.\n\n### Why supertest can't
cover this\n\nThe existing `pki_auth.ts` concurrent-request test (5
streams) uses\nsupertest, which speaks HTTP/1.1. RST_STREAM is an
HTTP/2-only signal —\nsupertest has no way to emit it. That is why the
bug wasn't caught by\nthe existing concurrent-auth test.\n\n### What the
FTR test does\n\n1. Establishes a PKI session via supertest (HTTP/1.1 —
simple,\nreliable).\n2. Opens a raw `http2.connect()` connection to
Kibana with the same PKI\nclient certificate.\n3. Fires 20 concurrent
slow requests against the test endpoints plugin\n(10s hold, giving
Kibana time to enter the async session-lookup window).\n4. After 150ms,
cancels all 20 streams with `RST_STREAM NGHTTP2_CANCEL`.\n5. Sends a
normal (non-cancelled) `GET /internal/security/me` on the\nsame HTTP/2
session.\n6. Asserts `200` + `username === 'first_client'` — the session
was NOT\ninvalidated.\n\nBefore the fix: at least one RST_STREAM would
destroy the stream during\nthe auth lifecycle, causing `authorized →
undefined`, which bypassed the\nguard, invalidated the ES token, and
returned 401.\nAfter the fix: all RST_STREAM frames are handled
gracefully; the session\nsurvives.\n\n### Stability note\n\nAfter the
fix is applied, the test is unconditionally stable — the guard\nchange
means RST_STREAM never reaches the token-invalidation path\nregardless
of timing. The test cannot produce a false positive (failing\nwhen the
fix is present). Before the fix it might occasionally miss the\nnarrow
timing window (false negative), but this is acceptable for a\nregression
test: it cannot regress silently if the fix is reverted,\nbecause the
unit test in `pki.test.ts` will always catch it.\n\n## Release
note\nFixes an issue where PKI authentication sessions were
unexpectedly\ninvalidated when in-flight requests were cancelled over
HTTP/2, logging\nusers out. This affects deployments on server.protocol:
http2, which\nbecame the default in 9.0.0 when TLS is
enabled.\n\n---------\n\nCo-authored-by: Claude Sonnet 4.6
<[email protected]>\nCo-authored-by: kibanamachine
<[email protected]>\nCo-authored-by: Larry
Gregory
<[email protected]>","sha":"a0520264b3fca9841b7e85f9c4b1996eb038b60e"}}]}]
BACKPORT-->

Co-authored-by: Jeramy Soucy <[email protected]>
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
Co-authored-by: Larry Gregory <[email protected]>
kibanamachine added a commit that referenced this pull request Aug 25, 2026
…ction (#285153) (#286786)

# Backport

This will backport the following commits from `main` to `9.4`:
- [[Security] Fix PKI session invalidation on HTTP/2 stream destruction
(#285153)](#285153)

<!--- Backport version: 9.6.6 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Jeramy
Soucy","email":"[email protected]"},"sourceCommit":{"committedDate":"2026-08-24T09:23:24Z","message":"[Security]
Fix PKI session invalidation on HTTP/2 stream destruction
(#285153)\n\n## Summary\n\nFixes #258232.\n\nPKI sessions were being
unexpectedly invalidated on Kibana 9.x when\n`server.protocol: http2`
was active (the new default when `ssl.enabled:\ntrue`). Setting
`server.protocol: http1` was the customer workaround.\n\n### Root
cause\n\nWhen an HTTP/2 stream is destroyed mid-request — by an
`AbortController`\ncancel, browser navigation abort, or any other cause
that sends\n`RST_STREAM` — Node.js clears the stream's session
reference\n(`stream.session = undefined`). The
`Http2ServerRequest.socket` proxy\nthen falls back from the TLS socket
to the stream itself, causing every\n`instanceof TLSSocket` check in
`KibanaSocket` to return `false`. The\nresult:\n\n- `socket.authorized`
→ `undefined` (not `true` or `false`)\n- `socket.getPeerCertificate()` →
`null`\n\nThe guard in `authenticateViaState` (pki.ts line ~208) was
written for\nexactly this \"connection closed\" case:\n\n```ts\n//
Before (buggy):\nif (peerCertificate === null &&
request.socket.authorized) {\n // return non-401 soft fail — session
preserved\n}\n```\n\nOn HTTP/1.1 a closed `TLSSocket` keeps `authorized
=== true`, so the\nguard fired correctly. On HTTP/2, `authorized ===
undefined` is falsy,\nso the guard was silently skipped. The code then
fell through to the\ntoken-invalidation block, revoked the ES access
token, and returned\n`notHandled()` — which `authenticate()` converted
to\n`Boom.unauthorized()`. `updateSessionValue()` saw the 401 on an
owned\nsession and **deleted the server-side session**, logging the user
out.\n\n### Fix\n\nChange the guard from a truthy check to `!==
false`:\n\n```ts\n// After (fixed):\nif (peerCertificate === null &&
request.socket.authorized !== false) {\n // return non-401 soft fail —
session preserved\n}\n```\n\n`false` = cert was presented but rejected →
token invalidation is\ncorrect.\n`undefined` = socket state is
unknowable (HTTP/2 stream destroyed) →\ntreat same as `true`, preserve
session.\n`true` = socket explicitly authorized → original behaviour
unchanged.\n\nAlso improves the diagnostic log in
`authenticateViaPeerCertificate` to\ndistinguish `false` (rejected cert)
from `undefined` (unknown state),\nmaking production logs
actionable.\n\n### Why this is new in 9.x\n\n`http_config.ts` defaults
`server.protocol` to `http2` when\n`ssl.enabled: true` — a silent
breaking change from 8.x. HTTP/1.1 is\nunaffected because each request
has a dedicated `TLSSocket` that keeps\n`authorized === true` as a plain
property after the connection closes.\n\n## Test plan\n\n- [x] New
regression test in `pki.test.ts`: `does not invalidate token\nor destroy
session when socket state is unknown due to HTTP/2 stream\ndestruction`
— fails on `main`, passes after fix\n- [x] New test in `pki.test.ts`:
`does not handle requests when socket\nauthorization state is unknown
due to HTTP/2 stream destruction` —\ncovers `login` and session-less
`authenticate`, verifies correct log\nmessage\n- [x] All 40 existing PKI
unit tests continue to pass\n\nRun tests:\n```\nnode scripts/jest
--config x-pack/platform/plugins/shared/security/jest.config.js
--testPathPattern=\"authentication/providers/pki\"
--no-coverage\n```\n\n🤖 Generated with [Claude
Code](https://claude.com/claude-code)\n\n---\n\n## FTR stress test
(regression coverage for RST_STREAM)\n\n`pki.test.ts` unit tests are the
authoritative regression lock — they're\ndeterministic and always catch
a revert. An additional FTR test is\nincluded to provide end-to-end
coverage of the exact failure
mode:\n\n\n**`x-pack/platform/test/security_api_integration/tests/pki/pki_http2_stress.ts`**\nRegistered
in `pki.http2.stress.config.ts` and added
to\n`ftr_platform_stateful_configs.yml`.\n\n### Why supertest can't
cover this\n\nThe existing `pki_auth.ts` concurrent-request test (5
streams) uses\nsupertest, which speaks HTTP/1.1. RST_STREAM is an
HTTP/2-only signal —\nsupertest has no way to emit it. That is why the
bug wasn't caught by\nthe existing concurrent-auth test.\n\n### What the
FTR test does\n\n1. Establishes a PKI session via supertest (HTTP/1.1 —
simple,\nreliable).\n2. Opens a raw `http2.connect()` connection to
Kibana with the same PKI\nclient certificate.\n3. Fires 20 concurrent
slow requests against the test endpoints plugin\n(10s hold, giving
Kibana time to enter the async session-lookup window).\n4. After 150ms,
cancels all 20 streams with `RST_STREAM NGHTTP2_CANCEL`.\n5. Sends a
normal (non-cancelled) `GET /internal/security/me` on the\nsame HTTP/2
session.\n6. Asserts `200` + `username === 'first_client'` — the session
was NOT\ninvalidated.\n\nBefore the fix: at least one RST_STREAM would
destroy the stream during\nthe auth lifecycle, causing `authorized →
undefined`, which bypassed the\nguard, invalidated the ES token, and
returned 401.\nAfter the fix: all RST_STREAM frames are handled
gracefully; the session\nsurvives.\n\n### Stability note\n\nAfter the
fix is applied, the test is unconditionally stable — the guard\nchange
means RST_STREAM never reaches the token-invalidation path\nregardless
of timing. The test cannot produce a false positive (failing\nwhen the
fix is present). Before the fix it might occasionally miss the\nnarrow
timing window (false negative), but this is acceptable for a\nregression
test: it cannot regress silently if the fix is reverted,\nbecause the
unit test in `pki.test.ts` will always catch it.\n\n## Release
note\nFixes an issue where PKI authentication sessions were
unexpectedly\ninvalidated when in-flight requests were cancelled over
HTTP/2, logging\nusers out. This affects deployments on server.protocol:
http2, which\nbecame the default in 9.0.0 when TLS is
enabled.\n\n---------\n\nCo-authored-by: Claude Sonnet 4.6
<[email protected]>\nCo-authored-by: kibanamachine
<[email protected]>\nCo-authored-by: Larry
Gregory
<[email protected]>","sha":"a0520264b3fca9841b7e85f9c4b1996eb038b60e","branchLabelMapping":{"^v9.6.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","Team:Security","backport:all-open","reviewer:scout","v9.6.0"],"title":"[Security]
Fix PKI session invalidation on HTTP/2 stream
destruction","number":285153,"url":"https://github.com/elastic/kibana/pull/285153","mergeCommit":{"message":"[Security]
Fix PKI session invalidation on HTTP/2 stream destruction
(#285153)\n\n## Summary\n\nFixes #258232.\n\nPKI sessions were being
unexpectedly invalidated on Kibana 9.x when\n`server.protocol: http2`
was active (the new default when `ssl.enabled:\ntrue`). Setting
`server.protocol: http1` was the customer workaround.\n\n### Root
cause\n\nWhen an HTTP/2 stream is destroyed mid-request — by an
`AbortController`\ncancel, browser navigation abort, or any other cause
that sends\n`RST_STREAM` — Node.js clears the stream's session
reference\n(`stream.session = undefined`). The
`Http2ServerRequest.socket` proxy\nthen falls back from the TLS socket
to the stream itself, causing every\n`instanceof TLSSocket` check in
`KibanaSocket` to return `false`. The\nresult:\n\n- `socket.authorized`
→ `undefined` (not `true` or `false`)\n- `socket.getPeerCertificate()` →
`null`\n\nThe guard in `authenticateViaState` (pki.ts line ~208) was
written for\nexactly this \"connection closed\" case:\n\n```ts\n//
Before (buggy):\nif (peerCertificate === null &&
request.socket.authorized) {\n // return non-401 soft fail — session
preserved\n}\n```\n\nOn HTTP/1.1 a closed `TLSSocket` keeps `authorized
=== true`, so the\nguard fired correctly. On HTTP/2, `authorized ===
undefined` is falsy,\nso the guard was silently skipped. The code then
fell through to the\ntoken-invalidation block, revoked the ES access
token, and returned\n`notHandled()` — which `authenticate()` converted
to\n`Boom.unauthorized()`. `updateSessionValue()` saw the 401 on an
owned\nsession and **deleted the server-side session**, logging the user
out.\n\n### Fix\n\nChange the guard from a truthy check to `!==
false`:\n\n```ts\n// After (fixed):\nif (peerCertificate === null &&
request.socket.authorized !== false) {\n // return non-401 soft fail —
session preserved\n}\n```\n\n`false` = cert was presented but rejected →
token invalidation is\ncorrect.\n`undefined` = socket state is
unknowable (HTTP/2 stream destroyed) →\ntreat same as `true`, preserve
session.\n`true` = socket explicitly authorized → original behaviour
unchanged.\n\nAlso improves the diagnostic log in
`authenticateViaPeerCertificate` to\ndistinguish `false` (rejected cert)
from `undefined` (unknown state),\nmaking production logs
actionable.\n\n### Why this is new in 9.x\n\n`http_config.ts` defaults
`server.protocol` to `http2` when\n`ssl.enabled: true` — a silent
breaking change from 8.x. HTTP/1.1 is\nunaffected because each request
has a dedicated `TLSSocket` that keeps\n`authorized === true` as a plain
property after the connection closes.\n\n## Test plan\n\n- [x] New
regression test in `pki.test.ts`: `does not invalidate token\nor destroy
session when socket state is unknown due to HTTP/2 stream\ndestruction`
— fails on `main`, passes after fix\n- [x] New test in `pki.test.ts`:
`does not handle requests when socket\nauthorization state is unknown
due to HTTP/2 stream destruction` —\ncovers `login` and session-less
`authenticate`, verifies correct log\nmessage\n- [x] All 40 existing PKI
unit tests continue to pass\n\nRun tests:\n```\nnode scripts/jest
--config x-pack/platform/plugins/shared/security/jest.config.js
--testPathPattern=\"authentication/providers/pki\"
--no-coverage\n```\n\n🤖 Generated with [Claude
Code](https://claude.com/claude-code)\n\n---\n\n## FTR stress test
(regression coverage for RST_STREAM)\n\n`pki.test.ts` unit tests are the
authoritative regression lock — they're\ndeterministic and always catch
a revert. An additional FTR test is\nincluded to provide end-to-end
coverage of the exact failure
mode:\n\n\n**`x-pack/platform/test/security_api_integration/tests/pki/pki_http2_stress.ts`**\nRegistered
in `pki.http2.stress.config.ts` and added
to\n`ftr_platform_stateful_configs.yml`.\n\n### Why supertest can't
cover this\n\nThe existing `pki_auth.ts` concurrent-request test (5
streams) uses\nsupertest, which speaks HTTP/1.1. RST_STREAM is an
HTTP/2-only signal —\nsupertest has no way to emit it. That is why the
bug wasn't caught by\nthe existing concurrent-auth test.\n\n### What the
FTR test does\n\n1. Establishes a PKI session via supertest (HTTP/1.1 —
simple,\nreliable).\n2. Opens a raw `http2.connect()` connection to
Kibana with the same PKI\nclient certificate.\n3. Fires 20 concurrent
slow requests against the test endpoints plugin\n(10s hold, giving
Kibana time to enter the async session-lookup window).\n4. After 150ms,
cancels all 20 streams with `RST_STREAM NGHTTP2_CANCEL`.\n5. Sends a
normal (non-cancelled) `GET /internal/security/me` on the\nsame HTTP/2
session.\n6. Asserts `200` + `username === 'first_client'` — the session
was NOT\ninvalidated.\n\nBefore the fix: at least one RST_STREAM would
destroy the stream during\nthe auth lifecycle, causing `authorized →
undefined`, which bypassed the\nguard, invalidated the ES token, and
returned 401.\nAfter the fix: all RST_STREAM frames are handled
gracefully; the session\nsurvives.\n\n### Stability note\n\nAfter the
fix is applied, the test is unconditionally stable — the guard\nchange
means RST_STREAM never reaches the token-invalidation path\nregardless
of timing. The test cannot produce a false positive (failing\nwhen the
fix is present). Before the fix it might occasionally miss the\nnarrow
timing window (false negative), but this is acceptable for a\nregression
test: it cannot regress silently if the fix is reverted,\nbecause the
unit test in `pki.test.ts` will always catch it.\n\n## Release
note\nFixes an issue where PKI authentication sessions were
unexpectedly\ninvalidated when in-flight requests were cancelled over
HTTP/2, logging\nusers out. This affects deployments on server.protocol:
http2, which\nbecame the default in 9.0.0 when TLS is
enabled.\n\n---------\n\nCo-authored-by: Claude Sonnet 4.6
<[email protected]>\nCo-authored-by: kibanamachine
<[email protected]>\nCo-authored-by: Larry
Gregory
<[email protected]>","sha":"a0520264b3fca9841b7e85f9c4b1996eb038b60e"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.6.0","branchLabelMappingKey":"^v9.6.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/285153","number":285153,"mergeCommit":{"message":"[Security]
Fix PKI session invalidation on HTTP/2 stream destruction
(#285153)\n\n## Summary\n\nFixes #258232.\n\nPKI sessions were being
unexpectedly invalidated on Kibana 9.x when\n`server.protocol: http2`
was active (the new default when `ssl.enabled:\ntrue`). Setting
`server.protocol: http1` was the customer workaround.\n\n### Root
cause\n\nWhen an HTTP/2 stream is destroyed mid-request — by an
`AbortController`\ncancel, browser navigation abort, or any other cause
that sends\n`RST_STREAM` — Node.js clears the stream's session
reference\n(`stream.session = undefined`). The
`Http2ServerRequest.socket` proxy\nthen falls back from the TLS socket
to the stream itself, causing every\n`instanceof TLSSocket` check in
`KibanaSocket` to return `false`. The\nresult:\n\n- `socket.authorized`
→ `undefined` (not `true` or `false`)\n- `socket.getPeerCertificate()` →
`null`\n\nThe guard in `authenticateViaState` (pki.ts line ~208) was
written for\nexactly this \"connection closed\" case:\n\n```ts\n//
Before (buggy):\nif (peerCertificate === null &&
request.socket.authorized) {\n // return non-401 soft fail — session
preserved\n}\n```\n\nOn HTTP/1.1 a closed `TLSSocket` keeps `authorized
=== true`, so the\nguard fired correctly. On HTTP/2, `authorized ===
undefined` is falsy,\nso the guard was silently skipped. The code then
fell through to the\ntoken-invalidation block, revoked the ES access
token, and returned\n`notHandled()` — which `authenticate()` converted
to\n`Boom.unauthorized()`. `updateSessionValue()` saw the 401 on an
owned\nsession and **deleted the server-side session**, logging the user
out.\n\n### Fix\n\nChange the guard from a truthy check to `!==
false`:\n\n```ts\n// After (fixed):\nif (peerCertificate === null &&
request.socket.authorized !== false) {\n // return non-401 soft fail —
session preserved\n}\n```\n\n`false` = cert was presented but rejected →
token invalidation is\ncorrect.\n`undefined` = socket state is
unknowable (HTTP/2 stream destroyed) →\ntreat same as `true`, preserve
session.\n`true` = socket explicitly authorized → original behaviour
unchanged.\n\nAlso improves the diagnostic log in
`authenticateViaPeerCertificate` to\ndistinguish `false` (rejected cert)
from `undefined` (unknown state),\nmaking production logs
actionable.\n\n### Why this is new in 9.x\n\n`http_config.ts` defaults
`server.protocol` to `http2` when\n`ssl.enabled: true` — a silent
breaking change from 8.x. HTTP/1.1 is\nunaffected because each request
has a dedicated `TLSSocket` that keeps\n`authorized === true` as a plain
property after the connection closes.\n\n## Test plan\n\n- [x] New
regression test in `pki.test.ts`: `does not invalidate token\nor destroy
session when socket state is unknown due to HTTP/2 stream\ndestruction`
— fails on `main`, passes after fix\n- [x] New test in `pki.test.ts`:
`does not handle requests when socket\nauthorization state is unknown
due to HTTP/2 stream destruction` —\ncovers `login` and session-less
`authenticate`, verifies correct log\nmessage\n- [x] All 40 existing PKI
unit tests continue to pass\n\nRun tests:\n```\nnode scripts/jest
--config x-pack/platform/plugins/shared/security/jest.config.js
--testPathPattern=\"authentication/providers/pki\"
--no-coverage\n```\n\n🤖 Generated with [Claude
Code](https://claude.com/claude-code)\n\n---\n\n## FTR stress test
(regression coverage for RST_STREAM)\n\n`pki.test.ts` unit tests are the
authoritative regression lock — they're\ndeterministic and always catch
a revert. An additional FTR test is\nincluded to provide end-to-end
coverage of the exact failure
mode:\n\n\n**`x-pack/platform/test/security_api_integration/tests/pki/pki_http2_stress.ts`**\nRegistered
in `pki.http2.stress.config.ts` and added
to\n`ftr_platform_stateful_configs.yml`.\n\n### Why supertest can't
cover this\n\nThe existing `pki_auth.ts` concurrent-request test (5
streams) uses\nsupertest, which speaks HTTP/1.1. RST_STREAM is an
HTTP/2-only signal —\nsupertest has no way to emit it. That is why the
bug wasn't caught by\nthe existing concurrent-auth test.\n\n### What the
FTR test does\n\n1. Establishes a PKI session via supertest (HTTP/1.1 —
simple,\nreliable).\n2. Opens a raw `http2.connect()` connection to
Kibana with the same PKI\nclient certificate.\n3. Fires 20 concurrent
slow requests against the test endpoints plugin\n(10s hold, giving
Kibana time to enter the async session-lookup window).\n4. After 150ms,
cancels all 20 streams with `RST_STREAM NGHTTP2_CANCEL`.\n5. Sends a
normal (non-cancelled) `GET /internal/security/me` on the\nsame HTTP/2
session.\n6. Asserts `200` + `username === 'first_client'` — the session
was NOT\ninvalidated.\n\nBefore the fix: at least one RST_STREAM would
destroy the stream during\nthe auth lifecycle, causing `authorized →
undefined`, which bypassed the\nguard, invalidated the ES token, and
returned 401.\nAfter the fix: all RST_STREAM frames are handled
gracefully; the session\nsurvives.\n\n### Stability note\n\nAfter the
fix is applied, the test is unconditionally stable — the guard\nchange
means RST_STREAM never reaches the token-invalidation path\nregardless
of timing. The test cannot produce a false positive (failing\nwhen the
fix is present). Before the fix it might occasionally miss the\nnarrow
timing window (false negative), but this is acceptable for a\nregression
test: it cannot regress silently if the fix is reverted,\nbecause the
unit test in `pki.test.ts` will always catch it.\n\n## Release
note\nFixes an issue where PKI authentication sessions were
unexpectedly\ninvalidated when in-flight requests were cancelled over
HTTP/2, logging\nusers out. This affects deployments on server.protocol:
http2, which\nbecame the default in 9.0.0 when TLS is
enabled.\n\n---------\n\nCo-authored-by: Claude Sonnet 4.6
<[email protected]>\nCo-authored-by: kibanamachine
<[email protected]>\nCo-authored-by: Larry
Gregory
<[email protected]>","sha":"a0520264b3fca9841b7e85f9c4b1996eb038b60e"}}]}]
BACKPORT-->

Co-authored-by: Jeramy Soucy <[email protected]>
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
Co-authored-by: Larry Gregory <[email protected]>
@kibanamachine kibanamachine added v9.4.6 backport missing Added to PRs automatically when the are determined to be missing a backport. labels Aug 25, 2026
@kibanamachine

Copy link
Copy Markdown
Contributor

Looks like this PR has backport PRs but they still haven't been merged. Please merge them ASAP to keep the branches relatively in sync.
cc: @jeramysoucy

jeramysoucy added a commit to jeramysoucy/kibana that referenced this pull request Aug 26, 2026
…astic#285153)

Scout's HTTP/2 server config (`http2: true`, elastic#274000) was not backported to
8.19, so the `scout_pki_stress` suite is unbuildable on this branch. Drop the
7 Scout files and replace them with a direct FTR port of the same deterministic
pre-auth-hold test:

- `tests/pki/pki_http2_stress.ts` — parks a request inside
  PKIAuthenticationProvider.authenticate via the test_endpoints pre-auth hold,
  RSTs the HTTP/2 stream, confirms socket degradation, releases the hold, and
  asserts the session survives the follow-up request. Same four-step logic as
  the Scout spec; only the harness glue differs (retry.tryForTime/tough-cookie
  vs. expect.poll/findSessionCookie).
- `pki.http2.stress.config.ts` — extends pki.config with configureHTTP2 and
  debug-level security logging.
- `ftr_platform_stateful_configs.yml` — registers the new config.

`init_routes.ts` (+230 lines, pre-auth hold machinery) is retained; it is what
makes the test deterministic. The production fix in pki.ts is unchanged.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
jeramysoucy added a commit to jeramysoucy/kibana that referenced this pull request Aug 26, 2026
… with plain state

The cherry-pick of elastic#285153 brought main's test code verbatim, including a
call to sessionMock.createValue(). On main, pki.ts authenticate() takes
SessionValue<ProviderState>; on 8.19 it still takes ProviderState directly.
The sessionMock import was also missing from the cherry-pick, so tsc
reported: "Cannot find name 'sessionMock'" (TS2304).

Replace sessionMock.createValue({ state: ... }) with a plain ProviderState
object, matching the pattern used by all other authenticateViaState tests in
this file on 8.19.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
jeramysoucy added a commit that referenced this pull request Aug 27, 2026
…uction (#285153) (#286791)

# Backport

This will backport the following commits from `main` to `8.19`:
- [[Security] Fix PKI session invalidation on HTTP/2 stream destruction
(#285153)](#285153)

<!--- Backport version: 11.0.2 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Jeramy
Soucy","email":"[email protected]"},"sourceCommit":{"committedDate":"2026-08-24T09:23:24Z","message":"[Security]
Fix PKI session invalidation on HTTP/2 stream destruction
(#285153)\n\n## Summary\n\nFixes #258232.\n\nPKI sessions were being
unexpectedly invalidated on Kibana 9.x when\n`server.protocol: http2`
was active (the new default when `ssl.enabled:\ntrue`). Setting
`server.protocol: http1` was the customer workaround.\n\n### Root
cause\n\nWhen an HTTP/2 stream is destroyed mid-request — by an
`AbortController`\ncancel, browser navigation abort, or any other cause
that sends\n`RST_STREAM` — Node.js clears the stream's session
reference\n(`stream.session = undefined`). The
`Http2ServerRequest.socket` proxy\nthen falls back from the TLS socket
to the stream itself, causing every\n`instanceof TLSSocket` check in
`KibanaSocket` to return `false`. The\nresult:\n\n- `socket.authorized`
→ `undefined` (not `true` or `false`)\n- `socket.getPeerCertificate()` →
`null`\n\nThe guard in `authenticateViaState` (pki.ts line ~208) was
written for\nexactly this \"connection closed\" case:\n\n```ts\n//
Before (buggy):\nif (peerCertificate === null &&
request.socket.authorized) {\n // return non-401 soft fail — session
preserved\n}\n```\n\nOn HTTP/1.1 a closed `TLSSocket` keeps `authorized
=== true`, so the\nguard fired correctly. On HTTP/2, `authorized ===
undefined` is falsy,\nso the guard was silently skipped. The code then
fell through to the\ntoken-invalidation block, revoked the ES access
token, and returned\n`notHandled()` — which `authenticate()` converted
to\n`Boom.unauthorized()`. `updateSessionValue()` saw the 401 on an
owned\nsession and **deleted the server-side session**, logging the user
out.\n\n### Fix\n\nChange the guard from a truthy check to `!==
false`:\n\n```ts\n// After (fixed):\nif (peerCertificate === null &&
request.socket.authorized !== false) {\n // return non-401 soft fail —
session preserved\n}\n```\n\n`false` = cert was presented but rejected →
token invalidation is\ncorrect.\n`undefined` = socket state is
unknowable (HTTP/2 stream destroyed) →\ntreat same as `true`, preserve
session.\n`true` = socket explicitly authorized → original behaviour
unchanged.\n\nAlso improves the diagnostic log in
`authenticateViaPeerCertificate` to\ndistinguish `false` (rejected cert)
from `undefined` (unknown state),\nmaking production logs
actionable.\n\n### Why this is new in 9.x\n\n`http_config.ts` defaults
`server.protocol` to `http2` when\n`ssl.enabled: true` — a silent
breaking change from 8.x. HTTP/1.1 is\nunaffected because each request
has a dedicated `TLSSocket` that keeps\n`authorized === true` as a plain
property after the connection closes.\n\n## Test plan\n\n- [x] New
regression test in `pki.test.ts`: `does not invalidate token\nor destroy
session when socket state is unknown due to HTTP/2 stream\ndestruction`
— fails on `main`, passes after fix\n- [x] New test in `pki.test.ts`:
`does not handle requests when socket\nauthorization state is unknown
due to HTTP/2 stream destruction` —\ncovers `login` and session-less
`authenticate`, verifies correct log\nmessage\n- [x] All 40 existing PKI
unit tests continue to pass\n\nRun tests:\n```\nnode scripts/jest
--config x-pack/platform/plugins/shared/security/jest.config.js
--testPathPattern=\"authentication/providers/pki\"
--no-coverage\n```\n\n🤖 Generated with [Claude
Code](https://claude.com/claude-code)\n\n---\n\n## FTR stress test
(regression coverage for RST_STREAM)\n\n`pki.test.ts` unit tests are the
authoritative regression lock — they're\ndeterministic and always catch
a revert. An additional FTR test is\nincluded to provide end-to-end
coverage of the exact failure
mode:\n\n\n**`x-pack/platform/test/security_api_integration/tests/pki/pki_http2_stress.ts`**\nRegistered
in `pki.http2.stress.config.ts` and added
to\n`ftr_platform_stateful_configs.yml`.\n\n### Why supertest can't
cover this\n\nThe existing `pki_auth.ts` concurrent-request test (5
streams) uses\nsupertest, which speaks HTTP/1.1. RST_STREAM is an
HTTP/2-only signal —\nsupertest has no way to emit it. That is why the
bug wasn't caught by\nthe existing concurrent-auth test.\n\n### What the
FTR test does\n\n1. Establishes a PKI session via supertest (HTTP/1.1 —
simple,\nreliable).\n2. Opens a raw `http2.connect()` connection to
Kibana with the same PKI\nclient certificate.\n3. Fires 20 concurrent
slow requests against the test endpoints plugin\n(10s hold, giving
Kibana time to enter the async session-lookup window).\n4. After 150ms,
cancels all 20 streams with `RST_STREAM NGHTTP2_CANCEL`.\n5. Sends a
normal (non-cancelled) `GET /internal/security/me` on the\nsame HTTP/2
session.\n6. Asserts `200` + `username === 'first_client'` — the session
was NOT\ninvalidated.\n\nBefore the fix: at least one RST_STREAM would
destroy the stream during\nthe auth lifecycle, causing `authorized →
undefined`, which bypassed the\nguard, invalidated the ES token, and
returned 401.\nAfter the fix: all RST_STREAM frames are handled
gracefully; the session\nsurvives.\n\n### Stability note\n\nAfter the
fix is applied, the test is unconditionally stable — the guard\nchange
means RST_STREAM never reaches the token-invalidation path\nregardless
of timing. The test cannot produce a false positive (failing\nwhen the
fix is present). Before the fix it might occasionally miss the\nnarrow
timing window (false negative), but this is acceptable for a\nregression
test: it cannot regress silently if the fix is reverted,\nbecause the
unit test in `pki.test.ts` will always catch it.\n\n## Release
note\nFixes an issue where PKI authentication sessions were
unexpectedly\ninvalidated when in-flight requests were cancelled over
HTTP/2, logging\nusers out. This affects deployments on server.protocol:
http2, which\nbecame the default in 9.0.0 when TLS is
enabled.\n\n---------\n\nCo-authored-by: Claude Sonnet 4.6
<[email protected]>\nCo-authored-by: kibanamachine
<[email protected]>\nCo-authored-by: Larry
Gregory
<[email protected]>","sha":"a0520264b3fca9841b7e85f9c4b1996eb038b60e","branchLabelMapping":{"^v9.6.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","Team:Security","backport:all-open","reviewer:scout","v9.6.0"],"title":"[Security]
Fix PKI session invalidation on HTTP/2 stream
destruction","number":285153,"url":"https://github.com/elastic/kibana/pull/285153","mergeCommit":{"message":"[Security]
Fix PKI session invalidation on HTTP/2 stream destruction
(#285153)\n\n## Summary\n\nFixes #258232.\n\nPKI sessions were being
unexpectedly invalidated on Kibana 9.x when\n`server.protocol: http2`
was active (the new default when `ssl.enabled:\ntrue`). Setting
`server.protocol: http1` was the customer workaround.\n\n### Root
cause\n\nWhen an HTTP/2 stream is destroyed mid-request — by an
`AbortController`\ncancel, browser navigation abort, or any other cause
that sends\n`RST_STREAM` — Node.js clears the stream's session
reference\n(`stream.session = undefined`). The
`Http2ServerRequest.socket` proxy\nthen falls back from the TLS socket
to the stream itself, causing every\n`instanceof TLSSocket` check in
`KibanaSocket` to return `false`. The\nresult:\n\n- `socket.authorized`
→ `undefined` (not `true` or `false`)\n- `socket.getPeerCertificate()` →
`null`\n\nThe guard in `authenticateViaState` (pki.ts line ~208) was
written for\nexactly this \"connection closed\" case:\n\n```ts\n//
Before (buggy):\nif (peerCertificate === null &&
request.socket.authorized) {\n // return non-401 soft fail — session
preserved\n}\n```\n\nOn HTTP/1.1 a closed `TLSSocket` keeps `authorized
=== true`, so the\nguard fired correctly. On HTTP/2, `authorized ===
undefined` is falsy,\nso the guard was silently skipped. The code then
fell through to the\ntoken-invalidation block, revoked the ES access
token, and returned\n`notHandled()` — which `authenticate()` converted
to\n`Boom.unauthorized()`. `updateSessionValue()` saw the 401 on an
owned\nsession and **deleted the server-side session**, logging the user
out.\n\n### Fix\n\nChange the guard from a truthy check to `!==
false`:\n\n```ts\n// After (fixed):\nif (peerCertificate === null &&
request.socket.authorized !== false) {\n // return non-401 soft fail —
session preserved\n}\n```\n\n`false` = cert was presented but rejected →
token invalidation is\ncorrect.\n`undefined` = socket state is
unknowable (HTTP/2 stream destroyed) →\ntreat same as `true`, preserve
session.\n`true` = socket explicitly authorized → original behaviour
unchanged.\n\nAlso improves the diagnostic log in
`authenticateViaPeerCertificate` to\ndistinguish `false` (rejected cert)
from `undefined` (unknown state),\nmaking production logs
actionable.\n\n### Why this is new in 9.x\n\n`http_config.ts` defaults
`server.protocol` to `http2` when\n`ssl.enabled: true` — a silent
breaking change from 8.x. HTTP/1.1 is\nunaffected because each request
has a dedicated `TLSSocket` that keeps\n`authorized === true` as a plain
property after the connection closes.\n\n## Test plan\n\n- [x] New
regression test in `pki.test.ts`: `does not invalidate token\nor destroy
session when socket state is unknown due to HTTP/2 stream\ndestruction`
— fails on `main`, passes after fix\n- [x] New test in `pki.test.ts`:
`does not handle requests when socket\nauthorization state is unknown
due to HTTP/2 stream destruction` —\ncovers `login` and session-less
`authenticate`, verifies correct log\nmessage\n- [x] All 40 existing PKI
unit tests continue to pass\n\nRun tests:\n```\nnode scripts/jest
--config x-pack/platform/plugins/shared/security/jest.config.js
--testPathPattern=\"authentication/providers/pki\"
--no-coverage\n```\n\n🤖 Generated with [Claude
Code](https://claude.com/claude-code)\n\n---\n\n## FTR stress test
(regression coverage for RST_STREAM)\n\n`pki.test.ts` unit tests are the
authoritative regression lock — they're\ndeterministic and always catch
a revert. An additional FTR test is\nincluded to provide end-to-end
coverage of the exact failure
mode:\n\n\n**`x-pack/platform/test/security_api_integration/tests/pki/pki_http2_stress.ts`**\nRegistered
in `pki.http2.stress.config.ts` and added
to\n`ftr_platform_stateful_configs.yml`.\n\n### Why supertest can't
cover this\n\nThe existing `pki_auth.ts` concurrent-request test (5
streams) uses\nsupertest, which speaks HTTP/1.1. RST_STREAM is an
HTTP/2-only signal —\nsupertest has no way to emit it. That is why the
bug wasn't caught by\nthe existing concurrent-auth test.\n\n### What the
FTR test does\n\n1. Establishes a PKI session via supertest (HTTP/1.1 —
simple,\nreliable).\n2. Opens a raw `http2.connect()` connection to
Kibana with the same PKI\nclient certificate.\n3. Fires 20 concurrent
slow requests against the test endpoints plugin\n(10s hold, giving
Kibana time to enter the async session-lookup window).\n4. After 150ms,
cancels all 20 streams with `RST_STREAM NGHTTP2_CANCEL`.\n5. Sends a
normal (non-cancelled) `GET /internal/security/me` on the\nsame HTTP/2
session.\n6. Asserts `200` + `username === 'first_client'` — the session
was NOT\ninvalidated.\n\nBefore the fix: at least one RST_STREAM would
destroy the stream during\nthe auth lifecycle, causing `authorized →
undefined`, which bypassed the\nguard, invalidated the ES token, and
returned 401.\nAfter the fix: all RST_STREAM frames are handled
gracefully; the session\nsurvives.\n\n### Stability note\n\nAfter the
fix is applied, the test is unconditionally stable — the guard\nchange
means RST_STREAM never reaches the token-invalidation path\nregardless
of timing. The test cannot produce a false positive (failing\nwhen the
fix is present). Before the fix it might occasionally miss the\nnarrow
timing window (false negative), but this is acceptable for a\nregression
test: it cannot regress silently if the fix is reverted,\nbecause the
unit test in `pki.test.ts` will always catch it.\n\n## Release
note\nFixes an issue where PKI authentication sessions were
unexpectedly\ninvalidated when in-flight requests were cancelled over
HTTP/2, logging\nusers out. This affects deployments on server.protocol:
http2, which\nbecame the default in 9.0.0 when TLS is
enabled.\n\n---------\n\nCo-authored-by: Claude Sonnet 4.6
<[email protected]>\nCo-authored-by: kibanamachine
<[email protected]>\nCo-authored-by: Larry
Gregory
<[email protected]>","sha":"a0520264b3fca9841b7e85f9c4b1996eb038b60e"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.6.0","branchLabelMappingKey":"^v9.6.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/285153","number":285153,"mergeCommit":{"message":"[Security]
Fix PKI session invalidation on HTTP/2 stream destruction
(#285153)\n\n## Summary\n\nFixes #258232.\n\nPKI sessions were being
unexpectedly invalidated on Kibana 9.x when\n`server.protocol: http2`
was active (the new default when `ssl.enabled:\ntrue`). Setting
`server.protocol: http1` was the customer workaround.\n\n### Root
cause\n\nWhen an HTTP/2 stream is destroyed mid-request — by an
`AbortController`\ncancel, browser navigation abort, or any other cause
that sends\n`RST_STREAM` — Node.js clears the stream's session
reference\n(`stream.session = undefined`). The
`Http2ServerRequest.socket` proxy\nthen falls back from the TLS socket
to the stream itself, causing every\n`instanceof TLSSocket` check in
`KibanaSocket` to return `false`. The\nresult:\n\n- `socket.authorized`
→ `undefined` (not `true` or `false`)\n- `socket.getPeerCertificate()` →
`null`\n\nThe guard in `authenticateViaState` (pki.ts line ~208) was
written for\nexactly this \"connection closed\" case:\n\n```ts\n//
Before (buggy):\nif (peerCertificate === null &&
request.socket.authorized) {\n // return non-401 soft fail — session
preserved\n}\n```\n\nOn HTTP/1.1 a closed `TLSSocket` keeps `authorized
=== true`, so the\nguard fired correctly. On HTTP/2, `authorized ===
undefined` is falsy,\nso the guard was silently skipped. The code then
fell through to the\ntoken-invalidation block, revoked the ES access
token, and returned\n`notHandled()` — which `authenticate()` converted
to\n`Boom.unauthorized()`. `updateSessionValue()` saw the 401 on an
owned\nsession and **deleted the server-side session**, logging the user
out.\n\n### Fix\n\nChange the guard from a truthy check to `!==
false`:\n\n```ts\n// After (fixed):\nif (peerCertificate === null &&
request.socket.authorized !== false) {\n // return non-401 soft fail —
session preserved\n}\n```\n\n`false` = cert was presented but rejected →
token invalidation is\ncorrect.\n`undefined` = socket state is
unknowable (HTTP/2 stream destroyed) →\ntreat same as `true`, preserve
session.\n`true` = socket explicitly authorized → original behaviour
unchanged.\n\nAlso improves the diagnostic log in
`authenticateViaPeerCertificate` to\ndistinguish `false` (rejected cert)
from `undefined` (unknown state),\nmaking production logs
actionable.\n\n### Why this is new in 9.x\n\n`http_config.ts` defaults
`server.protocol` to `http2` when\n`ssl.enabled: true` — a silent
breaking change from 8.x. HTTP/1.1 is\nunaffected because each request
has a dedicated `TLSSocket` that keeps\n`authorized === true` as a plain
property after the connection closes.\n\n## Test plan\n\n- [x] New
regression test in `pki.test.ts`: `does not invalidate token\nor destroy
session when socket state is unknown due to HTTP/2 stream\ndestruction`
— fails on `main`, passes after fix\n- [x] New test in `pki.test.ts`:
`does not handle requests when socket\nauthorization state is unknown
due to HTTP/2 stream destruction` —\ncovers `login` and session-less
`authenticate`, verifies correct log\nmessage\n- [x] All 40 existing PKI
unit tests continue to pass\n\nRun tests:\n```\nnode scripts/jest
--config x-pack/platform/plugins/shared/security/jest.config.js
--testPathPattern=\"authentication/providers/pki\"
--no-coverage\n```\n\n🤖 Generated with [Claude
Code](https://claude.com/claude-code)\n\n---\n\n## FTR stress test
(regression coverage for RST_STREAM)\n\n`pki.test.ts` unit tests are the
authoritative regression lock — they're\ndeterministic and always catch
a revert. An additional FTR test is\nincluded to provide end-to-end
coverage of the exact failure
mode:\n\n\n**`x-pack/platform/test/security_api_integration/tests/pki/pki_http2_stress.ts`**\nRegistered
in `pki.http2.stress.config.ts` and added
to\n`ftr_platform_stateful_configs.yml`.\n\n### Why supertest can't
cover this\n\nThe existing `pki_auth.ts` concurrent-request test (5
streams) uses\nsupertest, which speaks HTTP/1.1. RST_STREAM is an
HTTP/2-only signal —\nsupertest has no way to emit it. That is why the
bug wasn't caught by\nthe existing concurrent-auth test.\n\n### What the
FTR test does\n\n1. Establishes a PKI session via supertest (HTTP/1.1 —
simple,\nreliable).\n2. Opens a raw `http2.connect()` connection to
Kibana with the same PKI\nclient certificate.\n3. Fires 20 concurrent
slow requests against the test endpoints plugin\n(10s hold, giving
Kibana time to enter the async session-lookup window).\n4. After 150ms,
cancels all 20 streams with `RST_STREAM NGHTTP2_CANCEL`.\n5. Sends a
normal (non-cancelled) `GET /internal/security/me` on the\nsame HTTP/2
session.\n6. Asserts `200` + `username === 'first_client'` — the session
was NOT\ninvalidated.\n\nBefore the fix: at least one RST_STREAM would
destroy the stream during\nthe auth lifecycle, causing `authorized →
undefined`, which bypassed the\nguard, invalidated the ES token, and
returned 401.\nAfter the fix: all RST_STREAM frames are handled
gracefully; the session\nsurvives.\n\n### Stability note\n\nAfter the
fix is applied, the test is unconditionally stable — the guard\nchange
means RST_STREAM never reaches the token-invalidation path\nregardless
of timing. The test cannot produce a false positive (failing\nwhen the
fix is present). Before the fix it might occasionally miss the\nnarrow
timing window (false negative), but this is acceptable for a\nregression
test: it cannot regress silently if the fix is reverted,\nbecause the
unit test in `pki.test.ts` will always catch it.\n\n## Release
note\nFixes an issue where PKI authentication sessions were
unexpectedly\ninvalidated when in-flight requests were cancelled over
HTTP/2, logging\nusers out. This affects deployments on server.protocol:
http2, which\nbecame the default in 9.0.0 when TLS is
enabled.\n\n---------\n\nCo-authored-by: Claude Sonnet 4.6
<[email protected]>\nCo-authored-by: kibanamachine
<[email protected]>\nCo-authored-by: Larry
Gregory
<[email protected]>","sha":"a0520264b3fca9841b7e85f9c4b1996eb038b60e"}},{"url":"https://github.com/elastic/kibana/pull/286786","number":286786,"branch":"9.4","state":"OPEN"},{"url":"https://github.com/elastic/kibana/pull/286787","number":286787,"branch":"9.5","state":"OPEN"}]}]
BACKPORT-->

---

## 8.19 backport adjustments

### Scout test suite replaced with FTR equivalent

On `main`, the regression suite lives in
`x-pack/platform/plugins/shared/security/test/scout_pki_stress/` and
uses Scout's HTTP/2 server
configuration (`http2: true` in the server config, landed in #274000).
That Scout HTTP/2 plumbing
was not backported to 8.19 (`backport:skip` on #274000; backporting it
would require ~19 files of
shared Scout code). PR #286876 (the Scout HTTP/2 backport) is being
closed as a result.

The Scout files have been dropped from this backport. The test coverage
is preserved via a direct
FTR port of the same deterministic test:


**`x-pack/platform/test/security_api_integration/tests/pki/pki_http2_stress.ts`**

The test logic is identical to the Scout spec — same four steps (login /
park inside
`PKIAuthenticationProvider.authenticate` via the pre-auth hold / RST
until socket is confirmed
degraded / release and follow-up). Only the harness glue differs
(`retry.tryForTime` replaces
`expect.poll`; `tough-cookie` `parseCookie` replaces `findSessionCookie`
whose root barrel is absent
on 8.19). The pre-auth hold infrastructure in `init_routes.ts` is
retained; it is what makes the
test deterministic.

Note: the `main` PR description's "FTR stress test" section describes a
timing-based design
(commit `3d344b8d46c5`, which fired 20 slow requests and RST'd them
after 150ms). That commit was
removed in `24d55c0727ac` in favour of the Scout API test and the
description was never updated. The
FTR test in this 8.19 backport uses the deterministic pre-auth-hold
design, not the timing-based
one.

`pki.http2.stress.config.ts` registered in
`ftr_platform_stateful_configs.yml`.

---------

Co-authored-by: Claude Sonnet 4.6 <[email protected]>
Co-authored-by: kibanamachine <[email protected]>
@kibanamachine kibanamachine added v8.19.21 and removed backport missing Added to PRs automatically when the are determined to be missing a backport. labels Aug 27, 2026
jeramysoucy added a commit that referenced this pull request Aug 28, 2026
…ests (#285344)

## Summary

Fixes the root cause of #258232
— PKI sessions being invalidated after upgrading to Kibana 9.x, where
`server.protocol` silently defaults to `http2` when `ssl.enabled: true`.

This is the companion to #285153, which fixes the symptom in `pki.ts`.
This PR fixes the underlying socket resolution in Core HTTP.

## Problem

For HTTP/2 requests, `Http2ServerRequest.socket` returns a `Proxy` over
the **stream**, not the session's `TLSSocket`. The load-bearing traps
branch on `stream[kSession]`:

```js
// Node.js lib/internal/http2/compat.js
getPrototypeOf(stream) {
  if (stream.session !== undefined) return ReflectGetPrototypeOf(stream.session[kSocket]);
  return ReflectGetPrototypeOf(stream);   // <-- Http2Stream, NOT TLSSocket
}
```

Node clears `stream[kSession]` whenever the stream is destroyed —
RST_STREAM, GOAWAY, client abort, timeout. After that point the proxy
resolves against the `Http2Stream` itself, so every `instanceof
TLSSocket` check in `KibanaSocket` fails and the accessors degrade:

| accessor | value after stream destruction |
|---|---|
| `authorized` | `undefined` |
| `authorizationError` | `undefined` |
| `getPeerCertificate(true)` | `null` |
| `getProtocol()` | `null` |
| `remoteAddress` | `undefined` |

This happens on **live, authorized connections** — the TLS handshake
succeeded and the client certificate is valid, but the stream-level
proxy can no longer report it.

Kibana's own frontend aborts in-flight requests constantly via
`AbortController` (search sessions, unified search, autocomplete). On
HTTP/1.1 an abort closes a dedicated socket; on HTTP/2 it emits
RST_STREAM on a **shared** session mid-flight, so one cancelled stream
degrades the socket view for concurrent requests.

## Fix

Resolve the **session-level** socket (`req.stream.session.socket`) at
`KibanaSocket` construction time instead of using the stream-level
proxy. The session socket resolves against `session[kSocket]`, is a real
`TLSSocket`, and remains stable for the lifetime of the TCP connection.

This is semantically correct: in HTTP/2 the client certificate is a
property of the **connection**, not of an individual stream.

Falls back to `req.socket` when the session socket is unavailable —
either an HTTP/1.1 request (no `stream` property) or a fully destroyed
session (`session.socket` throws `ERR_HTTP2_SOCKET_UNBOUND`).

## Secondary fix

`audit_service.ts` reads `request.socket.remoteAddress` for audit log
client IP enrichment. On destroyed HTTP/2 streams this silently yielded
`undefined`, losing the client IP in audit records for **all** auth
providers. This patch fixes that too, since the session socket carries a
stable `remoteAddress`.

## Testing

Four new unit tests in `socket.test.ts` covering `resolveRawSocket`:
- HTTP/1.1 request (no `stream` property) → returns `req.socket`
- HTTP/2 request → returns the session-level socket
- `stream.session` undefined → falls back to `req.socket`
- `session.socket` throws `ERR_HTTP2_SOCKET_UNBOUND` → falls back to
`req.socket`

```
node scripts/jest src/core/packages/http/router-server-internal/src/socket.test.ts
# 20 passed, 20 total
```

## Relationship to #285153

- **#285153** (`pki.ts`) — defensive guard so an unknowable socket state
(`authorized === undefined`) is not treated as an explicit cert
rejection. Narrow, safe to backport.
- **This PR** (Core HTTP) — removes the condition that produces the
unknowable state in the first place.

Both are worth having. The `pki.ts` guard remains correct
defense-in-depth for the fully-destroyed-session case, where even the
session socket is unavailable.

## Risk

Touches socket resolution for all Core HTTP requests, not just PKI.
Mitigations:
- HTTP/1.1 requests are unaffected — no `stream` property means the
original `req.socket` is returned unchanged.
- The HTTP/2 path only changes *which* socket object is wrapped;
`KibanaSocket`'s own logic is untouched.
- Fallback preserves today's behaviour whenever the session socket
cannot be resolved.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---

## Release note

Fixes an issue where audit log records could omit the client IP address
for requests made over HTTP/2.

---------

Co-authored-by: Claude Sonnet 4.6 <[email protected]>
Co-authored-by: kibanamachine <[email protected]>
jeramysoucy added a commit that referenced this pull request Aug 28, 2026
…2 requests (#285344) (#287794)

# Backport

This will backport the following commits from `main` to `9.4`:
- [[HTTP/2] Fix KibanaSocket to use session-level socket for HTTP/2
requests (#285344)](#285344)

<!--- Backport version: 12.0.0 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Jeramy
Soucy","email":"[email protected]"},"sourceCommit":{"committedDate":"2026-08-28T09:44:29Z","message":"[HTTP/2]
Fix KibanaSocket to use session-level socket for HTTP/2 requests
(#285344)\n\n## Summary\n\nFixes the root cause of
https://github.com/elastic/kibana/issues/258232\n— PKI sessions being
invalidated after upgrading to Kibana 9.x, where\n`server.protocol`
silently defaults to `http2` when `ssl.enabled: true`.\n\nThis is the
companion to #285153, which fixes the symptom in `pki.ts`.\nThis PR
fixes the underlying socket resolution in Core HTTP.\n\n##
Problem\n\nFor HTTP/2 requests, `Http2ServerRequest.socket` returns a
`Proxy` over\nthe **stream**, not the session's `TLSSocket`. The
load-bearing traps\nbranch on `stream[kSession]`:\n\n```js\n// Node.js
lib/internal/http2/compat.js\ngetPrototypeOf(stream) {\n if
(stream.session !== undefined) return
ReflectGetPrototypeOf(stream.session[kSocket]);\n return
ReflectGetPrototypeOf(stream); // <-- Http2Stream, NOT
TLSSocket\n}\n```\n\nNode clears `stream[kSession]` whenever the stream
is destroyed —\nRST_STREAM, GOAWAY, client abort, timeout. After that
point the proxy\nresolves against the `Http2Stream` itself, so every
`instanceof\nTLSSocket` check in `KibanaSocket` fails and the accessors
degrade:\n\n| accessor | value after stream destruction |\n|---|---|\n|
`authorized` | `undefined` |\n| `authorizationError` | `undefined` |\n|
`getPeerCertificate(true)` | `null` |\n| `getProtocol()` | `null` |\n|
`remoteAddress` | `undefined` |\n\nThis happens on **live, authorized
connections** — the TLS handshake\nsucceeded and the client certificate
is valid, but the stream-level\nproxy can no longer report
it.\n\nKibana's own frontend aborts in-flight requests constantly
via\n`AbortController` (search sessions, unified search, autocomplete).
On\nHTTP/1.1 an abort closes a dedicated socket; on HTTP/2 it
emits\nRST_STREAM on a **shared** session mid-flight, so one cancelled
stream\ndegrades the socket view for concurrent requests.\n\n##
Fix\n\nResolve the **session-level** socket
(`req.stream.session.socket`) at\n`KibanaSocket` construction time
instead of using the stream-level\nproxy. The session socket resolves
against `session[kSocket]`, is a real\n`TLSSocket`, and remains stable
for the lifetime of the TCP connection.\n\nThis is semantically correct:
in HTTP/2 the client certificate is a\nproperty of the **connection**,
not of an individual stream.\n\nFalls back to `req.socket` when the
session socket is unavailable —\neither an HTTP/1.1 request (no `stream`
property) or a fully destroyed\nsession (`session.socket` throws
`ERR_HTTP2_SOCKET_UNBOUND`).\n\n## Secondary fix\n\n`audit_service.ts`
reads `request.socket.remoteAddress` for audit log\nclient IP
enrichment. On destroyed HTTP/2 streams this silently
yielded\n`undefined`, losing the client IP in audit records for **all**
auth\nproviders. This patch fixes that too, since the session socket
carries a\nstable `remoteAddress`.\n\n## Testing\n\nFour new unit tests
in `socket.test.ts` covering `resolveRawSocket`:\n- HTTP/1.1 request (no
`stream` property) → returns `req.socket`\n- HTTP/2 request → returns
the session-level socket\n- `stream.session` undefined → falls back to
`req.socket`\n- `session.socket` throws `ERR_HTTP2_SOCKET_UNBOUND` →
falls back to\n`req.socket`\n\n```\nnode scripts/jest
src/core/packages/http/router-server-internal/src/socket.test.ts\n# 20
passed, 20 total\n```\n\n## Relationship to #285153\n\n- **#285153**
(`pki.ts`) — defensive guard so an unknowable socket state\n(`authorized
=== undefined`) is not treated as an explicit cert\nrejection. Narrow,
safe to backport.\n- **This PR** (Core HTTP) — removes the condition
that produces the\nunknowable state in the first place.\n\nBoth are
worth having. The `pki.ts` guard remains correct\ndefense-in-depth for
the fully-destroyed-session case, where even the\nsession socket is
unavailable.\n\n## Risk\n\nTouches socket resolution for all Core HTTP
requests, not just PKI.\nMitigations:\n- HTTP/1.1 requests are
unaffected — no `stream` property means the\noriginal `req.socket` is
returned unchanged.\n- The HTTP/2 path only changes *which* socket
object is wrapped;\n`KibanaSocket`'s own logic is untouched.\n- Fallback
preserves today's behaviour whenever the session socket\ncannot be
resolved.\n\n🤖 Generated with [Claude
Code](https://claude.com/claude-code)\n\n---\n\n## Release note\n\nFixes
an issue where audit log records could omit the client IP address\nfor
requests made over HTTP/2.\n\n---------\n\nCo-authored-by: Claude Sonnet
4.6 <[email protected]>\nCo-authored-by: kibanamachine
<[email protected]>","sha":"d433465ac0fe5b721a0324706dc138d602287bff","branchLabelMapping":{"^v9.6.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Feature:http","Team:Core","release_note:fix","backport:version","reviewer:scout","v9.6.0","v9.4.6","v9.5.3"],"title":"[HTTP/2]
Fix KibanaSocket to use session-level socket for HTTP/2
requests","number":285344,"url":"https://github.com/elastic/kibana/pull/285344","mergeCommit":{"message":"[HTTP/2]
Fix KibanaSocket to use session-level socket for HTTP/2 requests
(#285344)\n\n## Summary\n\nFixes the root cause of
https://github.com/elastic/kibana/issues/258232\n— PKI sessions being
invalidated after upgrading to Kibana 9.x, where\n`server.protocol`
silently defaults to `http2` when `ssl.enabled: true`.\n\nThis is the
companion to #285153, which fixes the symptom in `pki.ts`.\nThis PR
fixes the underlying socket resolution in Core HTTP.\n\n##
Problem\n\nFor HTTP/2 requests, `Http2ServerRequest.socket` returns a
`Proxy` over\nthe **stream**, not the session's `TLSSocket`. The
load-bearing traps\nbranch on `stream[kSession]`:\n\n```js\n// Node.js
lib/internal/http2/compat.js\ngetPrototypeOf(stream) {\n if
(stream.session !== undefined) return
ReflectGetPrototypeOf(stream.session[kSocket]);\n return
ReflectGetPrototypeOf(stream); // <-- Http2Stream, NOT
TLSSocket\n}\n```\n\nNode clears `stream[kSession]` whenever the stream
is destroyed —\nRST_STREAM, GOAWAY, client abort, timeout. After that
point the proxy\nresolves against the `Http2Stream` itself, so every
`instanceof\nTLSSocket` check in `KibanaSocket` fails and the accessors
degrade:\n\n| accessor | value after stream destruction |\n|---|---|\n|
`authorized` | `undefined` |\n| `authorizationError` | `undefined` |\n|
`getPeerCertificate(true)` | `null` |\n| `getProtocol()` | `null` |\n|
`remoteAddress` | `undefined` |\n\nThis happens on **live, authorized
connections** — the TLS handshake\nsucceeded and the client certificate
is valid, but the stream-level\nproxy can no longer report
it.\n\nKibana's own frontend aborts in-flight requests constantly
via\n`AbortController` (search sessions, unified search, autocomplete).
On\nHTTP/1.1 an abort closes a dedicated socket; on HTTP/2 it
emits\nRST_STREAM on a **shared** session mid-flight, so one cancelled
stream\ndegrades the socket view for concurrent requests.\n\n##
Fix\n\nResolve the **session-level** socket
(`req.stream.session.socket`) at\n`KibanaSocket` construction time
instead of using the stream-level\nproxy. The session socket resolves
against `session[kSocket]`, is a real\n`TLSSocket`, and remains stable
for the lifetime of the TCP connection.\n\nThis is semantically correct:
in HTTP/2 the client certificate is a\nproperty of the **connection**,
not of an individual stream.\n\nFalls back to `req.socket` when the
session socket is unavailable —\neither an HTTP/1.1 request (no `stream`
property) or a fully destroyed\nsession (`session.socket` throws
`ERR_HTTP2_SOCKET_UNBOUND`).\n\n## Secondary fix\n\n`audit_service.ts`
reads `request.socket.remoteAddress` for audit log\nclient IP
enrichment. On destroyed HTTP/2 streams this silently
yielded\n`undefined`, losing the client IP in audit records for **all**
auth\nproviders. This patch fixes that too, since the session socket
carries a\nstable `remoteAddress`.\n\n## Testing\n\nFour new unit tests
in `socket.test.ts` covering `resolveRawSocket`:\n- HTTP/1.1 request (no
`stream` property) → returns `req.socket`\n- HTTP/2 request → returns
the session-level socket\n- `stream.session` undefined → falls back to
`req.socket`\n- `session.socket` throws `ERR_HTTP2_SOCKET_UNBOUND` →
falls back to\n`req.socket`\n\n```\nnode scripts/jest
src/core/packages/http/router-server-internal/src/socket.test.ts\n# 20
passed, 20 total\n```\n\n## Relationship to #285153\n\n- **#285153**
(`pki.ts`) — defensive guard so an unknowable socket state\n(`authorized
=== undefined`) is not treated as an explicit cert\nrejection. Narrow,
safe to backport.\n- **This PR** (Core HTTP) — removes the condition
that produces the\nunknowable state in the first place.\n\nBoth are
worth having. The `pki.ts` guard remains correct\ndefense-in-depth for
the fully-destroyed-session case, where even the\nsession socket is
unavailable.\n\n## Risk\n\nTouches socket resolution for all Core HTTP
requests, not just PKI.\nMitigations:\n- HTTP/1.1 requests are
unaffected — no `stream` property means the\noriginal `req.socket` is
returned unchanged.\n- The HTTP/2 path only changes *which* socket
object is wrapped;\n`KibanaSocket`'s own logic is untouched.\n- Fallback
preserves today's behaviour whenever the session socket\ncannot be
resolved.\n\n🤖 Generated with [Claude
Code](https://claude.com/claude-code)\n\n---\n\n## Release note\n\nFixes
an issue where audit log records could omit the client IP address\nfor
requests made over HTTP/2.\n\n---------\n\nCo-authored-by: Claude Sonnet
4.6 <[email protected]>\nCo-authored-by: kibanamachine
<[email protected]>","sha":"d433465ac0fe5b721a0324706dc138d602287bff"}},"sourceBranch":"main","suggestedTargetBranches":["9.4","9.5"],"targetPullRequestStates":[{"branch":"main","label":"v9.6.0","branchLabelMappingKey":"^v9.6.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/285344","number":285344,"mergeCommit":{"message":"[HTTP/2]
Fix KibanaSocket to use session-level socket for HTTP/2 requests
(#285344)\n\n## Summary\n\nFixes the root cause of
https://github.com/elastic/kibana/issues/258232\n— PKI sessions being
invalidated after upgrading to Kibana 9.x, where\n`server.protocol`
silently defaults to `http2` when `ssl.enabled: true`.\n\nThis is the
companion to #285153, which fixes the symptom in `pki.ts`.\nThis PR
fixes the underlying socket resolution in Core HTTP.\n\n##
Problem\n\nFor HTTP/2 requests, `Http2ServerRequest.socket` returns a
`Proxy` over\nthe **stream**, not the session's `TLSSocket`. The
load-bearing traps\nbranch on `stream[kSession]`:\n\n```js\n// Node.js
lib/internal/http2/compat.js\ngetPrototypeOf(stream) {\n if
(stream.session !== undefined) return
ReflectGetPrototypeOf(stream.session[kSocket]);\n return
ReflectGetPrototypeOf(stream); // <-- Http2Stream, NOT
TLSSocket\n}\n```\n\nNode clears `stream[kSession]` whenever the stream
is destroyed —\nRST_STREAM, GOAWAY, client abort, timeout. After that
point the proxy\nresolves against the `Http2Stream` itself, so every
`instanceof\nTLSSocket` check in `KibanaSocket` fails and the accessors
degrade:\n\n| accessor | value after stream destruction |\n|---|---|\n|
`authorized` | `undefined` |\n| `authorizationError` | `undefined` |\n|
`getPeerCertificate(true)` | `null` |\n| `getProtocol()` | `null` |\n|
`remoteAddress` | `undefined` |\n\nThis happens on **live, authorized
connections** — the TLS handshake\nsucceeded and the client certificate
is valid, but the stream-level\nproxy can no longer report
it.\n\nKibana's own frontend aborts in-flight requests constantly
via\n`AbortController` (search sessions, unified search, autocomplete).
On\nHTTP/1.1 an abort closes a dedicated socket; on HTTP/2 it
emits\nRST_STREAM on a **shared** session mid-flight, so one cancelled
stream\ndegrades the socket view for concurrent requests.\n\n##
Fix\n\nResolve the **session-level** socket
(`req.stream.session.socket`) at\n`KibanaSocket` construction time
instead of using the stream-level\nproxy. The session socket resolves
against `session[kSocket]`, is a real\n`TLSSocket`, and remains stable
for the lifetime of the TCP connection.\n\nThis is semantically correct:
in HTTP/2 the client certificate is a\nproperty of the **connection**,
not of an individual stream.\n\nFalls back to `req.socket` when the
session socket is unavailable —\neither an HTTP/1.1 request (no `stream`
property) or a fully destroyed\nsession (`session.socket` throws
`ERR_HTTP2_SOCKET_UNBOUND`).\n\n## Secondary fix\n\n`audit_service.ts`
reads `request.socket.remoteAddress` for audit log\nclient IP
enrichment. On destroyed HTTP/2 streams this silently
yielded\n`undefined`, losing the client IP in audit records for **all**
auth\nproviders. This patch fixes that too, since the session socket
carries a\nstable `remoteAddress`.\n\n## Testing\n\nFour new unit tests
in `socket.test.ts` covering `resolveRawSocket`:\n- HTTP/1.1 request (no
`stream` property) → returns `req.socket`\n- HTTP/2 request → returns
the session-level socket\n- `stream.session` undefined → falls back to
`req.socket`\n- `session.socket` throws `ERR_HTTP2_SOCKET_UNBOUND` →
falls back to\n`req.socket`\n\n```\nnode scripts/jest
src/core/packages/http/router-server-internal/src/socket.test.ts\n# 20
passed, 20 total\n```\n\n## Relationship to #285153\n\n- **#285153**
(`pki.ts`) — defensive guard so an unknowable socket state\n(`authorized
=== undefined`) is not treated as an explicit cert\nrejection. Narrow,
safe to backport.\n- **This PR** (Core HTTP) — removes the condition
that produces the\nunknowable state in the first place.\n\nBoth are
worth having. The `pki.ts` guard remains correct\ndefense-in-depth for
the fully-destroyed-session case, where even the\nsession socket is
unavailable.\n\n## Risk\n\nTouches socket resolution for all Core HTTP
requests, not just PKI.\nMitigations:\n- HTTP/1.1 requests are
unaffected — no `stream` property means the\noriginal `req.socket` is
returned unchanged.\n- The HTTP/2 path only changes *which* socket
object is wrapped;\n`KibanaSocket`'s own logic is untouched.\n- Fallback
preserves today's behaviour whenever the session socket\ncannot be
resolved.\n\n🤖 Generated with [Claude
Code](https://claude.com/claude-code)\n\n---\n\n## Release note\n\nFixes
an issue where audit log records could omit the client IP address\nfor
requests made over HTTP/2.\n\n---------\n\nCo-authored-by: Claude Sonnet
4.6 <[email protected]>\nCo-authored-by: kibanamachine
<[email protected]>","sha":"d433465ac0fe5b721a0324706dc138d602287bff"}},{"branch":"9.4","label":"v9.4.6","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"9.5","label":"v9.5.3","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->
jeramysoucy added a commit that referenced this pull request Aug 28, 2026
…2 requests (#285344) (#287792)

# Backport

This will backport the following commits from `main` to `9.5`:
- [[HTTP/2] Fix KibanaSocket to use session-level socket for HTTP/2
requests (#285344)](#285344)

<!--- Backport version: 12.0.0 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Jeramy
Soucy","email":"[email protected]"},"sourceCommit":{"committedDate":"2026-08-28T09:44:29Z","message":"[HTTP/2]
Fix KibanaSocket to use session-level socket for HTTP/2 requests
(#285344)\n\n## Summary\n\nFixes the root cause of
https://github.com/elastic/kibana/issues/258232\n— PKI sessions being
invalidated after upgrading to Kibana 9.x, where\n`server.protocol`
silently defaults to `http2` when `ssl.enabled: true`.\n\nThis is the
companion to #285153, which fixes the symptom in `pki.ts`.\nThis PR
fixes the underlying socket resolution in Core HTTP.\n\n##
Problem\n\nFor HTTP/2 requests, `Http2ServerRequest.socket` returns a
`Proxy` over\nthe **stream**, not the session's `TLSSocket`. The
load-bearing traps\nbranch on `stream[kSession]`:\n\n```js\n// Node.js
lib/internal/http2/compat.js\ngetPrototypeOf(stream) {\n if
(stream.session !== undefined) return
ReflectGetPrototypeOf(stream.session[kSocket]);\n return
ReflectGetPrototypeOf(stream); // <-- Http2Stream, NOT
TLSSocket\n}\n```\n\nNode clears `stream[kSession]` whenever the stream
is destroyed —\nRST_STREAM, GOAWAY, client abort, timeout. After that
point the proxy\nresolves against the `Http2Stream` itself, so every
`instanceof\nTLSSocket` check in `KibanaSocket` fails and the accessors
degrade:\n\n| accessor | value after stream destruction |\n|---|---|\n|
`authorized` | `undefined` |\n| `authorizationError` | `undefined` |\n|
`getPeerCertificate(true)` | `null` |\n| `getProtocol()` | `null` |\n|
`remoteAddress` | `undefined` |\n\nThis happens on **live, authorized
connections** — the TLS handshake\nsucceeded and the client certificate
is valid, but the stream-level\nproxy can no longer report
it.\n\nKibana's own frontend aborts in-flight requests constantly
via\n`AbortController` (search sessions, unified search, autocomplete).
On\nHTTP/1.1 an abort closes a dedicated socket; on HTTP/2 it
emits\nRST_STREAM on a **shared** session mid-flight, so one cancelled
stream\ndegrades the socket view for concurrent requests.\n\n##
Fix\n\nResolve the **session-level** socket
(`req.stream.session.socket`) at\n`KibanaSocket` construction time
instead of using the stream-level\nproxy. The session socket resolves
against `session[kSocket]`, is a real\n`TLSSocket`, and remains stable
for the lifetime of the TCP connection.\n\nThis is semantically correct:
in HTTP/2 the client certificate is a\nproperty of the **connection**,
not of an individual stream.\n\nFalls back to `req.socket` when the
session socket is unavailable —\neither an HTTP/1.1 request (no `stream`
property) or a fully destroyed\nsession (`session.socket` throws
`ERR_HTTP2_SOCKET_UNBOUND`).\n\n## Secondary fix\n\n`audit_service.ts`
reads `request.socket.remoteAddress` for audit log\nclient IP
enrichment. On destroyed HTTP/2 streams this silently
yielded\n`undefined`, losing the client IP in audit records for **all**
auth\nproviders. This patch fixes that too, since the session socket
carries a\nstable `remoteAddress`.\n\n## Testing\n\nFour new unit tests
in `socket.test.ts` covering `resolveRawSocket`:\n- HTTP/1.1 request (no
`stream` property) → returns `req.socket`\n- HTTP/2 request → returns
the session-level socket\n- `stream.session` undefined → falls back to
`req.socket`\n- `session.socket` throws `ERR_HTTP2_SOCKET_UNBOUND` →
falls back to\n`req.socket`\n\n```\nnode scripts/jest
src/core/packages/http/router-server-internal/src/socket.test.ts\n# 20
passed, 20 total\n```\n\n## Relationship to #285153\n\n- **#285153**
(`pki.ts`) — defensive guard so an unknowable socket state\n(`authorized
=== undefined`) is not treated as an explicit cert\nrejection. Narrow,
safe to backport.\n- **This PR** (Core HTTP) — removes the condition
that produces the\nunknowable state in the first place.\n\nBoth are
worth having. The `pki.ts` guard remains correct\ndefense-in-depth for
the fully-destroyed-session case, where even the\nsession socket is
unavailable.\n\n## Risk\n\nTouches socket resolution for all Core HTTP
requests, not just PKI.\nMitigations:\n- HTTP/1.1 requests are
unaffected — no `stream` property means the\noriginal `req.socket` is
returned unchanged.\n- The HTTP/2 path only changes *which* socket
object is wrapped;\n`KibanaSocket`'s own logic is untouched.\n- Fallback
preserves today's behaviour whenever the session socket\ncannot be
resolved.\n\n🤖 Generated with [Claude
Code](https://claude.com/claude-code)\n\n---\n\n## Release note\n\nFixes
an issue where audit log records could omit the client IP address\nfor
requests made over HTTP/2.\n\n---------\n\nCo-authored-by: Claude Sonnet
4.6 <[email protected]>\nCo-authored-by: kibanamachine
<[email protected]>","sha":"d433465ac0fe5b721a0324706dc138d602287bff","branchLabelMapping":{"^v9.6.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Feature:http","Team:Core","release_note:fix","backport:version","reviewer:scout","v9.6.0","v9.4.6","v9.5.3"],"title":"[HTTP/2]
Fix KibanaSocket to use session-level socket for HTTP/2
requests","number":285344,"url":"https://github.com/elastic/kibana/pull/285344","mergeCommit":{"message":"[HTTP/2]
Fix KibanaSocket to use session-level socket for HTTP/2 requests
(#285344)\n\n## Summary\n\nFixes the root cause of
https://github.com/elastic/kibana/issues/258232\n— PKI sessions being
invalidated after upgrading to Kibana 9.x, where\n`server.protocol`
silently defaults to `http2` when `ssl.enabled: true`.\n\nThis is the
companion to #285153, which fixes the symptom in `pki.ts`.\nThis PR
fixes the underlying socket resolution in Core HTTP.\n\n##
Problem\n\nFor HTTP/2 requests, `Http2ServerRequest.socket` returns a
`Proxy` over\nthe **stream**, not the session's `TLSSocket`. The
load-bearing traps\nbranch on `stream[kSession]`:\n\n```js\n// Node.js
lib/internal/http2/compat.js\ngetPrototypeOf(stream) {\n if
(stream.session !== undefined) return
ReflectGetPrototypeOf(stream.session[kSocket]);\n return
ReflectGetPrototypeOf(stream); // <-- Http2Stream, NOT
TLSSocket\n}\n```\n\nNode clears `stream[kSession]` whenever the stream
is destroyed —\nRST_STREAM, GOAWAY, client abort, timeout. After that
point the proxy\nresolves against the `Http2Stream` itself, so every
`instanceof\nTLSSocket` check in `KibanaSocket` fails and the accessors
degrade:\n\n| accessor | value after stream destruction |\n|---|---|\n|
`authorized` | `undefined` |\n| `authorizationError` | `undefined` |\n|
`getPeerCertificate(true)` | `null` |\n| `getProtocol()` | `null` |\n|
`remoteAddress` | `undefined` |\n\nThis happens on **live, authorized
connections** — the TLS handshake\nsucceeded and the client certificate
is valid, but the stream-level\nproxy can no longer report
it.\n\nKibana's own frontend aborts in-flight requests constantly
via\n`AbortController` (search sessions, unified search, autocomplete).
On\nHTTP/1.1 an abort closes a dedicated socket; on HTTP/2 it
emits\nRST_STREAM on a **shared** session mid-flight, so one cancelled
stream\ndegrades the socket view for concurrent requests.\n\n##
Fix\n\nResolve the **session-level** socket
(`req.stream.session.socket`) at\n`KibanaSocket` construction time
instead of using the stream-level\nproxy. The session socket resolves
against `session[kSocket]`, is a real\n`TLSSocket`, and remains stable
for the lifetime of the TCP connection.\n\nThis is semantically correct:
in HTTP/2 the client certificate is a\nproperty of the **connection**,
not of an individual stream.\n\nFalls back to `req.socket` when the
session socket is unavailable —\neither an HTTP/1.1 request (no `stream`
property) or a fully destroyed\nsession (`session.socket` throws
`ERR_HTTP2_SOCKET_UNBOUND`).\n\n## Secondary fix\n\n`audit_service.ts`
reads `request.socket.remoteAddress` for audit log\nclient IP
enrichment. On destroyed HTTP/2 streams this silently
yielded\n`undefined`, losing the client IP in audit records for **all**
auth\nproviders. This patch fixes that too, since the session socket
carries a\nstable `remoteAddress`.\n\n## Testing\n\nFour new unit tests
in `socket.test.ts` covering `resolveRawSocket`:\n- HTTP/1.1 request (no
`stream` property) → returns `req.socket`\n- HTTP/2 request → returns
the session-level socket\n- `stream.session` undefined → falls back to
`req.socket`\n- `session.socket` throws `ERR_HTTP2_SOCKET_UNBOUND` →
falls back to\n`req.socket`\n\n```\nnode scripts/jest
src/core/packages/http/router-server-internal/src/socket.test.ts\n# 20
passed, 20 total\n```\n\n## Relationship to #285153\n\n- **#285153**
(`pki.ts`) — defensive guard so an unknowable socket state\n(`authorized
=== undefined`) is not treated as an explicit cert\nrejection. Narrow,
safe to backport.\n- **This PR** (Core HTTP) — removes the condition
that produces the\nunknowable state in the first place.\n\nBoth are
worth having. The `pki.ts` guard remains correct\ndefense-in-depth for
the fully-destroyed-session case, where even the\nsession socket is
unavailable.\n\n## Risk\n\nTouches socket resolution for all Core HTTP
requests, not just PKI.\nMitigations:\n- HTTP/1.1 requests are
unaffected — no `stream` property means the\noriginal `req.socket` is
returned unchanged.\n- The HTTP/2 path only changes *which* socket
object is wrapped;\n`KibanaSocket`'s own logic is untouched.\n- Fallback
preserves today's behaviour whenever the session socket\ncannot be
resolved.\n\n🤖 Generated with [Claude
Code](https://claude.com/claude-code)\n\n---\n\n## Release note\n\nFixes
an issue where audit log records could omit the client IP address\nfor
requests made over HTTP/2.\n\n---------\n\nCo-authored-by: Claude Sonnet
4.6 <[email protected]>\nCo-authored-by: kibanamachine
<[email protected]>","sha":"d433465ac0fe5b721a0324706dc138d602287bff"}},"sourceBranch":"main","suggestedTargetBranches":["9.4","9.5"],"targetPullRequestStates":[{"branch":"main","label":"v9.6.0","branchLabelMappingKey":"^v9.6.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/285344","number":285344,"mergeCommit":{"message":"[HTTP/2]
Fix KibanaSocket to use session-level socket for HTTP/2 requests
(#285344)\n\n## Summary\n\nFixes the root cause of
https://github.com/elastic/kibana/issues/258232\n— PKI sessions being
invalidated after upgrading to Kibana 9.x, where\n`server.protocol`
silently defaults to `http2` when `ssl.enabled: true`.\n\nThis is the
companion to #285153, which fixes the symptom in `pki.ts`.\nThis PR
fixes the underlying socket resolution in Core HTTP.\n\n##
Problem\n\nFor HTTP/2 requests, `Http2ServerRequest.socket` returns a
`Proxy` over\nthe **stream**, not the session's `TLSSocket`. The
load-bearing traps\nbranch on `stream[kSession]`:\n\n```js\n// Node.js
lib/internal/http2/compat.js\ngetPrototypeOf(stream) {\n if
(stream.session !== undefined) return
ReflectGetPrototypeOf(stream.session[kSocket]);\n return
ReflectGetPrototypeOf(stream); // <-- Http2Stream, NOT
TLSSocket\n}\n```\n\nNode clears `stream[kSession]` whenever the stream
is destroyed —\nRST_STREAM, GOAWAY, client abort, timeout. After that
point the proxy\nresolves against the `Http2Stream` itself, so every
`instanceof\nTLSSocket` check in `KibanaSocket` fails and the accessors
degrade:\n\n| accessor | value after stream destruction |\n|---|---|\n|
`authorized` | `undefined` |\n| `authorizationError` | `undefined` |\n|
`getPeerCertificate(true)` | `null` |\n| `getProtocol()` | `null` |\n|
`remoteAddress` | `undefined` |\n\nThis happens on **live, authorized
connections** — the TLS handshake\nsucceeded and the client certificate
is valid, but the stream-level\nproxy can no longer report
it.\n\nKibana's own frontend aborts in-flight requests constantly
via\n`AbortController` (search sessions, unified search, autocomplete).
On\nHTTP/1.1 an abort closes a dedicated socket; on HTTP/2 it
emits\nRST_STREAM on a **shared** session mid-flight, so one cancelled
stream\ndegrades the socket view for concurrent requests.\n\n##
Fix\n\nResolve the **session-level** socket
(`req.stream.session.socket`) at\n`KibanaSocket` construction time
instead of using the stream-level\nproxy. The session socket resolves
against `session[kSocket]`, is a real\n`TLSSocket`, and remains stable
for the lifetime of the TCP connection.\n\nThis is semantically correct:
in HTTP/2 the client certificate is a\nproperty of the **connection**,
not of an individual stream.\n\nFalls back to `req.socket` when the
session socket is unavailable —\neither an HTTP/1.1 request (no `stream`
property) or a fully destroyed\nsession (`session.socket` throws
`ERR_HTTP2_SOCKET_UNBOUND`).\n\n## Secondary fix\n\n`audit_service.ts`
reads `request.socket.remoteAddress` for audit log\nclient IP
enrichment. On destroyed HTTP/2 streams this silently
yielded\n`undefined`, losing the client IP in audit records for **all**
auth\nproviders. This patch fixes that too, since the session socket
carries a\nstable `remoteAddress`.\n\n## Testing\n\nFour new unit tests
in `socket.test.ts` covering `resolveRawSocket`:\n- HTTP/1.1 request (no
`stream` property) → returns `req.socket`\n- HTTP/2 request → returns
the session-level socket\n- `stream.session` undefined → falls back to
`req.socket`\n- `session.socket` throws `ERR_HTTP2_SOCKET_UNBOUND` →
falls back to\n`req.socket`\n\n```\nnode scripts/jest
src/core/packages/http/router-server-internal/src/socket.test.ts\n# 20
passed, 20 total\n```\n\n## Relationship to #285153\n\n- **#285153**
(`pki.ts`) — defensive guard so an unknowable socket state\n(`authorized
=== undefined`) is not treated as an explicit cert\nrejection. Narrow,
safe to backport.\n- **This PR** (Core HTTP) — removes the condition
that produces the\nunknowable state in the first place.\n\nBoth are
worth having. The `pki.ts` guard remains correct\ndefense-in-depth for
the fully-destroyed-session case, where even the\nsession socket is
unavailable.\n\n## Risk\n\nTouches socket resolution for all Core HTTP
requests, not just PKI.\nMitigations:\n- HTTP/1.1 requests are
unaffected — no `stream` property means the\noriginal `req.socket` is
returned unchanged.\n- The HTTP/2 path only changes *which* socket
object is wrapped;\n`KibanaSocket`'s own logic is untouched.\n- Fallback
preserves today's behaviour whenever the session socket\ncannot be
resolved.\n\n🤖 Generated with [Claude
Code](https://claude.com/claude-code)\n\n---\n\n## Release note\n\nFixes
an issue where audit log records could omit the client IP address\nfor
requests made over HTTP/2.\n\n---------\n\nCo-authored-by: Claude Sonnet
4.6 <[email protected]>\nCo-authored-by: kibanamachine
<[email protected]>","sha":"d433465ac0fe5b721a0324706dc138d602287bff"}},{"branch":"9.4","label":"v9.4.6","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"9.5","label":"v9.5.3","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->
dej611 pushed a commit to dej611/kibana that referenced this pull request Aug 31, 2026
…ests (elastic#285344)

## Summary

Fixes the root cause of elastic#258232
— PKI sessions being invalidated after upgrading to Kibana 9.x, where
`server.protocol` silently defaults to `http2` when `ssl.enabled: true`.

This is the companion to elastic#285153, which fixes the symptom in `pki.ts`.
This PR fixes the underlying socket resolution in Core HTTP.

## Problem

For HTTP/2 requests, `Http2ServerRequest.socket` returns a `Proxy` over
the **stream**, not the session's `TLSSocket`. The load-bearing traps
branch on `stream[kSession]`:

```js
// Node.js lib/internal/http2/compat.js
getPrototypeOf(stream) {
  if (stream.session !== undefined) return ReflectGetPrototypeOf(stream.session[kSocket]);
  return ReflectGetPrototypeOf(stream);   // <-- Http2Stream, NOT TLSSocket
}
```

Node clears `stream[kSession]` whenever the stream is destroyed —
RST_STREAM, GOAWAY, client abort, timeout. After that point the proxy
resolves against the `Http2Stream` itself, so every `instanceof
TLSSocket` check in `KibanaSocket` fails and the accessors degrade:

| accessor | value after stream destruction |
|---|---|
| `authorized` | `undefined` |
| `authorizationError` | `undefined` |
| `getPeerCertificate(true)` | `null` |
| `getProtocol()` | `null` |
| `remoteAddress` | `undefined` |

This happens on **live, authorized connections** — the TLS handshake
succeeded and the client certificate is valid, but the stream-level
proxy can no longer report it.

Kibana's own frontend aborts in-flight requests constantly via
`AbortController` (search sessions, unified search, autocomplete). On
HTTP/1.1 an abort closes a dedicated socket; on HTTP/2 it emits
RST_STREAM on a **shared** session mid-flight, so one cancelled stream
degrades the socket view for concurrent requests.

## Fix

Resolve the **session-level** socket (`req.stream.session.socket`) at
`KibanaSocket` construction time instead of using the stream-level
proxy. The session socket resolves against `session[kSocket]`, is a real
`TLSSocket`, and remains stable for the lifetime of the TCP connection.

This is semantically correct: in HTTP/2 the client certificate is a
property of the **connection**, not of an individual stream.

Falls back to `req.socket` when the session socket is unavailable —
either an HTTP/1.1 request (no `stream` property) or a fully destroyed
session (`session.socket` throws `ERR_HTTP2_SOCKET_UNBOUND`).

## Secondary fix

`audit_service.ts` reads `request.socket.remoteAddress` for audit log
client IP enrichment. On destroyed HTTP/2 streams this silently yielded
`undefined`, losing the client IP in audit records for **all** auth
providers. This patch fixes that too, since the session socket carries a
stable `remoteAddress`.

## Testing

Four new unit tests in `socket.test.ts` covering `resolveRawSocket`:
- HTTP/1.1 request (no `stream` property) → returns `req.socket`
- HTTP/2 request → returns the session-level socket
- `stream.session` undefined → falls back to `req.socket`
- `session.socket` throws `ERR_HTTP2_SOCKET_UNBOUND` → falls back to
`req.socket`

```
node scripts/jest src/core/packages/http/router-server-internal/src/socket.test.ts
# 20 passed, 20 total
```

## Relationship to elastic#285153

- **elastic#285153** (`pki.ts`) — defensive guard so an unknowable socket state
(`authorized === undefined`) is not treated as an explicit cert
rejection. Narrow, safe to backport.
- **This PR** (Core HTTP) — removes the condition that produces the
unknowable state in the first place.

Both are worth having. The `pki.ts` guard remains correct
defense-in-depth for the fully-destroyed-session case, where even the
session socket is unavailable.

## Risk

Touches socket resolution for all Core HTTP requests, not just PKI.
Mitigations:
- HTTP/1.1 requests are unaffected — no `stream` property means the
original `req.socket` is returned unchanged.
- The HTTP/2 path only changes *which* socket object is wrapped;
`KibanaSocket`'s own logic is untouched.
- Fallback preserves today's behaviour whenever the session socket
cannot be resolved.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---

## Release note

Fixes an issue where audit log records could omit the client IP address
for requests made over HTTP/2.

---------

Co-authored-by: Claude Sonnet 4.6 <[email protected]>
Co-authored-by: kibanamachine <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport:all-open Backport to all branches that could still receive a release release_note:fix reviewer:scout Agentic PR Scout test review Team:Security Platform Security: Auth, Users, Roles, Spaces, Audit Logging, etc t// v8.19.22 v9.4.6 v9.5.3 v9.6.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Investigate HTTP2 null return

5 participants