{"record":{"id":"9b75d46b9e3effc0","repo":"paperclipai/paperclip","slug":"railway-ssh-key-invalid","errorCode":"railway_ssh_key_invalid","errorMessage":"Regenerate the Railway connection's SSH key.","messagePattern":"Regenerate the Railway connection's SSH key\\.","errorType":"error_code","errorClass":"RailwayError","httpStatus":422,"severity":"error","filePath":"server/src/services/railway-ssh.ts","lineNumber":48,"sourceCode":"}\n\nexport function railwaySshArguments(directory: string, instanceId: string): string[] {\n  if (!/^[a-f0-9-]{36}$/i.test(instanceId)) throw new RailwayError(\"railway_target_mismatch\", \"Invalid Railway container instance.\", 400);\n  return [\n    \"-F\", \"/dev/null\", \"-T\", \"-i\", path.join(directory, \"identity\"),\n    \"-o\", \"BatchMode=yes\", \"-o\", \"IdentitiesOnly=yes\", \"-o\", \"IdentityAgent=none\",\n    \"-o\", \"ForwardAgent=no\", \"-o\", \"ClearAllForwardings=yes\", \"-o\", \"ControlMaster=no\",\n    \"-o\", \"ControlPath=none\", \"-o\", \"PermitLocalCommand=no\", \"-o\", \"StrictHostKeyChecking=yes\",\n    \"-o\", `UserKnownHostsFile=${path.join(directory, \"known_hosts\")}`, \"-o\", \"GlobalKnownHostsFile=/dev/null\",\n    \"-o\", \"ConnectTimeout=10\", \"-o\", \"ServerAliveInterval=5\", \"-o\", \"ServerAliveCountMax=2\",\n    \"--\", `${instanceId}@ssh.railway.com`, \"sh -s\",\n  ];\n}\n\nexport async function runRailwaySshCommand(input: RailwaySshInput & { privateKey: string; knownHosts: string }) {\n  input.signal.throwIfAborted();\n  const knownHosts = validateRailwayKnownHosts(input.knownHosts);\n  if (!input.privateKey.startsWith(\"-----BEGIN OPENSSH PRIVATE KEY-----\")) throw new RailwayError(\"railway_ssh_key_invalid\", \"Regenerate the Railway connection's SSH key.\", 422);\n  const directory = await mkdtemp(path.join(tmpdir(), \"paperclip-railway-command-\"));\n  try {\n    await writeFile(path.join(directory, \"identity\"), input.privateKey, { mode: 0o600 });\n    await writeFile(path.join(directory, \"known_hosts\"), knownHosts, { mode: 0o600 });\n    input.signal.throwIfAborted();\n    return await new Promise<{ exitCode: number | null; stdout: string; stderr: string; truncated: boolean; timedOut: boolean }>((resolve, reject) => {\n      // No developer SSH config/agent, CLI login, provider token or ambient env.\n      const child = spawn(\"/usr/bin/ssh\", railwaySshArguments(directory, input.deploymentInstanceId), { env: { PATH: \"/usr/bin:/bin\", LANG: \"C.UTF-8\" }, stdio: [\"pipe\", \"pipe\", \"pipe\"] });\n      let stdout = \"\", stderr = \"\", bytes = 0, truncated = false, timedOut = false, deliveryFailed = false;\n      const marker = `paperclip_railway_completed_${randomBytes(16).toString(\"hex\")}`;\n      const stop = () => { child.kill(\"SIGKILL\"); };\n      const receive = (chunk: Buffer, stream: \"out\" | \"err\") => {\n        const remaining = Math.max(0, 64 * 1024 - bytes);\n        bytes += chunk.length;\n        const text = chunk.subarray(0, remaining).toString(\"utf8\");\n        if (stream === \"out\") stdout += text; else stderr += text;\n        if (bytes > 64 * 1024) { truncated = true; stop(); }\n      };","sourceCodeStart":30,"sourceCodeEnd":66,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/railway-ssh.ts#L30-L66","documentation":"RailwayError (code railway_ssh_key_invalid, HTTP 422) thrown by runRailwaySshCommand when the stored private key does not begin with the OpenSSH PEM header '-----BEGIN OPENSSH PRIVATE KEY-----'. The connection's key material is in a legacy or corrupted format that ssh with StrictHostKeyChecking and BatchMode cannot use, so the library fails fast and asks the user to regenerate the key.","triggerScenarios":"runRailwaySshCommand receives input.privateKey that is a PEM RSA key ('-----BEGIN RSA PRIVATE KEY-----'), a PuTTY .ppk, a public key pasted by mistake, a redacted/placeholder value, or an empty string.","commonSituations":"Older connections created before the switch to ed25519 OpenSSH format; user pasted the .pub file contents; secrets migration truncated or redacted the key; key generated by a tool emitting legacy PEM; copy/paste lost the header line.","solutions":["Regenerate the key via the Railway connection's SSH key regeneration flow (generateRailwaySshKey) and re-upload the public half to Railway","Confirm the stored secret starts with '-----BEGIN OPENSSH PRIVATE KEY-----' and ends with the matching footer","If the key is legacy PEM, convert it: ssh-keygen -p -m RFC4716? no — use `ssh-keygen -p -f key` to rewrite in OpenSSH format","Verify the secret path (railway.ssh_private_key) was not overwritten by a public key or placeholder during config"],"exampleFix":"// before\nprivateKey: fs.readFileSync(\"id_rsa_old\", \"utf8\") // BEGIN RSA PRIVATE KEY\n// after\nexecSync(\"ssh-keygen -p -f id_rsa_old -N ''\"); // rewrite as OPENSSH format\nprivateKey: fs.readFileSync(\"id_rsa_old\", \"utf8\") // BEGIN OPENSSH PRIVATE KEY","handlingStrategy":"validation","validationCode":"function isOpenSshPrivateKey(key) {\n  return typeof key === \"string\" && key.startsWith(\"-----BEGIN OPENSSH PRIVATE KEY-----\");\n}\nif (!isOpenSshPrivateKey(storedKey)) {\n  // prompt regeneration before attempting any SSH command\n}","typeGuard":"function hasValidRailwayPrivateKey(input) {\n  return typeof input.privateKey === \"string\" &&\n    input.privateKey.startsWith(\"-----BEGIN OPENSSH PRIVATE KEY-----\");\n}","tryCatchPattern":"try {\n  await runRailwaySshCommand({ ...input, privateKey, knownHosts });\n} catch (e) {\n  if (e.code === \"railway_ssh_key_invalid\") {\n    await regenerateAndStoreRailwaySshKey(connectionId); // then retry once\n    return runRailwaySshCommand({ ...input, privateKey: freshKey, knownHosts });\n  }\n  throw e;\n}","preventionTips":["Always generate keys via generateRailwaySshKey (ed25519, OpenSSH format) instead of importing legacy keys","Verify the secret at railway.ssh_private_key was not overwritten with the public key or a placeholder","Preserve the full PEM including BEGIN/END header lines through any copy/paste or migration","After upgrading Paperclip, regenerate keys created by older versions emitting legacy PEM"],"tags":["ssh","key-format","config","railway"],"backgroundTag":"invalid-config-value","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-22T11:17:16.035Z"}