ssh-mcp-server
给 AI 一把带保险栓的服务器钥匙 🔑
让 ZCode / Claude Code / Codex 等任何 MCP 客户端安全地登上你的服务器执行命令、传文件、跑任务—— 而服务器的 22 端口,从头到尾不需要对 AI(或互联网)开放。
┌──────────────┐ HTTPS + Bearer Token ┌──────────────────────┐ SSH(22) ┌──────────────┐
│ AI 客户端 │ ──────────────────────→ │ Nginx → 本网关(单文件)│ ─────────────────→ │ 内网服务器 ×N │
│ ZCode/Claude │ 唯一暴露的口子 │ 部署在任一台可达机器上 │ 22 只对网关放行 │ 低权限账号落地 │
└──────────────┘ └──────────────────────┘ └──────────────┘
三个一句话卖点:
- 🚪 AI 与服务器物理隔离——AI 只认识网关的 443 端口;目标机 22 端口防火墙只对网关放行,AI 所在的网络被攻破也摸不到服务器
- 🧰 六个 MCP 工具 + 两个 GB 级流式端点——执行命令、后台任务、上传下载一个端口全搞定,3GiB 文件恒定内存直达
- 🔐 为"AI 会犯错"设计的安全模型——命令黑名单硬拦、高危命令二次确认、敏感路径双拦、防爆破双维计数、全员审计,详见安全设计
🆚 它和传统方案的区别
一句话:堡垒机管的是"人干了什么",ssh-mcp-server 管的是"AI 能干什么、不能干什么"。它不替代堡垒机——很多团队把它部署在堡垒机之后,作为 AI 专属的那一层。
✨ 核心能力
六个 MCP 工具(一个 443 端口全部搞定):
两个流式端点(不走 MCP、不经 base64 膨胀):
POST /upload:GB 级文件流式上传,默认上限 3GiB 可调更大,恒定内存,返回远端 md5GET /download:不限大小流式下载,响应头X-Md5/X-File-Size先行供校验
其他内建能力:执行超时、输出截断(保头 60% + 尾 40%,防撑爆 AI 上下文)、SSH 并发上限、连接失败自动重试。
🤖 结合 AI 工具,能玩出什么
任何支持 MCP 的客户端都能接:ZCode、Claude Code、Codex、Cursor、Cline…… 配置一次,之后所有运维都是"说人话"。
场景 1 · 运维副驾驶——对话即操作:
「看下 web1、web2 的磁盘占用,超 80% 的把 /var/log 下 7 天前的日志清了」
AI 自己调 list_servers → 逐台 run_command → 汇总结果给结论。你不用登任何一台机器。
场景 2 · 代码改完直接上天——开发到部署零切换:
「这个 bug 修好了,发到测试机验证一下」
AI 在本地改代码 → 构建 → POST /upload 流式传包 → run_command 重启服务 → check_job/curl 验证 → 报告结果。全程没有人工 ssh,每一步都过安全防线、留审计。
场景 3 · 无人值守值班——把巡检交给定时任务:
用 ZCode 的定时任务(或系统 cron 拉起 AI 客户端)每天凌晨自动巡检:磁盘/内存/证书到期/服务存活,异常时才把报告推给你。低权限 Token 限定了它即使"抽风"也只能在白名单机器上做白名单外的事——做不了。
场景 4 · 团队分级授权——每个人、每个 AI 客户端一张专属门禁卡:
给每个同事发独立的 hmacUsers Token(每天自动换、各自 secret、各自服务器白名单),命令在各自低权限 OS 账号上落地。实习生和运维总监看到的服务器列表都不一样;谁的车钥匙丢了,吊销一张卡即可,不影响别人。
接入配置:
ZCode(~/.zcode/cli/config.json 用户级,或项目级 .zcode/config.json,改完重启生效):
{
"mcp": {
"servers": {
"ssh-gateway": {
// 方式一:网关在本机(stdio)
"command": "node",
"args": ["/opt/ssh-mcp-server/server.js"]
// 方式二:网关在远程(HTTP,推荐生产)
// "type": "streamableHttp",
// "url": "https://mcp.example.com/mcp",
// "headers": { "Authorization": "Bearer <你的token>" }
}
}
}
}
Claude Code:
claude mcp add --transport http ssh-gateway "https://mcp.example.com/mcp" \
--header "Authorization: Bearer <你的token>"
其他 MCP 客户端(Cursor / Cline / Codex 等)按各自的 MCP 配置格式填同一个 URL + 同一个 Authorization Header 即可。
🔒 安全设计:为什么敢让 AI 碰服务器
核心思路:不信任 AI 的任何一次输出。AI 生成的每一条命令都当作不可信输入,过完层层防线才落地,且落地账号本身就没有破坏力。
互联网请求
│
▼
┌─────────────────────────────────────────────────────────────┐
│ ① 传输层 Nginx 443 唯一入口(或内置 TLS)+ 可选 IP 白名单 │
├─────────────────────────────────────────────────────────────┤
│ ② 身份层 Bearer Token:多静态 / HMAC 按天轮换 / 每人独立 │
│ 失败统一 401,不泄露任何凭据状态 │
├─────────────────────────────────────────────────────────────┤
│ ③ 防爆破层 双维计数(IP×凭据) → 封 IP/封组合 → 429+Retry-After │
│ trustedProxies 防伪造头绕过 / banAllowlist 管理网段 │
├─────────────────────────────────────────────────────────────┤
│ ④ 授权层 Token 级服务器白名单:越权目标"看不见",拒绝不泄露 │
├─────────────────────────────────────────────────────────────┤
│ ⑤ 命令防线 黑名单硬拦(引号/包裹变体防绕过,内置兜底+自定义合并)│
│ 高危命令二次确认:命中默认拒绝,confirm 复核才放行 │
├─────────────────────────────────────────────────────────────┤
│ ⑥ 文件防线 路径规范化/符号链接真实路径解析/敏感文件读写双拦 │
│ allowedFileRoots 目录白名单 / 原子覆盖 / 大小上限 │
├─────────────────────────────────────────────────────────────┤
│ ⑦ 执行层 专用低权限 OS 账号 + sudo 白名单落地(损害兜底) │
│ 超时 / 输出截断 / SSH 并发上限 / 主机指纹防中间人 │
├─────────────────────────────────────────────────────────────┤
│ ⑧ 审计层 JSONL 按天:谁、从哪个 IP、对哪台、执行了什么、 │
│ 返回了什么(结果摘要)——事后可完整追责 │
└─────────────────────────────────────────────────────────────┘
每一层的关键细节
② 身份层——凭据不落配置、按天自换、可单独吊销
- 三类凭据来源并存:多静态 Token(按 id 区分身份,支持
expiresAt过期、disabled禁用、新旧并存轮换)、HMAC 派生 Token(两端只存 master secret,Token 每天自动变,无需改配置重启)、hmacUsers每人独立 secret(泄露一人只影响其自身白名单范围) SSH_MCP_TOKEN_FILE从 0600 权限文件读 Token,group/other 可写直接拒绝启动- 设计哲学 fail-closed:
expiresAt解析失败按已过期处理、三类凭据全缺拒绝启动、非法/冲突配置拒绝启动——宁可起不来,绝不带病运行
③ 防爆破层——在线暴露也不怕撞库
- 失败计数按「来源 IP × 提交凭据指纹」二元组:只换 IP 或只换 Token 都绕不过;同 IP 跨凭据累计失败达阈值升级封禁整个 IP
trustedProxies(IP/CIDR):只有 TCP 对端命中白名单才采信X-Real-IP/X-Forwarded-For(取最右项)——伪造代理头无法再洗白限速计数banAllowlist管理网段永不计数,防攻击者故意触发封禁把管理员锁在门外
⑤ 命令防线——假设 AI 总有一天会写出 rm -rf /
- 危险命令黑名单:
rm -rf /及其变体、mkfs、dd写盘、fork 炸弹、shutdown、chmod -R 777 /、kill -9 -1、清空磁盘设备……命中硬拒,confirm: true也救不回 - 内置最低兜底黑名单与自定义 config 合并生效(v0.4 修复了
bash -c "rm -rf /"类引号包裹绕过),不依赖部署方更新配置 - 高危命令二次确认:
rm -rf变体、shred、find / -delete、系统关键文件重定向等命中默认拒绝并返回命中说明,AI 复核确认(confirm: true)后才执行——误操作有人工节拍,破坏操作无豁免
⑥⑦ 文件与执行——就算防线全过,落地账号也掀不起浪
- 敏感路径黑名单读写双拦:
/etc/shadow、authorized_keys、sshd_config、/boot等 - 文件工具仅接受绝对路径,处理
../重复斜杠,在目标机解析符号链接真实路径后再校验allowedFileRoots - 上传先写临时文件再原子 rename,中断不毁原文件
- 配套
scripts/setup-limited-user.sh一键创建 AI 专用低权限账号(sudoers 强制--no-pager,防分页器逃逸提权) - SSH 主机指纹校验(SHA-256),防中间人替换目标机
- 命令超时 + 输出截断,防
tail -f拖死网关、防超大输出撑爆 AI 上下文
⑧ 审计层——每一笔都有账
audit.enabled 开启后 JSONL 按天落盘:调用者身份、来源 IP、目标服务器、命令全文、返回结果摘要(头 1024 + 尾 256 字符,可调/可关)。不记文件内容与 Token 原值,审计日志本身不成为新的泄密点。
上线前安全清单
- HTTP 模式强制 Bearer 鉴权,鉴权失败统一 401 不泄露状态
- 防爆破双维限速封禁(429 + Retry-After),代理头伪造不可绕过
- 危险命令黑名单 + 高危命令二次确认(网关侧拦截;权限隔离仍靠低权限账号兜底)
- 命令超时 / 输出截断 / 上下行大小上限 / SSH 并发上限
- 敏感路径读写双拦 + 符号链接真实路径解析 + 目录白名单
- 审计日志(可选开启,JSONL 按天,含命令全文与结果摘要)
- 移除
X-Powered-By框架指纹头,不向探测方暴露技术栈 - 给 AI 配专用低权限 SSH 账号,提权走 sudo 白名单(目标机上配,
scripts/setup-limited-user.sh一键搞定) - 目标机 22 端口防火墙只对网关机器放行
- 公网暴露务必 Nginx + HTTPS(可选再加 IP 白名单,反代开启
SSH_MCP_TRUST_PROXY=1) - 多客户端分身份发 Token(泄露即禁用单个 id),生产建议
hmacUsers每人独立 secret
🚀 快速开始(本机 stdio 模式)
npm install
cp config.example.json config.json
# 编辑 config.json:把 servers[0] 换成你的服务器(host/username/privateKeyPath,
# config.json 已 gitignore,不会误提交真实环境信息)
node test-direct.js # 直连测试(不走 MCP)
node test-mcp.js # stdio MCP 协议测试
node test-http.js # HTTP 网关模式测试(含 401 鉴权验证)
环境要求:Node.js >= 20(实测 node 18 下 MCP SDK 1.30 的 HTTP 模式会报
crypto is not defined,Ubuntu apt 版 node 尤其注意;SEA 产物自带运行时无此问题)。
📦 部署到服务器(HTTP 模式,生产推荐)
方式 A:单文件部署(推荐,服务器零安装)
打包成一个自带 Node 运行时的可执行文件(约 120MB),服务器上不需要安装 Node/npm/任何依赖。在任意 Linux 机器(或 WSL,需 Node >= 20)上执行:
bash scripts/build-linux.sh
# 产物:dist/ssh-mcp-server-linux
部署只需两个文件:
scp dist/ssh-mcp-server-linux config.json user@your-server:/opt/ssh-mcp/
# 服务器上:
cd /opt/ssh-mcp && chmod +x ssh-mcp-server-linux
SSH_MCP_TRANSPORT=http SSH_MCP_TOKEN=<你的强随机token> ./ssh-mcp-server-linux
配置文件查找顺序:SSH_MCP_CONFIG 环境变量 → 可执行文件同目录 → 当前目录。
systemd 里把 ExecStart 指向该二进制即可(其余环境变量同下表)。
打包机需要网络(npm 拉取 esbuild/postject),产物服务器完全离线可用。 Windows 版可参照脚本在 Windows 上用相同流程(postject 支持 PE 注入)。
方式 B:源码部署
# 1. 拷贝整个目录到服务器(Node.js >= 20)
npm install --omit=dev
# 2. 生成一个强随机 Token(或 node scripts/gen-token.js)
openssl rand -hex 32
# 3. 启动
SSH_MCP_TRANSPORT=http \
SSH_MCP_HOST=127.0.0.1 \
SSH_MCP_PORT=8080 \
SSH_MCP_TOKEN=<你的token> \
node server.js
环境变量
systemd 常驻
# /etc/systemd/system/ssh-mcp.service
[Unit]
Description=SSH MCP Gateway
After=network.target
[Service]
WorkingDirectory=/opt/ssh-mcp-server
ExecStart=/usr/bin/node server.js
Environment=SSH_MCP_TRANSPORT=http
Environment=SSH_MCP_HOST=127.0.0.1
Environment=SSH_MCP_PORT=8080
Environment=SSH_MCP_TOKEN=换成你的强随机token
Restart=always
[Install]
WantedBy=multi-user.target
systemctl daemon-reload && systemctl enable --now ssh-mcp
Nginx 反向代理 + HTTPS
# /etc/nginx/conf.d/ssh-mcp.conf
server {
listen 443 ssl;
server_name mcp.example.com;
ssl_certificate /etc/letsencrypt/live/mcp.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mcp.example.com/privkey.pem;
# 可选:再加一层 IP 白名单
# allow 你的出口IP;
# deny all;
location /mcp {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# MCP 可能为长连接响应,关闭缓冲
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 660s;
# 重要:upload_file 的内容走 base64(体积膨胀约 1/3),
# 默认上限 5MB 文件需要 ~10m 的 body 限制
client_max_body_size 10m;
}
# 大文件流式上传(可选启用):按 maxStreamUploadBytes 放开 body 限制
location /upload {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_request_buffering off; # 流式透传,不在 Nginx 落盘
client_max_body_size 0; # 0 = 不限;或按 maxStreamUploadBytes 设置
proxy_read_timeout 3600s;
}
}
AI 客户端最终访问:https://mcp.example.com/mcp
大文件上传 / 下载(流式端点)
MCP 的 upload_file/download_file 走 base64/JSON,只适合中小文件。GB 级文件用网关流式端点,
请求/响应体直接在网关与目标机 SFTP 之间搬运(恒定内存、不经 MCP、返回远端 md5):
TOKEN=<你的token>
# POST(curl 会流式读文件,不占内存)
curl -X POST -H "Authorization: Bearer $TOKEN" \
--data-binary @bigfile.iso \
"http://127.0.0.1:18080/upload?path=/opt/bigfile.iso"
# PUT 语义等价:curl -T bigfile.iso -H "..." "http://.../upload?path=/opt/bigfile.iso"
# 可选参数:&server=<服务器id>&mode=0600
下载同样流式(SFTP 读流直透响应,不限大小,响应头 X-Md5/X-File-Size 先行供校验):
curl -H "Authorization: Bearer $TOKEN" -o bigfile.iso \
"http://127.0.0.1:18080/download?path=/opt/bigfile.iso"
上限由 config.json 的 defaults.maxStreamUploadBytes 控制(默认 3GiB=3221225472,可调更大)。
云服务器安全组只需放行 443(或 80)一个端口——六个 MCP 工具 (run_command / check_job / kill_job / upload_file / download_file / list_servers) 与两个流式端点(/upload、/download)都经由这同一个端口完成,经 Nginx 反代即可使用。
⚙️ config.json 说明
{
"servers": [ // 多台服务器随便加
{
"id": "web1", // AI 调用时引用的 ID
"name": "官网生产机", // 备注名
"host": "10.0.0.11", // 内网地址(只要网关机器能连到)
"port": 22,
"username": "deploy", // 建议专用低权限账号
"privateKeyPath": "~/.ssh/id_ed25519",
"hostFingerprint": "SHA256:xxxx", // 可选:SSH 主机指纹校验
"allowedFileRoots": ["/opt/apps", "/tmp"] // 可选:限制文件操作目录(含符号链接真实路径)
}
],
"defaults": {
"serverId": "web1", // run_command 不传 server 时的默认目标
"timeoutMs": 30000, // 命令超时
"maxOutputBytes": 65536, // 输出截断上限(防撑爆 AI 上下文)
"maxUploadBytes": 5242880, // 单文件上传上限(5MB)
"maxDownloadBytes": 524288, // 单文件下载上限(512KB)
"maxStreamUploadBytes": 3221225472, // /upload 流式上传上限(3GiB,可调更大)
"maxConcurrentSsh": 10 // 全局 SSH 连接并发上限
},
"blockedPathPatterns": [ // 敏感路径黑名单(读写均拦截)
"^/etc/(passwd|shadow|gshadow|sudoers)",
"^/etc/ssh/sshd_config",
"authorized_keys$",
"known_hosts$",
"^/boot/"
],
"blockedCommandPatterns": [ // 危险命令黑名单(正则,忽略大小写;与内置兜底合并生效)
"rm\\s+-[a-zA-Z]*[rf][a-zA-Z]*\\s+/(\\s|$|\\*)",
"\\bmkfs(\\.\\w+)?\\b",
"\\bdd\\b[^|]*if=/dev/(zero|random)",
"\\bdd\\b[^|]*of=/dev/(sd|hd|nvme|vd)",
":\\(\\)\\s*\\{.*\\};\\s*:",
"\\b(shutdown|reboot|halt|poweroff)\\b",
"\\binit\\s+[06]\\b",
">\\s*/dev/sd[a-z]",
"chmod\\s+(-[a-zA-Z]+\\s+)*777\\s+/(\\s|$)",
"pkill\\s+(-[a-zA-Z0-9]+\\s+)*-f\\s+(['\"]{0,2})\\s*$",
"\\bkill\\s+(-9\\s+)?-1\\b",
"\\bkill\\s+--\\s+-1\\b"
],
"auth": { // HTTP 鉴权(可选;与 SSH_MCP_TOKEN 并存,全缺时拒绝启动)
"trustedProxies": ["127.0.0.1"], // 采信 X-Real-IP/XFF 的反代网段(IP/CIDR)
"banAllowlist": ["10.0.0.0/24"], // 封禁豁免网段:永不计数、永不 429
"confirmPatterns": [ // 可选:高危命令二次确认清单(显式配置即完全覆盖默认清单)
"rm\\s+-[a-zA-Z]*[rf]",
"shred\\s",
"find\\s+/.*-delete"
],
"tokens": [ // 多静态 Token:按 id 区分调用方,支持过期/禁用/服务器白名单
{ "id": "alice", "token": "<64位hex>", "note": "主力客户端" },
{ "id": "old", "token": "<64位hex>", "expiresAt": "2026-10-01T00:00:00Z" },
{ "id": "emp", "token": "<64位hex>", "servers": ["self-limited"] } // 该 Token 只能访问指定服务器
],
"hmac": { // HMAC 派生 Token 自动轮换(scripts/derive-token.js 推导)
"secret": "<master-secret>", // 注意:全局派生 Token 不受 servers 白名单限制,仅发可信调用方
"periodHours": 24, // 时间桶周期,默认 24h
"graceBuckets": 1 // 接受当前桶 ±N,默认 1
},
"hmacUsers": [ // 每人独立 HMAC secret:自动轮换 + 各自服务器白名单(两全)
{ "id": "emp-alice", "secret": "<每人独立随机串>", "servers": ["self-limited"] }
],
"rateLimit": { "windowMs": 60000, "maxFails": 10, "banMs": 600000 } // 防爆破
},
"audit": { "enabled": true, "dir": "/var/log/ssh-mcp" } // 审计日志(默认关闭;JSONL 按天)
}
获取目标机 SSH 指纹(建议从可信网络或服务器控制台核对):
ssh-keyscan -t ed25519 10.0.0.11 2>/dev/null | ssh-keygen -lf - -E sha256
🧪 测试环境说明(Windows + WSL2)
- Hyper-V 防火墙默认拦截 Windows→WSL 的直连。已用管理员 PowerShell 放行:
(改回Set-NetFirewallHyperVVMSetting -Name '{40E0AC32-46A5-438A-A0B2-2B479E8F2E90}' -DefaultInboundAction AllowBlock即可还原) - WSL 空闲一段时间后网络会"漂移"(连接超时),且重启后 IP 可能变化。
重新测试时运行
bash scripts/start-wsl-test.sh,它会重启 WSL、更新 config.json 里的 IP 并连跑三个测试。生产部署在真实 Linux 服务器上不存在这些问题。
📜 更新日志
见 CHANGELOG.md(当前 0.4.1)。