Skip to content

Commit ae93a37

Browse files
rgodfrey-elasticclaudelegrego
authored
[core/http] Fix FIPS-mode cookie sealing (Deriving bits failed) (elastic#268045)
> [!IMPORTANT] > this is a backwards-incompatible change. Existing sessions will be terminated following an upgrade to a version which includes this change ## Summary In FIPS mode, login succeeds at the auth layer but the very next response (the post-login redirect that sets the `sid` cookie) fails with: ``` Failed to encode cookie (sid) value: Deriving bits failed ``` …and the user is bounced back to the login page. Root cause: `@hapi/iron`'s PBKDF2 defaults are `iterations: 1`, which OpenSSL's FIPS provider rejects (`SP 800-132` requires ≥ 1000 iterations). This PR plumbs a custom `iron` config through the auth-cookie strategy in `cookie_session_storage.ts`, setting iterations to **1000** for both encryption and integrity blocks. ## Why 1000 - `SP 800-132` minimum for FIPS-approved PBKDF2 — clears the OpenSSL FIPS check. - Iron caps PBKDF2's load to one call per seal/unseal, so cookie work happens once at login + once per authenticated request. 1000 iterations is ~1 ms; bumping higher would add user-visible latency to every authenticated request without buying real security here. - The iteration count's job is to slow down brute force on weak passwords. Kibana already requires `xpack.security.encryptionKey` to be ≥ 32 characters, so the input is already high-entropy random material. ## Risk / impact - **Cookie format changes.** Existing `sid` cookies in user browsers will fail to decrypt after upgrade, forcing a single re-login. No data loss; worth a release note. - The `iron` field is officially typed on `StateOptions` from `@hapi/statehood` and forwarded to `Iron.seal`/`Iron.unseal`, so this is a supported usage path. - No behavior change for non-FIPS deployments beyond the cookie-format reset. ## How it was found Reproduced locally against the 9.3.4 staging FIPS build (`elasticsearch-cloud-ess-fips` + `kibana-cloud-fips`): 1. Without the fix: login → redirect → `Deriving bits failed` → bounce to login page. 2. Verified the failing call path by tracing `@hapi/statehood/lib/index.js:486 → prepareValue → Iron.seal` (default `iterations: 1`). 3. Confirmed `iterations: 1000` clears OpenSSL FIPS rejection in isolation against bundled Node, and that `iron` propagates correctly through `@hapi/cookie`'s `.unknown()` schema → `server.state()` → statehood → iron. 4. Patched the running Kibana container with this exact change; login then completes successfully. ## Test plan - [ ] CI green - [ ] Manual: bring up a FIPS-mode Kibana + ES, log in as `elastic`, confirm no `Deriving bits failed` and that the session persists across page refreshes - [ ] Manual: non-FIPS deployment — verify login still works and existing sessions are reset cleanly (one expected re-login on first deploy) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Larry Gregory <larry.gregory@elastic.co>
1 parent eb74355 commit ae93a37

1 file changed

Lines changed: 18 additions & 0 deletions

File tree

src/core/packages/http/server-internal/src/cookie_session_storage.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,24 @@ export async function createCookieSessionStorageFactory<T extends object>(
150150
isSameSite: cookieOptions.sameSite ?? false,
151151
isPartitioned:
152152
cookieOptions.sameSite === 'None' && cookieOptions.isSecure && !disableEmbedding,
153+
// Override @hapi/iron's defaults so cookie sealing works under FIPS-mode OpenSSL.
154+
// Iron's default is iterations=1, which OpenSSL's FIPS provider rejects with
155+
// "Deriving bits failed". 1000 is the SP 800-132 minimum and is sufficient given
156+
// encryptionKey is already required to be a 32+ char high-entropy value.
157+
iron: {
158+
encryption: {
159+
saltBits: 256,
160+
algorithm: 'aes-256-cbc',
161+
iterations: 1000,
162+
minPasswordlength: 32,
163+
},
164+
integrity: {
165+
saltBits: 256,
166+
algorithm: 'sha256',
167+
iterations: 1000,
168+
minPasswordlength: 32,
169+
},
170+
},
153171
},
154172
validate: async (req: Request, session: T | T[]) => {
155173
const result = cookieOptions.validate(session);

0 commit comments

Comments
 (0)