node-ssh vs ssh2 vs ssh2-promise vs ssh2-sftp-client
Node.jsにおけるSSH/SFTPクライアントライブラリの比較
node-sshssh2ssh2-promisessh2-sftp-client

Node.jsにおけるSSH/SFTPクライアントライブラリの比較

node-sshssh2ssh2-promisessh2-sftp-client はすべてNode.jsでSSHやSFTP通信を行うためのnpmパッケージです。ssh2は低レベルなSSH2プロトコル実装を提供し、他の多くのライブラリの基盤となっています。node-sshssh2をラップしてPromiseベースの高レベルAPIを提供します。ssh2-promiseも同様のラッパーでしたが、現在は非推奨です。ssh2-sftp-clientはSFTP(SSHファイル転送プロトコル)専用の操作に特化しており、ファイルのアップロード・ダウンロード・リスト取得などを簡単に実現できます。

npmのダウンロードトレンド

3 年

GitHub Starsランキング

統計詳細

パッケージ
ダウンロード数
Stars
サイズ
Issues
公開日時
ライセンス
node-ssh099678.3 kB601年前MIT
ssh205,7631.12 MB928ヶ月前-
ssh2-promise0151169 kB20-MIT
ssh2-sftp-client0918247 kB120日前Apache-2.0

SSHクライアントライブラリ比較: node-ssh vs ssh2 vs ssh2-promise vs ssh2-sftp-client

Node.jsでSSHやSFTP操作を行う場合、いくつかのnpmパッケージが選択肢として存在します。この記事では、node-sshssh2ssh2-promisessh2-sftp-client の4つのパッケージを、実際の開発現場での使いやすさ、機能性、メンテナンス状況といった観点から深く比較します。

📦 パッケージの基本的な役割

まず、これらのパッケージはすべて、Node.js環境でSSHプロトコル(およびSFTP)を使ってリモートサーバーと通信するためのものです。ただし、抽象化のレベルや目的が異なります。

  • ssh2 は低レベルなSSH2クライアント・サーバーライブラリで、他の多くのSSH関連パッケージの基盤となっています。
  • node-sshssh2 をラップして、より高レベルで使いやすいAPIを提供します。
  • ssh2-promise も同様に ssh2 をPromiseベースでラップしたものですが、現在は非推奨です。
  • ssh2-sftp-clientssh2 を使ってSFTP専用の操作を簡単にできるように設計されています。

それでは、具体的な違いを見ていきましょう。

⚙️ 接続と基本的なコマンド実行

ssh2: 低レベルで柔軟だが冗長

ssh2 は直接ストリームやイベントハンドラを扱う必要があり、初心者には敷居が高いです。

const { Client } = require('ssh2');

const conn = new Client();
conn.on('ready', () => {
  conn.exec('ls -l', (err, stream) => {
    if (err) throw err;
    let data = '';
    stream.on('data', (chunk) => {
      data += chunk.toString();
    });
    stream.on('close', (code, signal) => {
      console.log('Command output:', data);
      conn.end();
    });
  });
}).connect({
  host: 'example.com',
  username: 'user',
  password: 'password'
});

node-ssh: Promise/async対応でシンプル

node-ssh は接続、コマンド実行、切断を一貫したPromise APIで提供します。

const { NodeSSH } = require('node-ssh');

const ssh = new NodeSSH();
await ssh.connect({
  host: 'example.com',
  username: 'user',
  password: 'password'
});

const result = await ssh.execCommand('ls -l');
console.log('stdout:', result.stdout);
console.log('stderr:', result.stderr);

await ssh.dispose();

ssh2-promise: 非推奨 — 新規プロジェクトでは使用しない

ssh2-promise はかつて ssh2 をPromiseでラップしていましたが、npmページおよびGitHubリポジトリで公式に非推奨(deprecated)とされています。新規プロジェクトでの使用は避けてください。

// 非推奨のため、コード例は参考程度
const SSH = require('ssh2-promise');

const config = { host: 'example.com', username: 'user', password: 'password' };
const ssh = new SSH(config);
const output = await ssh.exec('ls -l');
console.log(output);

ssh2-sftp-client: SFTP専用、ファイル操作に特化

このパッケージはSSH接続そのものではなく、SFTP(ファイル転送)に焦点を当てています。コマンド実行はできません。

const Client = require('ssh2-sftp-client');

const sftp = new Client();
await sftp.connect({
  host: 'example.com',
  username: 'user',
  password: 'password'
});

const listing = await sftp.list('/remote/path');
console.log(listing);

await sftp.end();

📁 ファイル転送(SFTP)の使いやすさ

ssh2: 手動でSFTPセッションを管理

ssh2 でファイルをアップロードするには、SFTPサブシステムを明示的に開始し、ストリームを操作する必要があります。

const { Client } = require('ssh2');
const fs = require('fs');

const conn = new Client();
conn.on('ready', () => {
  conn.sftp((err, sftp) => {
    if (err) throw err;
    const readStream = fs.createReadStream('local-file.txt');
    const writeStream = sftp.createWriteStream('remote-file.txt');
    writeStream.on('close', () => {
      console.log('File uploaded');
      conn.end();
    });
    readStream.pipe(writeStream);
  });
}).connect({
  host: 'example.com',
  username: 'user',
  password: 'password'
});

node-ssh: putFile / getFile で簡単転送

node-sshputFilegetFile といったヘルパーメソッドを提供し、ファイル転送を1行で実現できます。

const { NodeSSH } = require('node-ssh');

const ssh = new NodeSSH();
await ssh.connect({
  host: 'example.com',
  username: 'user',
  password: 'password'
});

await ssh.putFile('local-file.txt', '/remote/path/remote-file.txt');
console.log('File uploaded');

await ssh.dispose();

ssh2-sftp-client: SFTP操作のフルサポート

list, get, put, mkdir, delete など、SFTPに必要な操作がすべてメソッドとして提供されています。

const Client = require('ssh2-sftp-client');

const sftp = new Client();
await sftp.connect({
  host: 'example.com',
  username: 'user',
  password: 'password'
});

await sftp.put('local-file.txt', '/remote/path/remote-file.txt');
const data = await sftp.get('/remote/path/remote-file.txt');
console.log('Downloaded file length:', data.length);

await sftp.end();

🔒 認証方法のサポート

すべてのパッケージはパスワード認証と公開鍵認証(privateKey)をサポートしていますが、設定方法に若干の違いがあります。

  • ssh2: privateKey にBufferまたは文字列を渡す。passphrase も指定可能。
  • node-ssh: 同様に privateKeypassphrase をサポート。さらに tryKeyboard などのオプションも利用可能。
  • ssh2-sftp-client: ssh2 と同じ認証オプションをそのまま受け渡すため、同等の柔軟性があります。
// node-sshでの鍵認証例
await ssh.connect({
  host: 'example.com',
  username: 'user',
  privateKey: require('fs').readFileSync('/path/to/id_rsa'),
  passphrase: 'optional-passphrase'
});

🛠️ メンテナンス状況と将来性

  • ssh2: 現在も活発にメンテナンスされており、SSHプロトコルの低レベル実装として信頼性が高い。
  • node-ssh: 定期的に更新されており、高レベルAPIとして安定している。
  • ssh2-promise: 公式に非推奨。GitHubリポジトリはアーカイブされており、バグ修正やセキュリティ対応は行われていない。
  • ssh2-sftp-client: SFTP専用として継続的にメンテナンスされている。

💡 重要な注意: ssh2-promise は新規プロジェクトで使用すべきではありません。代わりに node-sshssh2 を検討してください。

🎯 用途別の選定ガイド

コマンド実行が主目的なら → node-ssh

SSH経由でシェルコマンドを実行し、結果を取得したい場合、node-sshexecCommand() が最も直感的で安全です。エラーハンドリングも標準化されており、プロダクション環境でも安心して使えます。

SFTPファイル操作が主目的なら → ssh2-sftp-client

大量のファイルをアップロード・ダウンロードしたり、ディレクトリ操作が必要な場合は、ssh2-sftp-client が最適です。fastPut/fastGet といった高速転送メソッドも備えており、大容量ファイルにも対応できます。

最大限の制御が必要なら → ssh2

独自のSSH拡張や、特殊なフロー(ポートフォワーディング、複数チャネルの同時使用など)を実装したい場合は、ssh2 の低レベルAPIを使うしかありません。ただし、その分だけコード量と複雑さが増します。

ssh2-promise は使わない

すでに非推奨であり、セキュリティリスクや互換性問題の可能性があるため、既存プロジェクトでも移行を検討すべきです。

📊 まとめ:各パッケージの特徴

パッケージ主な用途抽象化レベルメンテナンス状況SFTP対応コマンド実行
ssh2低レベル制御活発✅(手動)✅(手動)
node-ssh一般用途のSSH操作活発✅(簡単)✅(簡単)
ssh2-promise(非推奨)❌ 非推奨
ssh2-sftp-clientSFTP専用操作中〜高活発✅(フル機能)

💡 最終的なアドバイス

  • 「SSHでコマンドを実行したい」node-ssh を選ぶ。
  • 「ファイルをSFTPでやり取りしたい」ssh2-sftp-client を選ぶ。
  • 「特別なSSH機能が必要」ssh2 を直接使う。
  • ssh2-promise は絶対に使わない

これらの選択は、あなたのプロジェクトの要件とチームのメンテナンス負荷に大きく影響します。シンプルさと安全性を重視するなら、node-ssh が多くのケースでベストバランスを提供します。

選び方: node-ssh vs ssh2 vs ssh2-promise vs ssh2-sftp-client

  • node-ssh:

    node-sshは、SSH経由でコマンドを実行したり、簡単なファイル転送を行いたい場合に最適です。Promise/async対応のクリーンなAPIを持ち、エラーハンドリングも統一されています。一般的なDevOpsタスクやCI/CDスクリプトでよく使われ、バランスの取れた選択肢です。

  • ssh2:

    ssh2は、SSHプロトコルを細かく制御したい場合や、ポートフォワーディング、複数チャネルの管理など高度な機能が必要なときに選んでください。ただし、イベント駆動の低レベルAPIのため、コードが冗長になりやすく、初心者には難しいです。他のライブラリの内部実装としても広く使われています。

  • ssh2-promise:

    ssh2-promiseは公式に非推奨(deprecated)とされており、GitHubリポジトリもアーカイブされています。新規プロジェクトでは絶対に使用せず、既存プロジェクトでもnode-sshssh2への移行を検討すべきです。セキュリティアップデートやバグ修正が行われないため、リスクがあります。

  • ssh2-sftp-client:

    ssh2-sftp-clientはSFTP操作(ファイル転送、ディレクトリ操作など)に特化したライブラリです。SSHコマンド実行はできませんが、putgetlistmkdirなどのメソッドが充実しており、ファイル中心のワークフローに最適です。大容量ファイル転送にも対応しています。

node-ssh のREADME

Node-SSH - SSH2 with Promises

Node-SSH is an extremely lightweight Promise wrapper for ssh2.

Installation

$ npm install node-ssh # If you're using npm
$ yarn add node-ssh # If you're using Yarn

Example

const fs = require('fs')
const path = require('path')
const {NodeSSH} = require('node-ssh')

const ssh = new NodeSSH()

ssh.connect({
  host: 'localhost',
  username: 'steel',
  privateKeyPath: '/home/steel/.ssh/id_rsa'
})

// or with inline privateKey

ssh.connect({
  host: 'localhost',
  username: 'steel',
  privateKey: Buffer.from('...')
})
.then(function() {
  // Local, Remote
  ssh.putFile('/home/steel/Lab/localPath/fileName', '/home/steel/Lab/remotePath/fileName').then(function() {
    console.log("The File thing is done")
  }, function(error) {
    console.log("Something's wrong")
    console.log(error)
  })
  // Array<Shape('local' => string, 'remote' => string)>
  ssh.putFiles([{ local: '/home/steel/Lab/localPath/fileName', remote: '/home/steel/Lab/remotePath/fileName' }]).then(function() {
    console.log("The File thing is done")
  }, function(error) {
    console.log("Something's wrong")
    console.log(error)
  })
  // Local, Remote
  ssh.getFile('/home/steel/Lab/localPath', '/home/steel/Lab/remotePath').then(function(Contents) {
    console.log("The File's contents were successfully downloaded")
  }, function(error) {
    console.log("Something's wrong")
    console.log(error)
  })
  // Putting entire directories
  const failed = []
  const successful = []
  ssh.putDirectory('/home/steel/Lab', '/home/steel/Lab', {
    recursive: true,
    concurrency: 10,
    // ^ WARNING: Not all servers support high concurrency
    // try a bunch of values and see what works on your server
    validate: function(itemPath) {
      const baseName = path.basename(itemPath)
      return baseName.substr(0, 1) !== '.' && // do not allow dot files
             baseName !== 'node_modules' // do not allow node_modules
    },
    tick: function(localPath, remotePath, error) {
      if (error) {
        failed.push(localPath)
      } else {
        successful.push(localPath)
      }
    }
  }).then(function(status) {
    console.log('the directory transfer was', status ? 'successful' : 'unsuccessful')
    console.log('failed transfers', failed.join(', '))
    console.log('successful transfers', successful.join(', '))
  })
  // Command
  ssh.execCommand('hh_client --json', { cwd:'/var/www' }).then(function(result) {
    console.log('STDOUT: ' + result.stdout)
    console.log('STDERR: ' + result.stderr)
  })
  // Command with escaped params
  ssh.exec('hh_client', ['--json'], { cwd: '/var/www', stream: 'stdout', options: { pty: true } }).then(function(result) {
    console.log('STDOUT: ' + result)
  })
  // With streaming stdout/stderr callbacks
  ssh.exec('hh_client', ['--json'], {
    cwd: '/var/www',
    onStdout(chunk) {
      console.log('stdoutChunk', chunk.toString('utf8'))
    },
    onStderr(chunk) {
      console.log('stderrChunk', chunk.toString('utf8'))
    },
  })
})

API

// API reference in Typescript typing format:
import SSH2, {
  AcceptConnection,
  Channel,
  ClientChannel,
  ConnectConfig,
  ExecOptions,
  Prompt,
  PseudoTtyOptions,
  RejectConnection,
  SFTPWrapper,
  ShellOptions,
  TcpConnectionDetails,
  TransferOptions,
  UNIXConnectionDetails,
} from 'ssh2'
import stream from 'stream'

// ^ You do NOT need to import these package, these are here for reference of where the
// types are coming from.

export type Config = ConnectConfig & {
  password?: string
  privateKey?: string
  privateKeyPath?: string
  tryKeyboard?: boolean
  onKeyboardInteractive?: (
    name: string,
    instructions: string,
    lang: string,
    prompts: Prompt[],
    finish: (responses: string[]) => void,
  ) => void
}
export interface SSHExecCommandOptions {
  cwd?: string
  stdin?: string | stream.Readable
  execOptions?: ExecOptions
  encoding?: BufferEncoding
  noTrim?: boolean
  onChannel?: (clientChannel: ClientChannel) => void
  onStdout?: (chunk: Buffer) => void
  onStderr?: (chunk: Buffer) => void
}
export interface SSHExecCommandResponse {
  stdout: string
  stderr: string
  code: number | null
  signal: string | null
}
export interface SSHExecOptions extends SSHExecCommandOptions {
  stream?: 'stdout' | 'stderr' | 'both'
}
export interface SSHPutFilesOptions {
  sftp?: SFTPWrapper | null
  concurrency?: number
  transferOptions?: TransferOptions
}
export interface SSHGetPutDirectoryOptions extends SSHPutFilesOptions {
  tick?: (localFile: string, remoteFile: string, error: Error | null) => void
  validate?: (path: string) => boolean
  recursive?: boolean
}
export type SSHMkdirMethod = 'sftp' | 'exec'
export type SSHForwardInListener = (
  details: TcpConnectionDetails,
  accept: AcceptConnection<ClientChannel>,
  reject: RejectConnection,
) => void
export interface SSHForwardInDetails {
  dispose(): Promise<void>
  port: number
}
export type SSHForwardInStreamLocalListener = (
  info: UNIXConnectionDetails,
  accept: AcceptConnection,
  reject: RejectConnection,
) => void
export interface SSHForwardInStreamLocalDetails {
  dispose(): Promise<void>
}
export class SSHError extends Error {
  code: string | null
  constructor(message: string, code?: string | null)
}

export class NodeSSH {
  connection: SSH2.Client | null

  connect(config: Config): Promise<this>
  isConnected(): boolean
  requestShell(options?: PseudoTtyOptions | ShellOptions | false): Promise<ClientChannel>
  withShell(
    callback: (channel: ClientChannel) => Promise<void>,
    options?: PseudoTtyOptions | ShellOptions | false,
  ): Promise<void>
  requestSFTP(): Promise<SFTPWrapper>
  withSFTP(callback: (sftp: SFTPWrapper) => Promise<void>): Promise<void>
  execCommand(givenCommand: string, options?: SSHExecCommandOptions): Promise<SSHExecCommandResponse>
  exec(
    command: string,
    parameters: string[],
    options?: SSHExecOptions & {
      stream?: 'stdout' | 'stderr'
    },
  ): Promise<string>
  exec(
    command: string,
    parameters: string[],
    options?: SSHExecOptions & {
      stream: 'both'
    },
  ): Promise<SSHExecCommandResponse>
  mkdir(path: string, method?: SSHMkdirMethod, givenSftp?: SFTPWrapper | null): Promise<void>
  getFile(
    localFile: string,
    remoteFile: string,
    givenSftp?: SFTPWrapper | null,
    transferOptions?: TransferOptions | null,
  ): Promise<void>
  putFile(
    localFile: string,
    remoteFile: string,
    givenSftp?: SFTPWrapper | null,
    transferOptions?: TransferOptions | null,
  ): Promise<void>
  putFiles(
    files: {
      local: string
      remote: string
    }[],
    { concurrency, sftp: givenSftp, transferOptions }?: SSHPutFilesOptions,
  ): Promise<void>
  putDirectory(
    localDirectory: string,
    remoteDirectory: string,
    { concurrency, sftp: givenSftp, transferOptions, recursive, tick, validate }?: SSHGetPutDirectoryOptions,
  ): Promise<boolean>
  getDirectory(
    localDirectory: string,
    remoteDirectory: string,
    { concurrency, sftp: givenSftp, transferOptions, recursive, tick, validate }?: SSHGetPutDirectoryOptions,
  ): Promise<boolean>
  forwardIn(remoteAddr: string, remotePort: number, onConnection?: SSHForwardInListener): Promise<SSHForwardInDetails>
  forwardOut(srcIP: string, srcPort: number, dstIP: string, dstPort: number): Promise<Channel>
  forwardInStreamLocal(
    socketPath: string,
    onConnection?: SSHForwardInStreamLocalListener,
  ): Promise<SSHForwardInStreamLocalDetails>
  forwardOutStreamLocal(socketPath: string): Promise<Channel>
  dispose(): void
}

Typescript support

node-ssh requires extra dependencies while working under Typescript. Please install them as shown below

yarn add --dev @types/ssh2
# OR
npm install --save-dev @types/ssh2

If you're still running into issues, try adding these to your tsconfig.json

{
  "compilerOptions": {
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true
  }
}

Keyboard-interactive user authentication

In some cases you have to enable keyboard-interactive user authentication. Otherwise you will get an All configured authentication methods failed error.

Example:

const password = 'test'

ssh.connect({
  host: 'localhost',
  username: 'steel',
  port: 22,
  password,
  tryKeyboard: true,
})

// Or if you want to add some custom keyboard-interactive logic:

ssh.connect({
  host: 'localhost',
  username: 'steel',
  port: 22,
  tryKeyboard: true,
  onKeyboardInteractive(name, instructions, instructionsLang, prompts, finish) {
    if (prompts.length > 0 && prompts[0].prompt.toLowerCase().includes('password')) {
      finish([password])
    }
  }
})

For further information see: https://github.com/mscdex/ssh2/issues/604

License

This project is licensed under the terms of MIT license. See the LICENSE file for more info.