{"record":{"id":"eb525c6b903a491f","repo":"paperclipai/paperclip","slug":"railway-ssh-unavailable","errorCode":"railway_ssh_unavailable","errorMessage":"Generating a Railway key requires system OpenSSH (ssh-keygen) on the Paperclip runtime.","messagePattern":"Generating a Railway key requires system OpenSSH \\(ssh-keygen\\) on the Paperclip runtime\\.","errorType":"error_code","errorClass":"RailwayError","httpStatus":422,"severity":"error","filePath":"server/src/services/railway-ssh.ts","lineNumber":28,"sourceCode":"export const RAILWAY_SSH_SECRET_PATH = \"railway.ssh_private_key\";\n\nexport function validateRailwayKnownHosts(value: string): string {\n  if (value.length > 8192) throw new RailwayError(\"railway_ssh_host_key_invalid\", \"The Railway host key is too long.\", 400);\n  const lines = value.trim().split(/\\r?\\n/);\n  if (lines.length === 0 || lines.length > 5 || lines.some((line) => !/^ssh\\.railway\\.com (ssh-ed25519|ssh-rsa|ecdsa-sha2-nistp256) [A-Za-z0-9+/]+={0,2}$/.test(line))) {\n    throw new RailwayError(\"railway_ssh_host_key_invalid\", \"Paste verified known_hosts lines for ssh.railway.com only, without aliases, wildcards or comments.\", 400);\n  }\n  return lines.join(\"\\n\") + \"\\n\";\n}\n\nexport async function generateRailwaySshKey(): Promise<{ publicKey: string; privateKey: string }> {\n  const directory = await mkdtemp(path.join(tmpdir(), \"paperclip-railway-key-\"));\n  try {\n    const keyPath = path.join(directory, \"identity\");\n    await promisify(execFile)(\"/usr/bin/ssh-keygen\", [\"-q\", \"-t\", \"ed25519\", \"-N\", \"\", \"-C\", \"paperclip-railway\", \"-f\", keyPath], { timeout: 10_000, env: { PATH: \"/usr/bin:/bin\" } });\n    return { publicKey: (await readFile(`${keyPath}.pub`, \"utf8\")).trim(), privateKey: await readFile(keyPath, \"utf8\") };\n  } catch {\n    throw new RailwayError(\"railway_ssh_unavailable\", \"Generating a Railway key requires system OpenSSH (ssh-keygen) on the Paperclip runtime.\", 422);\n  } finally { await rm(directory, { recursive: true, force: true }); }\n}\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();","sourceCodeStart":10,"sourceCodeEnd":46,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/railway-ssh.ts#L10-L46","documentation":"RailwayError (code railway_ssh_unavailable, HTTP 422) thrown by generateRailwaySshKey when spawning /usr/bin/ssh-keygen fails. Key generation depends on system OpenSSH being installed at that exact path on the Paperclip runtime; any spawn or execution error (missing binary, non-zero exit, timeout) is mapped to this error.","triggerScenarios":"generateRailwaySshKey runs but execFile('/usr/bin/ssh-keygen', ...) throws: binary not installed, installed at a different path (e.g. /bin/ssh-keygen or Homebrew /opt/homebrew/bin), not executable, ENOENT/EACCES, or exceeds the 10s timeout with the restricted PATH=/usr/bin:/bin.","commonSituations":"Minimal Docker images (distroless/alpine without openssh-client); macOS dev machines where ssh-keygen lives at /usr/bin but sandbox blocks execFile; hardened containers with noexec on /usr; slow disk causing the 10s timeout.","solutions":["Install OpenSSH client in the runtime image (e.g. apk add openssh-client / apt-get install -y openssh-client)","Verify the binary exists: ls -l /usr/bin/ssh-keygen; if it is elsewhere, symlink or adjust to /usr/bin","Run the container/hosts with permission to exec /usr/bin binaries (no noexec mount, not fully read-only without the package)","Check for 10s timeout being hit — retry once and confirm disk/CPU health"],"exampleFix":"// Dockerfile before\nFROM node:20-slim\n// after\nFROM node:20-slim\nRUN apt-get update && apt-get install -y --no-install-recommends openssh-client && rm -rf /var/lib/apt/lists/*","handlingStrategy":"fallback","validationCode":"import { accessSync, constants } from \"node:fs\";\nfunction sshKeygenAvailable() {\n  try { accessSync(\"/usr/bin/ssh-keygen\", constants.X_OK); return true; } catch { return false; }\n}\nif (!sshKeygenAvailable()) {\n  // surface install instructions or fall back to user-supplied keys\n}","typeGuard":"null","tryCatchPattern":"try {\n  const { publicKey, privateKey } = await generateRailwaySshKey();\n} catch (e) {\n  if (e.code === \"railway_ssh_unavailable\") {\n    showUserError(\"Install openssh-client in this environment, or paste your own ed25519 key.\");\n    return promptForManualKey();\n  }\n  throw e;\n}","preventionTips":["Bake openssh-client into the runtime image (apk add openssh-client / apt-get install openssh-client)","Proactively check for /usr/bin/ssh-keygen at startup and degrade gracefully","Allow manual key paste as a fallback path when generation is unavailable","Watch for exec timeouts (10s) on slow hosts and retry once before reporting unavailability"],"tags":["environment","ssh","missing-binary","railway"],"backgroundTag":"missing-dependency","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-22T16:17:23.217Z"}