From 59083aac00cc7971f39e91eb2aa65b9d6c768f23 Mon Sep 17 00:00:00 2001 From: ZacharyZcR Date: Sat, 3 Oct 2026 21:04:21 +0800 Subject: [PATCH] fix(ssh): read force keyboard-interactive as a boolean when resolving hosts (#1553) The column holds the text "true"/"false", and the resolver passed it through as-is. The string "false" is truthy, so the password provider treated every saved host as forced keyboard-interactive and left the password out. Jump hops are built straight from the resolved host, so a password hop whose server doesn't offer keyboard-interactive failed with "All configured authentication methods failed". --- src/backend/hosts/host-resolver.ts | 4 ++++ src/backend/tests/hosts/host-resolver.test.ts | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/backend/hosts/host-resolver.ts b/src/backend/hosts/host-resolver.ts index 7c9befb0f..771553b38 100644 --- a/src/backend/hosts/host-resolver.ts +++ b/src/backend/hosts/host-resolver.ts @@ -129,6 +129,10 @@ export async function resolveHostById( } host.terminalConfig = rest; } + // Stored as the text "true"/"false"; the string "false" is truthy and would + // make the password provider skip password auth. + host.forceKeyboardInteractive = + String(host.forceKeyboardInteractive) === "true"; // A row the boot copy has not reached yet still has them in terminal_config. host.sshOptions = parseSshOptions( host.sshOptions != null ? host.sshOptions : host.terminalConfig, diff --git a/src/backend/tests/hosts/host-resolver.test.ts b/src/backend/tests/hosts/host-resolver.test.ts index 94d8ecff3..e1057d643 100644 --- a/src/backend/tests/hosts/host-resolver.test.ts +++ b/src/backend/tests/hosts/host-resolver.test.ts @@ -487,6 +487,16 @@ describe("resolveHostById", () => { expect(host.sshOptions).toEqual({ keepaliveCountMax: 3 }); }); + it("turns the stored force_keyboard_interactive text into a boolean", async () => { + state.host = baseHost({ forceKeyboardInteractive: "false" }); + const off = (await resolveHostById(42, "owner")) as Record; + expect(off.forceKeyboardInteractive).toBe(false); + + state.host = baseHost({ forceKeyboardInteractive: "true" }); + const on = (await resolveHostById(42, "owner")) as Record; + expect(on.forceKeyboardInteractive).toBe(true); + }); + it("resolves an admin bypass like the owner, keeping owner-only secrets", async () => { state.isAdminBypass = true; state.host = baseHost({