mcpField
S
SSH MCP Server

SSH MCP Server

ローカル
github.com開発ツール8403 日前に更新

SSH MCP Server について

SSH MCP Serverは、AIエージェントにポリシーで制御されたリモートホストへのSSHアクセスを提供し、サーバー運用をMCPクライアントに接続したいチーム向けです。コマンド分類、ロールとホストグループによる認可、破壊的操作の承認、監査記録で実行を管理します。

主な機能

  1. 01ポリシー制御のSSHコマンド実行
  2. 02ロールとホストグループによる認可
  3. 03破壊的操作の人による承認
  4. 04対話型・バックグラウンドセッション
  5. 05監査記録付きSFTP転送

活用シーン

  1. 01リモートホストの状態確認とファイル閲覧
  2. 02永続セッションでのデプロイコマンド実行
  3. 03承認を得た特権・破壊的コマンドの実行
  4. 04制限ディレクトリ内でのリモートファイル転送

MCP 対応

MCPサーバーとして、ホスト接続、セッション、コマンド実行、SFTP、設定再読み込みのツールを対応クライアントに提供します。コマンドはポリシーに基づいて分類・認可され、操作は監査記録に残ります。

設定

ローカルサーバーの設定はどのクライアントでも共通です。コピーしてクライアントの MCP 設定ファイルに貼り付けてください。

mcp.json · 共通設定
{
"mcpServers": {
"ssh": {
"command": "npx",
"args": [
"-y",
"ssh-mcp"
],
"env": {
"SSH_MCP_PASSWORD": "<SSH_MCP_PASSWORD>",
"SSH_MCP_KEY": "<SSH_MCP_KEY>",
"SSH_MCP_PASSPHRASE": "<SSH_MCP_PASSPHRASE>",
"SSH_MCP_SUDO_PASSWORD": "<SSH_MCP_SUDO_PASSWORD>"
}
}
}
}
起動コマンドnpx -y ssh-mcp
設定が必要な環境変数
SSH_MCP_PASSWORDシークレットPassword for the configured profile; a per-profile SSH_MCP_<PROFILE>_PASSWORD takes precedence. Credentials are never accepted as CLI arguments.
SSH_MCP_KEYシークレットPath to the private key file to authenticate with — the path, not the key material. A per-profile SSH_MCP_<PROFILE>_KEY takes precedence.
SSH_MCP_PASSPHRASEシークレットPassphrase for an encrypted private key.
SSH_MCP_SUDO_PASSWORDシークレットPassword the sudo tool escalates with, when the policy in force allows the privileged class.

ローカルサーバーにはマシン上の実行環境(Node.js 18 以上など)が必要です。設定ファイルの場所はクライアントごとに異なります。

パッケージ
npmssh-mcp2.22.0

ツール

14

掲載時のツール一覧 · 3 週間前に更新

このツールにパラメーターはありません。

詳細

SSH MCP Server v2

NPM Version Downloads CI OpenSSF Scorecard OpenSSF Best Practices codecov License GitHub issues

SSH MCP Server は、LLM エージェントにリモートホストへの制御された SSH アクセスを提供する、セキュリティを最優先した Model Context Protocol サーバーです。コマンドの分類、ポリシーベースの認可、人間による承認、完全な監査ログを備えています。

このサーバーが対処するリスク。 リモートホスト上のシェルアクセスを LLM に与えると、プライベートデータ、信頼できない入力、ネットワークへの送信が一か所に集まります。これは Simon Willison が "lethal trifecta" と呼ぶものです。プロンプトインジェクションに一般的な解決策はないため、ssh-mcp はあらゆるコマンドが攻撃者の影響を受ける可能性があると想定します。実行前に分類し、ロール × ホストグループのマトリックスに基づいて認可し、破壊的な操作には承認を必須とし、判断の結果にかかわらず記録します。これにより被害範囲は縮小しますが、リスクがなくなるわけではありません。必ず守るべきことが 2 つあります。root アカウントを指定しないこと、そして本番プロファイルで承認を auto に設定しないことです。脅威モデルの詳細は SECURITY.md をご覧ください。


クイックスタート

1. インストール
npm install -g ssh-mcp
2. 設定する

設定ファイルがなくてもサーバーは起動するため、クライアントやディレクトリは MCP ハンドシェイクを完了し、tools/list を読み取れます。ただし、以下のパスを示すメッセージとともに すべてのツール呼び出しが拒否されます。この手順が完了するまでは、ホスト上で何も実行されません。 例外は reload-config です。ファイルを書き込んでから action: "reload" を指定して呼び出す (またはサーバーを再起動する)と、すでに実行中のサーバーを設定できます。詳しくは 再読み込みを参照してください。

お使いのプラットフォームに対応するパスに設定ファイルを作成します。

プラットフォームパス
Linux~/.config/ssh-mcp/config.toml(または $XDG_CONFIG_HOME/ssh-mcp/config.toml)
macOS~/Library/Application Support/ssh-mcp/config.toml
Windows%APPDATA%\ssh-mcp\config.toml
[defaults]
defaultProfile = "dev"
approvalMode = "ask-destructive"

[[profiles]]
name = "dev"
host = "192.168.1.100"
port = 22
user = "deploy"           # NOT root!
auth = "key"
keyRef = "~/.ssh/id_ed25519"
role = "admin"
approvalPolicy = "auto"    # dev is permissive
chmod 700 ~/.config/ssh-mcp && chmod 600 ~/.config/ssh-mcp/config.toml

この設定ファイルによって、このサーバーがどのホスト、ロール、ポリシールールを適用するかが決まるため、 自分以外のユーザーが読み取れないことを確認します。また、一方のプラットフォームではこの問いへの 答えがはるかに明確であるため、2つのプラットフォームでは扱いが異なります。

Linux と macOS:強制されます。 上記のモードチェックは、ファイルとディレクトリの両方を対象とします。 デフォルトの umask で mkdir -p を実行するとディレクトリのパーミッションが 0755 になるため、 このコマンドでは chmod 700 を使用しています。チェックを満たさない場合、サーバーは起動を拒否します。 ここでの「所有者のみ」は明確であり、chmod を1行実行すれば修正できます。

Windows:ACL で実際に許可されている内容に応じて分かれます。 モードビットがないため、代わりに ACL を 読み取ります。読み取りの公開と書き込みの公開は同じ扱いではありません。Windows では、その一方について はるかに明確だからです。

ACL によって別のアカウントに許可される操作…デフォルトの動作
設定ファイルの読み取りのみ報告され、サーバーは起動します
設定ファイルの変更拒否されます
何も許可されない(ACL がない)拒否されます — これは全員にフルコントロールを与える状態です
…また、ACL を読み取れない場合icacls がない場合、またはチェックがタイムアウトした場合を除き、拒否されます

%APPDATA% にある設定ファイルは、ユーザー、SYSTEM、Administrators のアクセス権を継承するため、 何も変更する必要はありません。別の場所に作成した設定ファイルはそうではありません。C:\ の下にある ファイルは、すべてのローカルアカウントに読み取りを、すべての認証済みアカウントに変更を許可する アクセス権を継承します。どちらの場合も、メッセージに修正用の icacls コマンドが2つ示されます。

Windows では POSIX よりも読み取りの公開の判断が実際に難しいため、読み取りの公開は拒否するのではなく 報告します。これを理由に拒否すると、ドキュメントに記載された場所の設定ファイルが使えなくなっていました (#138)。一方、書き込みの公開は明確な問題であるため 拒否します。このサーバーが適用するホスト、ロール、承認ポリシーを決めるファイルを別のアカウントが 書き換えられるのは、情報漏えいではなく認可の迂回だからです。

2つのフラグでこの動作全体を変更できます。--strictConfigAcl は読み取りのみの権限も含め、チェックで 問題とされる内容をすべて拒否します。--allowUncheckedConfigAcl はすべて報告しますが、何も拒否しません。 #138 の教訓は、どちらの設定でも逃げ道がなくなることはない、ということです。

終了ステータス
ステータス意味
0正常終了
1サーバーの不具合 — スタックトレースとともに出力されます。報告してください
2起動方法または設定の問題 — メッセージとして出力され、スタックトレースはありません

ゼロ以外のステータスをすべて失敗として扱うスーパーバイザーは、変更不要です。起動時の問題を検出するために 1 を条件としていた場合は、2 も条件に含めてください。

設定が何もない状態で起動しても、終了ステータスが 2 になるわけではありません。設定ファイルなしで イントロスペクションに対応したリリース以降、サーバーは状態を説明できるよう起動し、reload-config を除く 各ツール呼び出しを拒否します。未設定のデプロイを検出するためにゼロ以外の終了ステータスを監視していた スーパーバイザーは、標準エラー出力の starting unconfigured を監視するか、HTTP トランスポートの実行時に GET /health から configured を読み取ってください。

3. 環境変数で認証情報を設定する
export SSH_MCP_PASSWORD="your-password"        # if using auth=password
# OR use SSH agent (recommended):
export SSH_AUTH_SOCK="$SSH_AUTH_SOCK"           # already set if agent running
4. MCP クライアントから接続する

Claude Code:

claude mcp add --transport stdio ssh-mcp -- ssh-mcp

Claude Desktop / Cursor / Windsurf:

{
  "mcpServers": {
    "ssh-mcp": {
      "command": "ssh-mcp",
      "env": {
        "SSH_MCP_PASSWORD": "your-password"
      }
    }
  }
}

パスワードを CLI 引数として渡さないでください — ps aux で表示されます。環境変数、設定ファイル、SSH エージェント、または OS のキーチェーンを使用してください。


ツール(15)

ツール目的readOnlydestructive
list-connections利用可能なホストと接続状態を確認します✅—
list-sessionsホストごとのアクティブなセッションを一覧表示します✅—
open-session名前付きの対話型(ステートフル)セッションまたはバックグラウンドセッションを作成します——
close-sessionセッションを閉じます。バックグラウンドセッションのコマンドには、チャネルが切断される前にシグナル(INT/TERM/KILL)が送信されます—✅
read-session-outputバックグラウンドセッション(例:tail -f)の出力を読み取ります✅—
read-command許可リストに登録された読み取り専用コマンド(ls、cat、grep など)を実行します✅—
run-command任意のコマンドを実行します(approvalPolicy = "auto" の場合を除き、破壊的または特権が必要な操作には承認が必要です)——
privileged-commandsudo を使用して実行します(approvalPolicy = "auto" の場合を除き、承認が必要です)—✅
sftp-uploadSFTP でファイルをアップロードします(宛先は無条件に置き換えられます)—✅
sftp-downloadSFTP でファイルをダウンロードします✅—
sftp-listエントリ数とバイト数に上限を設けて、リモートディレクトリを一覧表示します✅—
sftp-upload-fileローカルファイルをリモートホストにストリーミングします。モデルコンテキストを経由することはありません—✅
sftp-download-fileリモートファイルをローカルディスクにストリーミングします。モデルコンテキストを経由することはありません✅—
signal-processリモート PID に INT/TERM/KILL を送信します—✅
reload-config設定ファイルの状態を表示するか、再起動せずに設定を再読み込みして適用します(再読み込みを参照)—✅
ストリーミングによるファイル転送

sftp-upload/sftp-download は、ファイルの内容をモデルのコンテキスト経由で転送します。送信時は引数としてテキストが渡され、戻り時はレスポンスになります。設定の一部を転送する場合には適していますが、200 MB の tarball やバイナリファイルにはまったく適していません。

この2つのツールは、既存の転送先に対する動作も異なります。承認対象の文字列には、どちらの動作かが示されます。sftp-upload は無条件に置き換え、常にその動作をするため、毎回 --overwrite を明示します。一方、sftp-upload-file は overwrite: true を渡さない限り拒否し、渡した場合にのみフラグを含めます。sftp-upload はローカルファイルを指定するのではなく、内容を引数として受け取るため、その文字列には --bytes=<n> --sha256=<32 hex> も含まれます。これがないと、同じパスへの2回のアップロードは同じ文字列になり、1回の承認で両方が許可され、監査者にはどちらのバイト列が書き込まれたのか分かりません。ダイジェストが短いプレフィックスではなく128ビットなのは、偶然の重複だけでなく、呼び出し元が両方のペイロードを選んだ場合にもその主張が成り立つ必要があるためです。

[policy].denylist のパターンは文字列全体に対して照合され、すべての SFTP ツールについてはリモートパス単体にも照合されます。パスは指定されたとおりに扱われ、字句的に正規化され、Windows の読み取り方で解釈されます(「Policy Engine」を参照)。したがって、authorized_keys$ のようにパスを対象とするルールは、文字列内での配置やファイル名の表記方法に左右されません。文字列全体にアンカーを付けるルール(^sftp:upload /root/.*$ など)は、引き続き配置に左右されます。この配置は変更される可能性があるため、代わりにパスにアンカーを付けてください。

sftp-upload-file と sftp-download-file は、リモートホストとローカルディスクの間でファイルをストリーミングします。バイト列も、その Base64 エンコードもモデルに渡されることはありません。レスポンスに含まれるのはバイト数と2つのパスです。

2つの転送ツールは、defaults.transferRoot を設定するまで無効であり、設定されていない場合は説明を返して処理を拒否します(sftp-list にはローカル側がないため、設定は不要です)。このディレクトリが、ツールがローカルでアクセスできる範囲のすべてです。

[defaults]
transferRoot = "/srv/ssh-mcp-transfers"
transferMaxBytes = 268435456        # 256MB per transfer
transferTimeoutMs = 300000          # 5min with no bytes moving

毎回の呼び出しで検証され、いずれかの条件を満たさない場合は、機能を縮小して続行するのではなく拒否されます。ディレクトリは 0700 で、サーバーを実行しているアカウントが所有し、グループまたは他のユーザーが書き込み可能な親ディレクトリが存在してはなりません。また、ssh-mcp のインストール先、設定ディレクトリ、監査ログディレクトリ、~/.ssh と重複してはなりません。呼び出し元が指定したパスはこのディレクトリ内に制限され、シンボリックリンクはたどらずに拒否されます。ダウンロードは同じディレクトリ内の .part ファイルに一時保存してからアトミックに公開されるため、転送に失敗しても転送先に不完全なファイルが残ることはありません。

transferTimeoutMs は合計時間ではなく、アイドル時間の上限です。1回のメタデータの往復、またはバイトが転送されないコピーの時間を制限し、進捗があればタイマーがリセットされます。低速でも転送が継続している大きなファイルはこの上限を超えて転送できますが、チャネルが停止した場合は1つの時間枠内に失敗します。このため、上記のバイト数上限をこの値で割る必要はありません。合計時間の上限だった場合、256MB の上限は「リンク速度が 900 KB/s を維持できる場合のみ」という意味になってしまいます。

Windows では利用できません。転送ルートがプライベートであることをまだ検証できないため、他のアカウントから読み取り可能な可能性があるディレクトリへの書き込みを避け、2つの転送ツールは処理を拒否します。

sftp-upload-file は省略可能な mode(1~511)を受け取ります。setuid、setgid、スティッキービットは拒否されます。省略した場合、新しいリモートファイルは 0600 で公開されます。既存ファイルを上書きする場合は、置き換えられるファイルのパーミッションビットを引き継ぎます。引き継ぐのはパーミッションビットのみで、setuid や setgid は引き継ぎません。

転送の承認対象には、次の引数も含まれます。ポリシーエンジンが分類する文字列、人間が読む承認プロンプト、監査レコードのすべてに、指定されていれば --overwrite と --mode が記載されます。そのため、あるパスへのアップロードを承認しても、同じパスへの別のアップロードは承認されません。また、承認許可(approvalGrantTtlMs)を異なるモードで再利用することもできません。

インタラクティブセッション

セッションはコマンド間で状態(CWD、環境変数)を維持します。

Agent: open-session(name="deploy", type="interactive")
Agent: run-command(session="deploy", command="cd /opt/myapp")
Agent: run-command(session="deploy", command="git pull")    # runs in /opt/myapp
Agent: run-command(session="deploy", command="npm ci")      # CWD persists
Agent: close-session(name="deploy")
バックグラウンドセッション

長時間実行されるプロセス(ログ、ビルド):

Agent: open-session(name="logs", type="background", command="tail -f /var/log/syslog")
Agent: read-session-output(name="logs", lines=20)   # poll
Agent: close-session(name="logs")
リモートホストのサポート

Linux(Debian/bash、Alpine/busybox ash)、Dropbear、Windows 11 上の Windows OpenSSH でテスト済みです。

Linux / BSD / macOSWindows OpenSSH
read-command、run-command、privileged-command、signal-process✅✅
sftp-upload、sftp-download✅✅
sftp-list✅✅
sftp-upload-file、sftp-download-file✅❌(ローカル側で検証できません)
バックグラウンドセッション✅✅
インタラクティブセッション✅❌

インタラクティブセッションには POSIX シェルが必要です(sh、bash、ash、zsh)。各コマンドを printf マーカーで囲み、末尾から $? と $PWD を読み取る仕組みですが、cmd.exe(Windows OpenSSH のデフォルトシェル)にはこれらがありません。そのようなホストに対してセッションを開こうとすると、タイムアウトせず、明示的なエラーがすぐに返されます。それ以外はすべて通常どおり動作します。

OpenSSH の DefaultShell に PowerShell を設定しても解決しません。プロトコルは単に cmd 以外を要求するのではなく、POSIX 固有です。


設定

プロファイルのオプション
[defaults]
defaultProfile = "dev"
sessionMaxPerConnection = 5
sessionIdleTimeoutMs = 600000       # 10min
sessionBackgroundMaxMs = 3600000    # 1hr
commandTimeoutMs = 60000
commandMaxChars = 5000              # 0 = unlimited, the config spelling of --maxChars=none
commandMaxOutputBytes = 1048576     # 1MB
connectionIdleReapMs = 900000       # 15min
commandQuotaPerDay = 0              # 0 = unlimited; circuit breaker for runaway agents
approvalGrantTtlMs = 0              # 0 = always prompt; see "Approval Grants"
approvalMode = "ask-destructive"    # auto | ask-destructive | ask-all | deny
# transferRoot = "/srv/ssh-mcp-transfers"   # enables the streaming file tools; see above
transferMaxBytes = 268435456        # 256MB per streaming transfer
transferTimeoutMs = 300000          # 5min with no bytes moving (idle, not total)

[[profiles]]
name = "prod-web-1"
host = "10.0.1.50"
port = 22
user = "deploy"
auth = "agent"                      # agent | key | password | keychain
keyRef = "~/.ssh/id_ed25519"        # for auth=key
keychainEntry = "ssh-mcp/prod"      # for auth=keychain (requires @napi-rs/keyring)
via = "bastion"                     # ProxyJump — route through bastion profile
group = "prod"                      # Policy tier: prod | staging | dev, or your own (see [policy])
workdir = "/var/www"
trustedHostKey = "SHA256:..."       # Pin host key (optional)
tty = false
role = "operator"                   # viewer | operator | admin
readOnly = false
approvalPolicy = "ask-all"
cert = false                        # SSH CA cert auth — auto-detects keyRef-cert.pub
announceAgent = true                # Send AI_AGENT=ssh-mcp to this host (see below); clear it for a host you do not control
sessionMaxPerConnection = 3         # per-profile override
sessionIdleTimeoutMs = 300000       # stricter for prod
commandQuotaPerDay = 200            # per-profile override
maxChars = 2000                     # per-profile override; stricter for prod
transferMaxBytes = 16777216         # per-profile override; transferRoot is not per-profile
transferTimeoutMs = 60000           # per-profile override

# Optional. Merged over the built-in role matrix; see "Policy Engine" below.
# roleBindings is keyed by role and then by tier, so the block below changes
# operator on prod and leaves operator's other tiers, and viewer and admin,
# on their defaults.
[policy]
denylist = ["^terraform\\s+destroy", "\\.ssh/authorized_keys$"]

[policy.roleBindings.operator]
prod = ["read-only", "safe", "destructive"]

不明なセクションやキーがある場合、警告ではなく起動エラーになります。そのため、タイプミスによって、上書きしたつもりの設定がデフォルトのまま実行されることはありません。この検証はロール名とティア名にも適用されます。[policy.roleBindings]` の下に記述するロールはすべて、いずれかのプロファイルから参照できる必要があります。また、各プロファイルのロールとティアは、実際のバインディングに解決される必要があります。起動時と再読み込みのたびに、両方向の整合性が検証されます。

各プロファイルには、それぞれ固有の name が必要です。同じ名前のプロファイルが 2 つあると起動エラーになり、再読み込みも拒否されます。エラーには名前ではなく、2 つの項目のインデックス(profiles[2].name repeats profiles[0].name)が示されます。このリリース以前は、そのような設定でも起動し、先にある方が使用されていました。

ホスト側での識別

このサーバーが実行するすべてのコマンドは、環境変数 AI_AGENT=ssh-mcp を使ってホストに自身を通知します。これにより、オペレーターは、/etc/profile のスニペットや ForceCommand のラッパーなど、セッション環境を読み取るホスト上の仕組みを使って、エージェントのセッションと人間のセッションを区別できます。これは、コマンドの実行に使える 3 つのチャネル、すなわち単発コマンド、バックグラウンドセッション、インタラクティブセッションすべてに適用されます。SFTP 転送には適用されません。このサーバーが使用する SSH ライブラリでは、サブシステムチャネル上で環境変数のリクエストを送信できないためです。

ホストが許可した場合にのみ、この変数がセッションに反映されます。 sshd_config に AcceptEnv AI_AGENT が設定されていない場合、sshd は変数を無視し、セッション環境は変更されません。これは、AcceptEnv の仕組みを持たない Dropbear、およびデフォルト設定の Windows OpenSSH で確認済みです。コマンドは実行され、終了コードも変わらず、変数だけが存在しません。

ただし、許可の有無にかかわらず、すべてのホストにリクエストが送信されます。 AcceptEnv は、サーバーが何を保存するかについてのポリシーであり、このクライアントが何を送信するかを制限するものではありません。許可していないホストも、悪意のあるホストであっても、セッションを操作しているのが人間ではなくエージェントであることを通信上で確認できます。そのため、制御していないホストではプロファイルの announceAgent = false を設定し、情報を一切送信しないようにできます。デフォルトで有効なのは、恩恵を受けるのが、自分の制御下にあるホストを運用する人だからです。

意図的に、バージョンは送信しません。バージョンを送ると、悪意のあるホストに特定のビルドを絞り込まれる可能性がありますが、ツール名だけでは絞り込めません(SSH トランスポートでは、すでに識別文字列に ライブラリ のバージョンが含まれています。これは、そのライブラリを使用するすべてのプログラムで共通のバージョンです)。

この変数は、証明ではなく便宜上のラベルとして扱ってください。どの SSH クライアントでも設定でき、どのエージェントでも省略できるため、すでに信頼しているセッションの帰属を示す用途には便利ですが、認可や侵入検知の入力として使用してはいけません。確実な帰属の根拠となるのは、認証に使われた鍵またはプリンシパルです。

ProxyJump(要塞)

要塞/ジャンプサーバーの背後にある内部ホストに接続します。via フィールドには、トンネル経由で接続するプロファイル名を指定します。

[[profiles]]
name = "bastion"
host = "bastion.example.com"
user = "deploy"
auth = "agent"

[[profiles]]
name = "internal-db"
host = "10.0.1.50"                 # private IP — not directly reachable
user = "dbadmin"
auth = "key"
keyRef = "~/.ssh/db_key"
via = "bastion"                     # tunnel through bastion

エージェント転送はありません。forwardOut を介した TCP トンネルのみです。要塞への接続は維持され、複数の内部ホストで再利用できます。

SSH CA 証明書

中央の SSH 証明機関を使用するエンタープライズ環境の場合:

[[profiles]]
name = "prod-db"
host = "db.internal"
user = "admin"
auth = "key"
keyRef = "~/.ssh/id_ed25519"
cert = true                         # enable CA cert auth

証明書ファイルは、OpenSSH の規約(keyRef + -cert.pub、例:~/.ssh/id_ed25519-cert.pub)に従って自動検出されます。パスは SSH_MCP_<NAME>_CERT 環境変数で上書きできます。ssh2 の規約に従い、証明書は秘密鍵に連結されます。

認証情報の解決順序
  1. SSH エージェント(SSH_AUTH_SOCK)— プロセスメモリに鍵のマテリアルを保持しません
  2. OS キーチェーン(macOS Keychain/Windows Credential Manager/Linux Secret Service)— auth = "keychain" と @napi-rs/keyring が必要です
  3. 環境変数 — SSH_MCP_PASSWORD、SSH_MCP_KEY、SSH_MCP_SUDO_PASSWORD、またはプロファイル固有の SSH_MCP_<NAME>_PASSWORD
  4. 鍵ファイル — keyRef パスまたは SSH_MCP_KEY 環境変数

CLI 引数は使用しません。 v2 では --password、--sudoPassword、--suPassword が完全に削除されています。

再読み込み

設定ファイルは起動時に読み込まれ、その後は要求されたときにのみ再読み込みされます。action: "reload" を指定して reload-config ツールを呼び出すか、Windows 以外のすべてのプラットフォームでプロセスに SIGHUP(kill -HUP <pid>)を送信します。Windows では SIGHUP はコンソールの終了を意味するため、再読み込みは行われません。ツールを使用してください。ファイルの監視は行われないため、書き込み途中のファイルが有効なポリシーになることはありません。再読み込みでは、サーバーの起動時と同じパスが読み込まれます。--config で指定したパス、またはプラットフォームのデフォルトパスです。

reload-config は、action という 1 つの引数を取ります。

  • "status"(デフォルト)では、設定パス、有効な設定の読み込み元(file、unconfigured、または flags)、読み込み時刻、解析元のバイト列の sha256、および最後の再読み込み結果が報告されます。ファイルの内容が表示されることはありません。
  • "reload" では、ファイルを再読み込みし、すべてのチェックに合格した場合に適用して、変更内容(追加、削除、変更されたプロファイルとその名前およびフィールド、ポリシールールが変更されたかどうか、切断した接続、その理由、および関連するセッション)が報告されます。同じレポートが stderr にも出力されます。

このツールでは承認は求められず、コマンドのクォータも消費しません。呼び出し元がサーバーに要求できるのは、オペレーターが書き込んだ内容を再読み込みさせることだけです。ファイルがオペレーターの同意を示します。

すべて成功するか、何も変更されないかのどちらかです。 再読み込みでは、何かを変更する前に、新しいファイルに対して起動時と同じチェックが行われます。権限チェック(Linux と macOS ではモードビット、Windows では ACL。サーバーの起動時に指定された --strictConfigAcl または --allowUncheckedConfigAcl を使用)、TOML の解析、厳密なスキーマ、プロファイル名の重複チェック、ロールとティアの相互チェック、拒否リストのコンパイルです。プロファイルがないファイルは起動時と同様にスキーマチェックに失敗するため、再読み込みによってサーバーが未設定の状態になることはありません。再読み込み時に限り、さらに 2 つの理由で拒否されます。

  • defaults.transferRoot が変更または削除されている場合。 転送処理中は古いルートが保持されるため、変更には再起動が必要です。ルートがなかった場合に transferRoot を追加することは許可されます。ルートがなければ、処理中の転送は存在し得ないためです。
  • サーバーが --host/--user フラグで起動されている場合。 フラグから作成されたプロファイルをデフォルトパスにあるファイルの内容に置き換えると、オペレーターが選択していないのにサーバーの信頼元が変わってしまいます。設定ファイルを指定して再起動してください。

再読み込みが拒否された場合、設定、ポリシー、接続、セッション、承認の付与は何も変更されません。拒否理由にはエラーのクラスとパスが示され、スキーマエラーの場合はフィールドのパスと問題コードも示されます。ファイル内の値が引用されることはありません。

適用されるタイミング。 すべてのチェックに合格すると、新しい設定が一度に古い設定と置き換わるため、ツール呼び出しが両方の設定の一部ずつを見ることはありません。プロファイル名ごとに、開いている各接続について:

プロファイルで変更された項目影響
role、group、approvalPolicy、readOnly、commandQuotaPerDay、maxChars、transferMaxBytes、transferTimeoutMs、または [policy] セクション接続は開いたままです。変更は次のコマンドから適用されます
workdir、tty、timeout、maxOutputBytes、announceAgent、sessionMaxPerConnection、sessionIdleTimeoutMs、sessionBackgroundMaxMs接続は開いたままです。新しい値は新しいセッションとコマンドに適用され、レポートにはこれらの項目が記載されます
host、port、user、auth、keyRef、keychainEntry、cert、via、trustedHostKey接続はセッションとともに閉じられます。次の呼び出しでは新しいプロファイルで接続します
プロファイルが削除された接続はセッションとともに閉じられます

踏み台経由の接続は、その踏み台が閉じられると閉じられます。さらに、その接続経由で実行されているものも同様に閉じられます。依存先は、経由する踏み台より先に閉じられます。[defaults] の値は、独自の値を設定していないすべてのプロファイルに適用されるため、変更すると、それらのプロファイルすべてが変更対象となります。defaultProfile、connectionIdleReapMs、approvalGrantTtlMs は即座に適用されます。コマンドクォータの使用回数は保持されるため、新しい commandQuotaPerDay は、それまでの使用分を差し引いた残りに適用されます。初回接続時に受け入れられたホストキーも保持されます。接続レジストリは再構築されず、その場で更新されるためです。再読み込み時に接続処理中だった新しい接続は、そのプロファイルが閉じられる場合(削除された場合、接続関連の項目が変更された場合、または踏み台が閉じられる場合)に限り破棄されます。その呼び出しは、"The configuration was reloaded while connecting; retry the call." というエラーで失敗します。それ以外の接続は、再読み込み後のプロファイルの状態で維持されるため、変更のないファイルを再読み込みしても、他の呼び出しの接続処理は失敗しません。

承認。 変更のないファイルであっても、再読み込みが適用されるたびに、それ以前に付与されたすべての承認許可が無効になります。そのため、許可済みコマンドを次に実行すると、再度確認が求められます。再読み込みの適用後に回答された承認は、新しいルールで再確認されます。接続がまだ開いており、新しいルールでも同じコマンドクラスで実行が許可されているか、確認を求められる場合に限り、コマンドが実行されます。それ以外の場合は CONFIG_RELOADED で拒否され、許可は記録されません。新しいルールで確認を受けるには、呼び出しを再試行してください。確認を必要としない判断も、再読み込みの適用中に行われた場合は、同じ方法で再確認されます(接続の解決中、または OPA の往復処理中など)。以前のルールで許可されていたコマンド、または許可の再利用は、接続がまだ開いており、新しいルールでも同じクラスで明示的に許可されている場合に限り実行されます。それ以外の場合は CONFIG_RELOADED で拒否されます。コマンドクォータは、再読み込み後のプロファイルに対して計上されます。

実行中の処理。 再読み込み時に実行中のコマンドや転送は、接続が再読み込みによって閉じられない限り、開始時点のルールで完了します。次の呼び出しには新しいルールが適用されます。プロファイルを削除して再読み込みすると接続は閉じられますが、停止できる処理は想像されるほど多くありません。バックグラウンドセッションのコマンドには INT、次に TERM、最後に KILL が送信されますが、単発のコマンドや対話型セッション内で実行中のコマンドには何も送信されず、ホスト上で実行され続ける場合があります。そのようなコマンドを停止するには、プロファイルでまだホストに接続できるうちに、まず signal-process を使用してください(SECURITY.md を参照)。

再読み込みされない項目。 コマンドラインフラグの値は、プロセスの起動時のまま維持されます。これには、トランスポートとその HTTP オプション、--hostKeyMode、OPA、監査、OpenTelemetry の各フラグ、ACL フラグ、--config 自体が含まれます。[defaults] の代わりとなるフラグ(--timeout、--maxChars、--sessionMax、--sessionTtl、--commandQuota、--approvalGrantTtl)は、起動時のようにファイルが読み込まれていない間だけ適用されます。そのため、設定なしで起動したサーバーは、再読み込みでファイルを読み込むと、そのファイルからデフォルト値を取得します。

監査。 実行された再読み込みはすべて、次の3つの形式のいずれかで config:reload レコードを書き込みます。

結果decisionruleIderror
適用済みallowconfig-reload未設定
拒否:チェックに失敗し、変更はありませんdenyconfig-reload拒否理由
入れ替え開始後に失敗したため、一部適用された可能性がありますdenyconfig-reload-partialエラーの種類と、一部適用された可能性があること

その他のフィールドは固定です。command と binary は config:reload、commandClass は safe、profile と user は -、mcpRequestId はツール呼び出しのリクエスト ID、 またはシグナルによって開始されたリロードの場合は sighup です。リロードによって閉じられた各セッションについて、 close-session ツールと同様に、session:close … (reload) レコードも書き込まれます。閉じることができなかった接続は、 ロールバックされずに報告されます。新しい設定はすでに有効になっています。

リロードは一度に 1 つずつ実行されます。別のリロードの実行中に要求されたリロードは、その完了を待ってから ファイルを改めて読み込みます。また、そのリロードがまだ待機中に要求されたリロードはそこに加わり、 レポートとレコードを共有するため、リクエストや SIGHUP が連続しても、リロードは最大 2 回で済みます。 チェックの通過後に(拒否ではなく、欠陥によって)問題が発生した場合、レポートには、入れ替え開始後にリロードが失敗し、 一部適用された可能性があると記載されます。reload-config のステータスを確認し、新しい設定が反映されていない場合は 再起動してください。

SIGHUP でサーバーが停止することはなくなりました。 以前は、デフォルトの動作によって 監査ログをフラッシュせずにプロセスが終了していました。現在は、Windows 以外のすべての環境で、 SIGHUP によってリロードが開始され、サーバーは実行を続けます。stdio サーバーは引き続き、 クライアントが標準入力を閉じると終了します。ターミナルから起動した HTTP サーバーは、そのターミナルが閉じても、 送信されるハングアップがリロードとして処理されるため、停止しなくなりました。停止するには SIGTERM または SIGINT を送信するか、スーパーバイザーの下で実行してください。


ポリシーエンジン

ロール
ロール開発ステージング本番
viewer読み取り専用読み取り専用読み取り専用
operator読み取り専用、安全、破壊的読み取り専用、安全、破壊的読み取り専用、安全
adminすべてすべて読み取り専用、安全、破壊的

どの列が適用されるかは、プロファイルの group で決まります。明示的に設定してください。 設定されていない場合、プロファイル名(prod/staging/dev、 local、test、sandbox)から階層が推測され、認識されない名前は最も厳しい階層である prod に解決されます。 そのため、web-01 という名前の本番ホストは、開発権限が誤って割り当てられることなく、本番として扱われます。

sudo への影響に注意してください。prod では admin に privileged がありません。 そのため、ここでは設計上 privileged-command は拒否されます。名前から階層を推測できず、 prod に分類されるクイックスタートプロファイルも同様です。ホストが本番ではない場合は、そのように指定してください。

npx ssh-mcp --host=10.0.0.5 --user=deploy --group=dev
[[profiles]]
name = "build-box"
group = "dev"
マトリックスの設定

上の表はデフォルトであり、上限ではありません。省略可能な [policy] セクションは起動時と各リロード時に デフォルトにマージされるため、prod と正直に設定したホストで sudo を許可する場合も、 ラベルを付け替えるのではなく、設定ファイル内のレビュー可能な項目として記述できます。

[policy.roleBindings.admin]
prod = ["read-only", "safe", "destructive", "privileged"]

マージはロールと階層の両方のレベルで行われます。このブロックが変更するのは prod の admin だけです。staging と dev の admin はデフォルトのままで、 viewer と operator には影響しません。デフォルトにないロールや階層も拒否されずに追加されます。 これにより、カスタム group は最も厳しい階層にフォールバックせず、実際のバインディングに解決されます。

[[profiles]]
name = "build-box"
role = "admin"
group = "tier-1"

[policy.roleBindings.admin]
"tier-1" = ["read-only", "safe", "destructive"]

追加の拒否パターンも同じセクションに記述され、拒否禁止リストを置き換えるのではなく、その上に適用されます。

[policy]
denylist = ["^terraform\\s+destroy", "\\.ssh/authorized_keys$"]

各パターンは、すべての呼び出しのコマンド文字列に対して評価される正規表現です。 5 つの SFTP ツールでは、リモートパス自体に対しても 3 通りの方法で評価されます。 指定されたとおりのパス、正規化したパス(// と /./ をまとめ、.. は直前のセグメントに対して解決し、 末尾の / を取り除き、\ は / として扱います)、そして大文字小文字を区別しない Windows 表記です。 拒否理由には、どのパターンに一致したかが示されます。 Windows での解釈は、Win32 がパスを解決した結果です。Windows 11(ビルド 26200)上で SFTP を使って確認したところ、大文字・小文字の違い、末尾に続くピリオドとスペース、および ::$DATA による既定ストリームの表記は、すべて同じファイルを参照します。そのため、ある表記には適用されて別の表記には適用されないルールは、区別ではなく回避手段になっていました。正規化した解釈に対して、これらの表記を取り除き、ドライブ相対パス(C:x/…)をそのドライブをルートとして扱います。実際の解決先はサーバーから見えないプロセス状態に左右されますが、ルートとして扱うのがフェイルクローズの解釈です。また、\\?\ と \\.\ のデバイスプレフィックスを取り除き、UNC ルート(\\server\share\… → //server/share/…)は保持します。大文字・小文字を区別せずにテストするため、authorized_keys に対して書かれたルールでも Authorized_Keys を検出します。POSIX のターゲットではこれらは別々のファイルですが、このエンジンが他のすべての場面で採る方針と同様、拒否する方向に倒します。

この解釈で行わないこと:

  • ターゲット上では何も解決しません。 相対パスは相対パスのままなので、/root/ を起点とするルールでは .ssh/authorized_keys は検出されません。シンボリックリンクはたどらず、8.3 短縮名(AUTHOR~1)も展開しません。どのような字句上の解釈でも、長い名前を知るにはターゲット自身のディレクトリ一覧が必要です。
  • sftp-upload-file または sftp-download-file のローカルパスを、それ単独でテストしません。 その側は transferRoot によって制限されます。ローカルパスはこれら 2 つのツールのコマンド文字列の末尾に含まれるため、文字列の末尾に固定されたパターンは、従来どおりそこで一致することがあります。

そのため、パスルールは末尾の要素を基準に固定してください。\.ssh/authorized_keys$ は、絶対パス、相対パス、Windows の表記のいずれにも一致し、それぞれの大文字・小文字、末尾のピリオド、::$DATA のバリエーションも含まれます。コマンド用に書かれたパターンも SFTP パスに対してテストされます。^rm は rmlist.txt という名前のファイルに対する sftp-download を拒否します。

パフォーマンスに関する注意点が 1 つあります。パターンは呼び出し元から渡されたパスのバイト列に対して実行される通常の正規表現であり、リモートパスの長さ制限によって制限されます。これは、コマンド文字列が以前から受けていたものと同じ影響範囲です。アンカー付きの線形パターンを優先してください。壊滅的なバックトラッキングを起こすパターンは、一致したツール呼び出しを停止させます。

ロール名とティア名は任意の文字列であるため、マージ処理だけでは、新しいカスタムロールと既存ロールのスペルミスを区別できません。起動時のクロスチェックで判別し、次のいずれの場合も、使用時ではなく起動時に失敗します。

  • read-only | safe | destructive | privileged 以外のコマンドクラス。これにより、priviledged という誤記が、何の権限も与えない設定として解析された後、コマンドが拒否された際にポリシー上の判断として扱われることを防ぎます。
  • 設定内のあらゆる未認識のセクションまたはキー。パーサーが理解できないブロックは、設定した動作が何も有効にならないまま正常に起動するのではなく、エラーになります。
  • [policy.roleBindings] のうち、どのプロファイルでも使われていないロールまたはティア。これにより、[policy.roleBindings.operater] が 4 つ目のロールとしてマージされ、制限するつもりだったプロファイルがデフォルトのまま動作し続ける事態を防ぎます。
  • バインディングのない role を持つプロファイル、またはそのロールに対応するティアのないプロファイル。これにより、誰も記述していない理由でホストが read-only になる事態を防ぎます。

最後の項目は、設定しなかったティアと設定したティアの両方に適用されます。group のないプロファイルでも、名前からグループが解決されます。その推定されたティアも、ほかのティアと同様に、プロファイルのロールに対して存在していなければなりません。

あるロールにバインディングのないティアには read-only が付与され、ほかのティアのクラスが付与されることはありません。ティア間のフォールバックはありません。マトリックスがコードに組み込まれていた頃は、prod へのフォールバックはロールの最も厳しいセルへのフォールバックを意味しましたが、現在は [policy] ブロックでそのセルを記述できます。

OPA サイドカーは、同じ権限を与える別の経路ではありません。OPA はローカルポリシーですでに許可されているコマンドについてのみ参照されるため、追加で拒否することはできますが、許可範囲を広げることはできません。許可範囲を広げる処理はここで行うか、まったく行わないかのどちらかです。

コマンドの分類

各コマンドは実行前に分類されます。

  • read-only:許可リストに含まれるコマンド(ls、cat、grep、df、stat、systemctl status、…)であり、 各引数が、そのバイナリ用に定義された文法に従うデータであると証明できる場合に限ります。詳しくは 以下を参照してください。
  • safe:破壊的ではない変更(npm install、git pull、…)
  • destructive:承認が必要な変更(rm -rf /tmp/build、…)
  • privileged:sudo、su、doas、pkexec

read-only はバイナリ名だけでなく、引数の文法によって許可されます。 許可リストに含まれるバイナリの引数リストは、src/policy/reader-grammar.ts に定義された文法に照らして、単語ごとに検査されます。文法に記載されていないオプション、その省略形、未記載の文字を含む短縮オプションの組み合わせ、引用符で囲まれていないシェルのワイルドカード(*、?、[)を含む引数、またはバイナリが書き込み先として扱うような形式のオペランドがあれば、コマンド全体が safe に分類されます。これにより、readOnly プロファイルで実行できる操作が制限されます。本番環境とステージング環境では viewer がこれを適用し(開発環境では viewer は safe も実行できます)、read-command も同じ制限に従います。これらはすべて、開発環境の viewer を除き safe を拒否するため、以前の名前だけに基づくルールで実行できたコマンドが、現在は拒否されることがあります。拒否の際には、拒否された単語が示されます(例:`journalctl` は文法に記載されたオプションとオペランドを指定した場合のみ read-only です。ここでは `--foo` は受け付けられません。)。そのため、同じコマンドをどの表記で実行できるかを確認できます。このルールに残るリスクについては、SECURITY.md を参照してください。

リーダーコマンドには、引用符で囲まれているかどうかにかかわらず、シェルのメタ文字(; & | < > ` $ ( ) { }、改行、または復帰改行)が含まれている場合も safe に分類されます。許可リストが保証するのはバイナリのみであり、シェルが行の残りの部分をどう処理するかは保証しないためです。拒否の際には、最初に見つかった文字が示されます。sftp-list と sftp-download では、対象となるのはリモートパスです。そのため、/srv/backup(old) は readOnly プロファイルでは拒否され、次のように通知されます。リモートパスに `(` が含まれています。ここではシェルのメタ文字として扱われるため、`sftp:list` はその文字を含まないパスに対してのみ read-only です。

別途定義された forbidden リストに含まれる操作は、ロールや承認ポリシーにかかわらず、決して許可されません。対象は、rm -rf /、mkfs、dd of=/dev/、shutdown、curl|sh、フォーク爆弾、/etc/cron、/etc/systemd、またはユーザーの authorized_keys を指定する書き込み、iptables -F、再帰的な chmod 777 //chown / です。アップロードツールのリモートパス、シェルのリダイレクト先、または tee、cp、mv、install、dd of= の出力先として、書き込み先にこれらのパスを指定した場合も該当します。上記のパス解釈で解決される表記であれば対象になります。2 つの /etc 系のパターンはプレフィックスで照合されるため、/etc/crontab は /etc/cron.d 以下のすべてと同様に対象となり、ディレクトリ名で始まるだけの兄弟パス(例:/etc/cronography)も、フェイルクローズの方針により拒否されます。これ以外の方法で指定される書き込み(sed -i、rsync、cmd.exe の copy、move、robocopy、インタープリターに渡されるプログラム、対象プログラムが計算するパスなど)はこのリストに含まれません。これらは分類と承認に従い、オペレーターの拒否リストで制限できます。独自のパターンはポリシーの拒否リストに追加してください。無効なパターンは、サイレントに制限が弱まることなく、起動時にエラーになります。

承認モード
  • auto — プロンプトを表示しません(開発時のみ!)
  • ask-destructive — 破壊的な操作/特権操作ではプロンプトを表示します(デフォルト)。対象は名前から受ける印象より限定的です。 禁止リストに該当しない範囲で、destructive に分類されるのは、rm -rf /path のパターン、書き込み/実行フラグを指定した find、解決できないコマンド語、このサーバーが読み取れないプログラムをインタープリターに渡すケース(python3 -c、perl -e、node -e、パイプで渡されるプログラム。ただし、プログラムを読み取らない awk は除く)、および sftp-upload/対話型の open-session です。権限昇格は privileged に分類され、こちらもプロンプトを表示します。 通常の書き込み、サービス制御、シグナルは対象外です。本番環境で利用する前に、 SECURITY.md を確認してください。
  • ask-all — すべてのコマンドでプロンプトを表示します
  • deny — 破壊的な操作/特権操作をプロンプトなしで拒否します
承認グラント(必要時に付与)

approvalGrantTtlMs を設定すると、1 回の明示的な承認で、同じプロファイル上の完全に同一のコマンドを、指定した時間内に繰り返し実行できます(例:5 分間なら 300000)。これは、反復作業中に数秒おきに rm -rf /tmp/build を承認すると、プロンプトを考えずにクリックする習慣がついてしまうためです。意図的に選んだグラントよりも、そのほうが安全性を損ないます。

グラントはコマンド文字列、プロファイル、コマンドクラスの完全一致に紐づきます。rm -rf /tmp/build を承認しても、rm -rf /tmp/build-prod、別のホストでの同じコマンド、sudo で権限を昇格させた同じコマンドには適用されません。グラントが適用された実行は、監査ログに approver: "jit-grant" と記録されるため、新たに人が承認した実行と区別できます。

デフォルトでは無効(0 = 毎回プロンプトを表示)。自動承認は、破壊的なコマンドを安全にするためのゲートを弱めるため、有効にするかどうかは慎重に判断してください。

プロンプトへの回答

承認には MCP のエリシテーションリクエストを使用するため、表示されるのはクライアントのダイアログです。承認するとコマンドが許可されます。入力する項目はほかにありません。

回答期限は10 分です。期限を過ぎるとリクエストは失効し、コマンドは保留のままにならず拒否され、その旨が拒否メッセージに表示されます。その時点でもクライアント上でプロンプトが開いたままの場合は、準備ができてからコマンドを再実行してください。クライアントがエリシテーションにまったく対応していない場合は、原因を示す APPROVAL_UNAVAILABLE とともに、破壊的な操作と特権操作がすべて拒否されます。設計上、承認に失敗した場合は安全側に倒します。

コマンドクォータ

commandQuotaPerDay は、プロファイルが直近 24 時間に実行できるコマンド数を制限します(0 = 無制限)。承認ゲートは破壊的なコマンドを停止し、HTTP レートリミッターはリクエスト頻度を制限しますが、どちらも作業の総量は制限しません。許可されたコマンドをプロンプトインジェクションされたエージェントがループ実行しても、両方の制限をすり抜けてしまいます。この場合のサーキットブレーカーとなるのがクォータです。

コマンドの実行がポリシーで許可された後、実行前にカウントされるため、拒否されたコマンドでクォータは消費されません。ウィンドウは午前 0 時にリセットされず、スライドします。午前 0 時のリセットでは、エージェントがリセット直前にクォータを使い切り、直後に再びクォータを使えるためです。

外部ポリシーエンジン(OPA)

Open Policy Agent/Rego を標準としている組織向けです。

ssh-mcp --opaUrl=http://localhost:8181

--opaUrl を設定すると、組み込みエンジンが許可するコマンドは、追加で OPA による評価を受けます。OPA は許可範囲を狭めることしかできません。 組み込みエンジンがすでに拒否したコマンドは、OPA に問い合わせずにその拒否が返されるため、サイドカーが allow と回答しても、ロールバインディングで許可されていないクラスへのアクセスは許可されません。許可範囲を広げるには、[policy] を編集してください。

障害時はローカルの判定にフォールバックし、1 分ごとに警告を 1 回ログに記録します。OPA は追加の拒否レイヤーであり、すべての作業を停止させるほうが重大な障害となるため、これがデフォルトの動作です。ただし、OPA を認可ゲートとして導入した運用担当者は、障害中にそのゲートを失います。唯一の兆候は、通常 MCP クライアントが破棄する stderr の 1 行だけです。--opaFailClosed を指定すると、ゲートが停止している場合は拒否されます。この拒否には ruleId: "opa-unavailable" が含まれるため、監査記録にはポリシーがコマンドを拒否したのではなく、ゲートが停止していたことが記録されます。

リクエストの形式は、AuthZEN Access Evaluation コントラクトを参考にしています。

{
  "input": {
    "subject": { "role": "operator", "profile": "prod-web-1" },
    "action": { "tool": "run-command", "commandClass": "destructive" },
    "resource": { "command": "rm -rf /tmp/cache", "binary": "rm", "host": "10.0.1.50" },
    "context": { "readOnly": false }
  }
}

5 つの SFTP ツールでは、resource に 3 つのパスキーも含まれます。remotePath は指定されたとおりのパス、remotePathNormalized と remotePathWindows はローカルの拒否リストがテストする 2 種類の表記です。そのため、ルールでは command を解析したり、Rego でいずれかの表記を再実装したりせずに、正規化された表記に一致させることができます。2 つのアップロードツールには remoteWrite: true も追加されます。これはローカルポリシーが決して許可しない書き込みパスに対して拒否する根拠となる情報で、ルールでは command を解析せずに書き込みを制御できます。それ以外のツールではこれら 4 つのキーは省略され、パスが空の場合も同様です。remotePath は指定されたとおりの値です。/root//.ssh/x はそのままの表記で渡されるため、ディレクトリに対する startswith ルールでは一致しません。末尾のセグメントに一致させるか、正規化されたキーを使用してください。remotePathWindows では指定された大文字・小文字が保持されます。大文字・小文字を区別しないルールでは、ポリシー内で両辺に lower() を適用してください。resource オブジェクト全体ではなく、フィールドに一致させてください。キーはツールによって異なります。

OPA は { "result": true/false } を返します。OPA が拒否した場合(result: false)、組み込みエンジンが許可していてもコマンドはブロックされます。OPA に到達できない場合、デフォルトでは組み込みエンジンの判定が適用されます(アクセスを締め出さないためのフェイルオープン)。--opaFailClosed を指定すると、代わりに拒否されます。ブール値の result を含まない 200 応答は、到達不能として扱われます。これは OPA が未定義のドキュメントに対して返す応答であり、パッケージ名の誤りやバンドルの未アクティブ化は、同意ではなく障害として扱われます。

Rego ポリシーの例(ssh-mcp.rego):

package ssh.mcp

default allow := false

# Admins pass the OPA gate on dev hosts. The built-in policy still applies on
# top: this widens nothing that the role bindings withhold.
allow if {
  input.subject.role == "admin"
  startswith(input.subject.profile, "dev")
}

# Deny all destructive commands on prod
deny if {
  input.action.commandClass == "destructive"
  startswith(input.subject.profile, "prod")
}

セキュリティ

脅威モデル

脅威モデルの全文、脆弱性の報告ポリシー、デプロイのチェックリストについては、SECURITY.md を参照してください。

サプライチェーン

リリースには署名済みの証明書が付属し、Sigstore を通じて公開され、公開の透明性ログに記録されます。これらは異なる 2 つのストアに保存されており、検証方法はその保存先によって決まります。

証明書Predicate保存先対象
ビルド来歴 — SLSA ビルド レベル 2slsa.dev/provenance/v1npmすべてのリリース
SBOM — CycloneDX と SPDXcyclonedx.org/bom、spdx.dev/DocumentGitHubv2.4.0 以降のリリース

ビルド来歴は npm の信頼された公開によるものです。リリースのワークフローは、有効期間の短い OIDC トークンで認証し、保存済みの認証情報を使いません。そのため、漏えいする可能性のある長期有効な npm トークンはありません。

npm audit signatures        # provenance, against an installed tree

npm pack ssh-mcp            # the SBOM attestation is bound to the tarball, so fetch it
gh attestation verify ssh-mcp-*.tgz --repo tufantunc/ssh-mcp --predicate-type https://cyclonedx.org/bom

最後のコマンドにある 2 つのフラグは、どちらも不可欠です。gh attestation verify はデフォルトで SLSA の Predicate を指定するため、--predicate-type がないと SBOM が除外され、何も見つからなかったと報告されます。また、代わりに検索されるビルド来歴は npm のストアにあり、--repo が検索する GitHub のストアにはありません。SPDX のものには https://spdx.dev/Document を使用してください。

どちらの SBOM も、検証用ではなく閲覧用として、各GitHub リリースに添付されています。

レベル 2 であり、3 ではありません。 ビルド来歴には汎用の GitHub ホストランナーが署名します。builder.id は https://github.com/actions/runner/github-hosted であり、ビルド自体が影響を及ぼせるものです。ビルドレベル 3 には、ビルドから影響を受けない隔離されたビルダーが必要です。レベル 3 の達成は、現時点では信頼された公開と両立しません。この設定がデフォルトのまま有効になっていると、npm は独自のビルド来歴を生成し、外部で生成されたものを無視するためです。そのため、現在レベル 3 を実現するには長期有効な npm トークンに戻る必要があり、上記の性質をレベルの数字と引き換えることになります。

安全なデフォルト設定
  • すべての例で非 root ユーザーを使用
  • ホスト鍵のTOFU 検証(初回接続時に受け入れ、その後に検証します。1 つのプロセス内で有効です。SECURITY.md を参照)
  • RFC 9142 のアルゴリズム許可リスト(SHA-1、CBC、ssh-rsa は使用しません)
  • 権限昇格には exec() のみ(privileged-command は session を受け取らないため、sudo は永続シェル内で実行されず、PTY の漏えいを防ぎます。通常のコマンドには対話型セッションがあります。この項目は権限昇格についてのものであり、すべての実行経路が exec チャネルであるという意味ではありません)
  • 標準入力経由で sudo を実行(argv ではなく標準入力を使用し、プロセス一覧への漏えいを防ぎます)
  • サニタイザーがすべてのメタデータから CR/LF/NUL を除去
  • 監査ログに3 層の秘匿化(フィールド → 正規表現 → エントロピー)
  • CLI 引数にシークレットを含めない(環境変数、キーチェーン、または設定を使用)
  • エージェントの通知は隠さず開示 — 各コマンドは、保存を許可していないホストを含め、ホストに AI_AGENT=ssh-mcp を通知します。管理していないホストを指すプロファイルでは announceAgent をクリアしてください(ホスト側での識別を参照)
セキュリティ強化チェックリスト
  • 対象ホストに権限を制限した専用のサービスアカウントを作成する
  • NOPASSWD: ALL ではなく、コマンド別の sudoers を使用する
  • 本番プロファイルで ask-all 承認を有効にする
  • 対象ホストのネットワーク送信を制限する
  • 監視用プロファイルでは readOnly = true を使用する
  • 監査ログを定期的に確認する
  • chmod 700 <config dir> && chmod 600 config.toml を実行する(Windows:%APPDATA% 配下の ACL はすでに制限されています)

トランスポート

stdio(デフォルト)

ローカル MCP クライアント(Claude Code、Cursor、Windsurf)向けです。ネットワークには公開されません。

ssh-mcp                          # reads config from XDG path
ssh-mcp --config=/path/to.toml   # custom config path
HTTP(オプション)

TLS を使用するリバースプロキシの背後にある、リモート/ウェブクライアント向けです。

ssh-mcp --transport=http --httpPort=3000 --bearerToken=secret
ssh-mcp --transport=http --httpPort=3000 --bearerToken=secret --rateLimit=60
フラグデフォルト説明
--bearerToken必須認証用のベアラートークン(GET /health を除くすべてのルート)
--httpPort3000HTTP のリッスンポート
--httpHost127.0.0.1バインドアドレス
--rateLimit0(無効)クライアントごとの認証済みリクエストの最大数(1 分あたり、0 = 無制限)
--httpSessionTtl1800000HTTP 経由の MCP クライアントセッションのアイドルタイムアウト(ミリ秒)。リクエスト処理中、または SSE ストリームが開いているセッションは期限切れになりません。セッション数が上限の 64 に達した状態で新しいクライアントが接続すると、リクエスト処理中ではないセッションのうち、最後にアクティブだった時刻が最も古いものが退去させられます。SSE ストリームだけを保持しているセッションが退去させられるのは、ほかに候補がない場合のみです
--authFailureLimit10クライアントごとに許可されるベアラー認証の失敗回数(1 分あたり、0 = 無効)
--trustProxyfalseX-Forwarded-For からクライアントアドレスを読み取ります。ただし、接続元がプロキシの場合に限ります。値を指定しない場合は接続元がループバックであると見なします
--trustedProxies—X-Forwarded-For の送信を許可する接続元アドレスをカンマ区切りで指定します。空の場合はループバックのみが許可されます

エンドポイント:POST /(MCP Streamable HTTP)、GET /status、GET /health

GET /health は {"healthy": true, "configured": <bool>} を返します。どちらの場合もステータスは 200 のままです。healthy は稼働状態を示し、プロファイルが設定されていない場合は configured が false になります。これは、設定のバインドマウントが気付かれないままアタッチされなかったケースに該当します。その場合、サーバーはポートをバインドしますが、reload-config 以外のすべてのツール呼び出しを拒否します。ファイルの読み込みを伴うリロードが行われると、configured は true になります。プロファイル一覧自体を返す GET /status には、引き続きベアラートークンが必要です。

--rateLimit は、以下の認証失敗の上限と同じ方法で識別されるクライアントごとの利用枠です。認証済みのリクエストはすべてこの枠を消費します。対象は POST / とそのほかの MCP ルート、GET /status、および存在しないパスへの認証済みリクエストです。GET /health は消費しません。トークンが誤っている場合も消費しないため、未認証のトラフィックによって正常に動作しているクライアントの利用枠が消費されることはありません。クライアントが利用枠を使い切ると、サーバーは JSON-RPC エラーボディーとともに HTTP 429 を返し、Retry-After でそのクライアントの次のリクエストが受け付けられるまでの秒数を通知します。

認証失敗は別途スロットリングされ、デフォルトで有効です。認証チェックはリミッターに達する前に応答するため、--rateLimit は誤ったベアラートークンを検知できず、トークンの推測はネットワーク速度で実行できていました。--authFailureLimit はクライアントごとに独立した小さな利用枠を設定し、401 の場合にのみ消費します。正しいトークンでは消費されないため、正常に動作しているクライアント自身がスロットリングされることはありません。アドレスが利用枠を使い切ると、正しいトークンを含むものも含め、そのアドレスからのすべてのリクエストが待たされます。これは意図的な動作です。推測に応答すると、呼び出し元にどのトークンが正しいかを知らせてしまうためです。この利用枠と --rateLimit のどちらも、クライアントはソケットアドレスで区別されます。リバースプロキシの背後では、すべてのクライアントが各利用枠を共有することになります。そのため、プロキシが自分の管理下にある場合は --trustProxy を設定してください。設定しないまま X-Forwarded-For を初めて検知すると、サーバーは警告を出力します。--trustProxy は X-Forwarded-For の 最も右側 のエントリを使用します。これはプロキシ自身が追加したホップです。その左側の情報はすべてクライアントに由来するため、最も左側を読み取ると、クライアントが自分の利用枠を選んだり、他人の利用枠を消費したりできてしまいます。ただし、これはプロキシが実際にエントリを追加した場合に限ります。そのため、ヘッダーを読み取るのは 接続元がプロキシの場合のみ です。値を指定しない --trustProxy は接続元がループバックであることを意味し、これは上記のデプロイ構成に該当します。それ以外の場所にあるプロキシを指定するには、--trustedProxies を使用してください。ヘッダーをアドレスとして読み取れない場合、または接続元が信頼されていない場合、サーバーはソケットアドレスにフォールバックし、その旨を一度だけ通知します。そのため、フラグが有効になっていないことが見過ごされることはありません。信頼するホップは 1 つと想定しています。不正な --authFailureLimit は認証チェックを黙って無効にするのではなく、起動時に拒否されます。無効にできるのは 0 を指定した場合のみです。 IPv6 クライアントは、2 種類の予算のどちらについても、完全なアドレスではなく /64 で識別されます。 /64 は通常、加入者に割り当てられる最小のブロックであるため、同じ /64 を共有するホスト(家庭やオフィスのネットワーク、またはホスティングプロバイダーが 1 つの /64 に配置した顧客)は、1 つの IPv4 NAT 配下のホストと同様に、予算を共有します。IPv4 アドレスを含む IPv6 アドレス(IPv4 マップ形式、または NAT64 プレフィックス 64:ff9b::/96)は、その IPv4 クライアントとして扱われます。独自のプレフィックスを使用する NAT64 または SIIT トランスレーターは認識されません。この場合、変換されるすべての IPv4 クライアントがそのプレフィックスの /64 を共有するため、予算も 1 つだけになります。

必ず TLS をリバースプロキシ(Caddy/nginx)で終端してください。サーバーは 127.0.0.1 のみでリッスンします。


Docker

# Build
docker build -t ssh-mcp .

# Run (config file + env vars for credentials)
docker run -i \
  -v ./config.toml:/home/appuser/.config/ssh-mcp/config.toml:ro \
  -e SSH_MCP_PASSWORD=secret \
  ssh-mcp

または docker-compose を使用します。

docker-compose --profile app up

Docker イメージは、最小構成の node:22-slim をベースとし、非 root の UID 65532 で実行されます。


CLI フラグ(v2)

シークレットを CLI 引数として渡すことはありません。

フラグデフォルト説明
--configplatform config dir (see Configure)TOML 設定ファイルへのパス
--host—クイックスタート:SSH ホスト(単一プロファイルの設定を作成)
--user—クイックスタート:SSH ユーザー名
--port22クイックスタート:SSH ポート
--key—クイックスタート:秘密鍵へのパス
--workdir—クイックスタート:コマンドとセッションの作業ディレクトリ
--groupprodクイックスタート:ポリシー階層 — prod、staging、または dev
--timeout60000コマンドのタイムアウト(ms)
--maxChars5000コマンドの最大長(none または 0 で制限を無効化します。設定ファイルでは同じ設定を commandMaxChars = 0 と指定します)
--sessionMax5接続ごとの同時セッションの最大数
--sessionTtl600000SSH セッションのアイドルタイムアウト(ms)
--transportstdiostdio または http
--httpPort3000HTTP トランスポートのポート
--httpHost127.0.0.1HTTP のバインドアドレス
--bearerToken—HTTP トランスポート認証用のベアラートークン(--transport=http で必須)
--rateLimit0クライアントごとの、認証済みリクエストの1分あたりの最大数(0 = 無制限)
--httpSessionTtl1800000HTTP 経由の MCP クライアントセッションのアイドルタイムアウト(ms)。リクエスト処理中、または SSE ストリームが開いているセッションは期限切れになりません。64 セッションの上限に達した状態で新しいクライアントが接続すると、リクエスト処理中ではないセッションのうち、最後にアクティブだった時刻が最も古いものを退去させます。SSE ストリームを保持しているセッションを退去させるのは、ほかに選択肢がない場合に限ります
--authFailureLimit10クライアントごとに許可される、1分あたりのベアラー認証失敗回数(0 = 無効)
--trustProxyfalseピアがプロキシの場合に限り、X-Forwarded-For からクライアントアドレスを読み取ります。単独で指定した場合はループバックのピアを意味します
--trustedProxies—X-Forwarded-For の送信を許可するピアアドレスをカンマ区切りで指定します。空の場合はループバックのみ
--allowedHostsbind address + localhostDNS リバインディングガードが受け入れる Host ヘッダーをカンマ区切りで指定します
--hostKeyModetofutofu | strict | insecure。strict は trustedHostKey でピン留めされたホストのみを受け入れます。SECURITY.md を参照してください
--insecureHostKeyfalsetrustedHostKey がないホストのホストキー検証を無効にします。ピンがある場合は引き続き拒否します(テスト専用!)
--allowUncheckedConfigAclfalseWindows:すべての ACL 検出事項を報告し、いずれも拒否しません
--strictConfigAclfalseWindows:読み取り専用の過剰許可を含め、すべての ACL 検出事項を拒否します
--disableApprovalfalse承認ゲートをスキップします(クイックスタートプロファイルのみ)
--opaUrl—外部ポリシー用の OPA サイドカー URL
--opaFailClosedfalseOPA に到達できない場合、ローカルポリシーにフォールバックせず、すべてのコマンドを拒否します
--opaTimeoutMs10000OPA サイドカーの応答を待つ時間です。短くするとフェイルオープンに早く到達し、長くすると障害の検知が遅くなります
--commandQuota0 (off)プロファイルごとの、ローリング24時間あたりのコマンド最大数
--approvalGrantTtl0 (off)承認後、このミリ秒数の間、同一のコマンドを自動承認します
--auditEntropyScanfalse監査でエントロピーに基づくシークレットスキャンを有効にします
--auditTamperEvidentfalseハッシュチェーンによる改ざん検知可能な監査ログを有効にします
--otelEndpoint—OpenTelemetry トレース用の OTLP/HTTP エンドポイント
--otelServiceNamessh-mcpトレーススパンで報告するサービス名
--dumpToolHashes—ツールの説明の SHA-256 ハッシュを出力して終了します

v1 からの移行

v2 は破壊的変更を含むリリースです。削除されたフラグを渡すと、わかりにくい認証エラーが後で発生するのではなく、置き換え先を示すエラーで起動時に失敗します。

ツール
v1v2備考
execread-command許可リストに登録された読み取り専用コマンドです。読み取りにはこちらを優先してください。
execrun-command任意のコマンドです。破壊的なコマンドや特権を必要とするコマンドは、approvalPolicy = "auto" の場合を除き、承認ゲートを通ります。
sudo-execprivileged-commandapprovalPolicy = "auto" の場合を除き、承認が必要です。パスワードは stdin 経由で渡されます。
description parameter—削除されました。インジェクションの経路 (#44) であり、ホストに届くこともありませんでした。

コマンドの結果にステータスが含まれるようになりました。 v1 では、コマンドが失敗すると Error (code N) で拒否されていました。v2 では、ゼロ以外の終了コードは、終了コードと stderr を含むエラー結果として返されるため、空の応答が「成功した」ことを意味するとは限りません。

フラグ
v1 のフラグ代替手段
--passwordSSH_MCP_PASSWORD 環境変数(または SSH_MCP_<PROFILE>_PASSWORD)
--suPasswordSSH_MCP_SUDO_PASSWORD 環境変数
--sudoPasswordSSH_MCP_SUDO_PASSWORD 環境変数
--disableSudoprivileged クラスを許可しないロール/ポリシーを使用します

CLI 引数は Linux の /proc/<pid>/cmdline 経由で誰でも読み取れるため (CWE-214)、認証情報はコマンドラインから移動しました。認証情報は、SSH エージェント → OS キーチェーン → 環境変数 → キーファイルの順に解決されます。

例
// v1
{ "command": "npx", "args": ["ssh-mcp", "--host=1.2.3.4", "--user=root", "--password=hunter2"] }

// v2 — credentials via env
{
  "command": "npx",
  "args": ["ssh-mcp", "--host=1.2.3.4", "--user=root"],
  "env": { "SSH_MCP_PASSWORD": "hunter2" }
}

複数のホストを使用する場合は、TOML 設定ファイルに移行し(設定を参照)、--config <path> を渡してください。プロファイルにはホストごとのロールと承認ポリシーを設定できます。

ホストキーの検証

v1 ではホストキーを検証しませんでした。v2 では、初回接続時に信頼する方式をデフォルトとし、キーをプロセスの実行中はメモリ内に記録します。同じプロセス内で後から不一致が発生すると、接続は失敗します。ディスクには何も書き込まれず、~/.ssh/known_hosts も参照されないため、再起動後は新たに受け入れます。詳しくは SECURITY.mdを参照してください。プロファイルで trustedHostKey を明示的に固定してください。これは再起動後も有効な、この項目で唯一の制御です。ホストキーのモードによって上書きされることはないため、--insecureHostKey で無効化できるのは、キーを固定していないホストのみです(テスト環境専用)。


テスト

# Start test SSH servers. --build matters: compose reuses an existing image, and a
# fixture change (such as the alpine target's AcceptEnv line) then never lands,
# which shows up as a product failure in the integration suite.
docker compose --profile test up -d --build

# Run all tests
npm test

# Run only unit tests
npm test -- test/unit/

# Run with coverage
npm run coverage

MCP Inspector

npm run inspect

コントリビューション

CONTRIBUTING.mdを参照してください。すべての PR でセキュリティチェックリストに従ってください。

サポート

SSH MCP Server が役に立った場合は、リポジトリにスターを付けるか、スポンサーになっていただけると幸いです。

掲載先

Glama の SSH MCP Server

公式 MCP レジストリにも io.github.tufantunc/ssh-mcp として掲載されています。

コメント

MCP は初めてですか?

プロトコルの概要から最初のサーバーの導入まで、よくある疑問にまとめて答えます。

AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。