node-ssh、ssh2、ssh2-promise、ssh2-sftp-client はすべてNode.jsでSSHやSFTP通信を行うためのnpmパッケージです。ssh2は低レベルなSSH2プロトコル実装を提供し、他の多くのライブラリの基盤となっています。node-sshはssh2をラップしてPromiseベースの高レベルAPIを提供します。ssh2-promiseも同様のラッパーでしたが、現在は非推奨です。ssh2-sftp-clientはSFTP(SSHファイル転送プロトコル)専用の操作に特化しており、ファイルのアップロード・ダウンロード・リスト取得などを簡単に実現できます。
Node.jsでSSHやSFTP操作を行う場合、いくつかのnpmパッケージが選択肢として存在します。この記事では、node-ssh、ssh2、ssh2-promise、ssh2-sftp-client の4つのパッケージを、実際の開発現場での使いやすさ、機能性、メンテナンス状況といった観点から深く比較します。
まず、これらのパッケージはすべて、Node.js環境でSSHプロトコル(およびSFTP)を使ってリモートサーバーと通信するためのものです。ただし、抽象化のレベルや目的が異なります。
ssh2 は低レベルなSSH2クライアント・サーバーライブラリで、他の多くのSSH関連パッケージの基盤となっています。node-ssh は ssh2 をラップして、より高レベルで使いやすいAPIを提供します。ssh2-promise も同様に ssh2 をPromiseベースでラップしたものですが、現在は非推奨です。ssh2-sftp-client は ssh2 を使って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();
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-ssh は putFile や getFile といったヘルパーメソッドを提供し、ファイル転送を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: 同様に privateKey と passphrase をサポート。さらに 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-sshかssh2を検討してください。
node-sshSSH経由でシェルコマンドを実行し、結果を取得したい場合、node-ssh の execCommand() が最も直感的で安全です。エラーハンドリングも標準化されており、プロダクション環境でも安心して使えます。
ssh2-sftp-client大量のファイルをアップロード・ダウンロードしたり、ディレクトリ操作が必要な場合は、ssh2-sftp-client が最適です。fastPut/fastGet といった高速転送メソッドも備えており、大容量ファイルにも対応できます。
ssh2独自のSSH拡張や、特殊なフロー(ポートフォワーディング、複数チャネルの同時使用など)を実装したい場合は、ssh2 の低レベルAPIを使うしかありません。ただし、その分だけコード量と複雑さが増します。
ssh2-promise は使わないすでに非推奨であり、セキュリティリスクや互換性問題の可能性があるため、既存プロジェクトでも移行を検討すべきです。
| パッケージ | 主な用途 | 抽象化レベル | メンテナンス状況 | SFTP対応 | コマンド実行 |
|---|---|---|---|---|---|
ssh2 | 低レベル制御 | 低 | 活発 | ✅(手動) | ✅(手動) |
node-ssh | 一般用途のSSH操作 | 高 | 活発 | ✅(簡単) | ✅(簡単) |
ssh2-promise | (非推奨) | 中 | ❌ 非推奨 | ✅ | ✅ |
ssh2-sftp-client | SFTP専用操作 | 中〜高 | 活発 | ✅(フル機能) | ❌ |
node-ssh を選ぶ。ssh2-sftp-client を選ぶ。ssh2 を直接使う。ssh2-promise は絶対に使わない。これらの選択は、あなたのプロジェクトの要件とチームのメンテナンス負荷に大きく影響します。シンプルさと安全性を重視するなら、node-ssh が多くのケースでベストバランスを提供します。
node-sshは、SSH経由でコマンドを実行したり、簡単なファイル転送を行いたい場合に最適です。Promise/async対応のクリーンなAPIを持ち、エラーハンドリングも統一されています。一般的なDevOpsタスクやCI/CDスクリプトでよく使われ、バランスの取れた選択肢です。
ssh2は、SSHプロトコルを細かく制御したい場合や、ポートフォワーディング、複数チャネルの管理など高度な機能が必要なときに選んでください。ただし、イベント駆動の低レベルAPIのため、コードが冗長になりやすく、初心者には難しいです。他のライブラリの内部実装としても広く使われています。
ssh2-promiseは公式に非推奨(deprecated)とされており、GitHubリポジトリもアーカイブされています。新規プロジェクトでは絶対に使用せず、既存プロジェクトでもnode-sshやssh2への移行を検討すべきです。セキュリティアップデートやバグ修正が行われないため、リスクがあります。
ssh2-sftp-clientはSFTP操作(ファイル転送、ディレクトリ操作など)に特化したライブラリです。SSHコマンド実行はできませんが、put、get、list、mkdirなどのメソッドが充実しており、ファイル中心のワークフローに最適です。大容量ファイル転送にも対応しています。
Node-SSH is an extremely lightweight Promise wrapper for ssh2.
$ npm install node-ssh # If you're using npm
$ yarn add node-ssh # If you're using Yarn
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 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
}
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
}
}
In some cases you have to enable keyboard-interactive user authentication.
Otherwise you will get an All configured authentication methods failed error.
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
This project is licensed under the terms of MIT license. See the LICENSE file for more info.