fix ssh sandbox key cp (#48924)

Signed-off-by: sallyom <somalley@redhat.com>
This commit is contained in:
Sally O'Malley
2026-03-17 07:22:33 -04:00
committed by GitHub
parent f404ff32d5
commit 59cd98068f
2 changed files with 83 additions and 12 deletions

View File

@@ -39,10 +39,52 @@ describe("sandbox ssh helpers", () => {
expect(config).toContain("UpdateHostKeys no");
const configDir = session.configPath.slice(0, session.configPath.lastIndexOf("/"));
expect(await fs.readFile(`${configDir}/identity`, "utf8")).toBe("PRIVATE KEY");
expect(await fs.readFile(`${configDir}/certificate.pub`, "utf8")).toBe("SSH CERT");
expect(await fs.readFile(`${configDir}/identity`, "utf8")).toBe("PRIVATE KEY\n");
expect(await fs.readFile(`${configDir}/certificate.pub`, "utf8")).toBe("SSH CERT\n");
expect(await fs.readFile(`${configDir}/known_hosts`, "utf8")).toBe(
"example.com ssh-ed25519 AAAATEST",
"example.com ssh-ed25519 AAAATEST\n",
);
});
it("normalizes CRLF and escaped-newline private keys before writing temp files", async () => {
const session = await createSshSandboxSessionFromSettings({
command: "ssh",
target: "peter@example.com:2222",
strictHostKeyChecking: true,
updateHostKeys: false,
identityData:
"-----BEGIN OPENSSH PRIVATE KEY-----\\nbGluZTE=\\r\\nbGluZTI=\\r\\n-----END OPENSSH PRIVATE KEY-----",
knownHostsData: "example.com ssh-ed25519 AAAATEST",
});
sessions.push(session);
const configDir = session.configPath.slice(0, session.configPath.lastIndexOf("/"));
expect(await fs.readFile(`${configDir}/identity`, "utf8")).toBe(
"-----BEGIN OPENSSH PRIVATE KEY-----\n" +
"bGluZTE=\n" +
"bGluZTI=\n" +
"-----END OPENSSH PRIVATE KEY-----\n",
);
});
it("normalizes mixed real and escaped newlines in private keys", async () => {
const session = await createSshSandboxSessionFromSettings({
command: "ssh",
target: "peter@example.com:2222",
strictHostKeyChecking: true,
updateHostKeys: false,
identityData:
"-----BEGIN OPENSSH PRIVATE KEY-----\nline-1\\nline-2\n-----END OPENSSH PRIVATE KEY-----",
knownHostsData: "example.com ssh-ed25519 AAAATEST",
});
sessions.push(session);
const configDir = session.configPath.slice(0, session.configPath.lastIndexOf("/"));
expect(await fs.readFile(`${configDir}/identity`, "utf8")).toBe(
"-----BEGIN OPENSSH PRIVATE KEY-----\n" +
"line-1\n" +
"line-2\n" +
"-----END OPENSSH PRIVATE KEY-----\n",
);
});

View File

@@ -35,6 +35,35 @@ export type RunSshSandboxCommandParams = {
tty?: boolean;
};
function normalizeInlineSshMaterial(contents: string, filename: string): string {
const withoutBom = contents.replace(/^\uFEFF/, "");
const normalizedNewlines = withoutBom.replace(/\r\n?/g, "\n");
const normalizedEscapedNewlines = normalizedNewlines
.replace(/\\r\\n/g, "\\n")
.replace(/\\r/g, "\\n");
const expanded =
filename === "identity" || filename === "certificate.pub"
? normalizedEscapedNewlines.replace(/\\n/g, "\n")
: normalizedEscapedNewlines;
return expanded.endsWith("\n") ? expanded : `${expanded}\n`;
}
function buildSshFailureMessage(stderr: string, exitCode?: number): string {
const trimmed = stderr.trim();
if (
trimmed.includes("error in libcrypto") &&
(trimmed.includes('Load key "') || trimmed.includes("Permission denied (publickey)"))
) {
return `${trimmed}\nSSH sandbox failed to load the configured identity. The private key contents may be malformed (for example CRLF or escaped newlines). Prefer identityFile when possible.`;
}
return (
trimmed ||
(exitCode !== undefined
? `ssh exited with code ${exitCode}`
: "ssh exited with a non-zero status")
);
}
export function shellEscape(value: string): string {
return `'${value.replaceAll("'", `'"'"'`)}'`;
}
@@ -201,14 +230,11 @@ export async function runSshSandboxCommand(
const exitCode = code ?? 0;
if (exitCode !== 0 && !params.allowFailure) {
reject(
Object.assign(
new Error(stderr.toString("utf8").trim() || `ssh exited with code ${exitCode}`),
{
code: exitCode,
stdout,
stderr,
},
),
Object.assign(new Error(buildSshFailureMessage(stderr.toString("utf8"), exitCode)), {
code: exitCode,
stdout,
stderr,
}),
);
return;
}
@@ -328,7 +354,10 @@ async function writeSecretMaterial(
contents: string,
): Promise<string> {
const pathname = path.join(dir, filename);
await fs.writeFile(pathname, contents, { encoding: "utf8", mode: 0o600 });
await fs.writeFile(pathname, normalizeInlineSshMaterial(contents, filename), {
encoding: "utf8",
mode: 0o600,
});
await fs.chmod(pathname, 0o600);
return pathname;
}