@perhamm/ssh-mcp-server
v1.13.1
Published
SSH MCP server with guard profiles, sudo from the environment, known_hosts verification and tunnels
Downloads
906
Maintainers
Readme
ssh-mcp-server
MCP-сервер поверх SSH: агент выполняет команды на удалённых машинах, а ключи, пароли и sudo остаются на нашей стороне.
Форк classfang/ssh-mcp-server под ISC.
Русский | English
Что это
ssh-mcp-server это мост между MCP-клиентом (Claude Code, Cursor, Cline) и SSH. Агент вызывает инструменты, сервер подключается к машине и возвращает вывод. Модель не видит ни приватного ключа, ни пароля, ни пароля sudo: всё это читается из локального конфига и переменных окружения процесса.
Один сервер обслуживает любое число хостов. Хост выбирается на лету по алиасу из ~/.ssh/config, поэтому в конфиг MCP-клиента не нужно вписывать каждую машину.
Что добавлено в этом форке
| Возможность | Зачем |
|---|---|
| Хосты из ~/.ssh/config на лету | Один MCP на весь парк. Алиас передаётся в connectionName, соединение поднимается при первом обращении |
| ProxyJump | Хост за бастионом доступен по алиасу, цепочка ProxyJump разбирается из SSH-конфига |
| sudo из переменной окружения | Агент просит sudo: true, пароль подставляет сервер и вырезает его из вывода |
| Запретное ядро | Список операций, которые не выполняются никогда: ни под sudo, ни в любом профиле, ни через SFTP |
| Профили гвардов | Готовый набор запретов safe и белый список readonly, версионированный и обновляемый |
| Туннели | SOCKS5 (аналог ssh -D) и проброс порта (аналог ssh -L) на произвольный локальный порт |
| Проверка хост-ключей | known_hosts сверяется по умолчанию, ключ не из списка означает отказ подключаться |
| Современная криптография | Ed25519 первым в списке, без SHA-1, CBC и DSA |
| Аудит-лог | Каждый вызов пишется в JSONL с ротацией и gzip-архивами |
| Без загрузки файлов | upload по умолчанию не публикуется: файл, который гварды не прочитают, это способ занести код на хост |
Инструменты
| Инструмент | Что делает |
|---|---|
| execute-command | Выполняет команду, умеет sudo и произвольный connectionName |
| download | Забирает файл с сервера |
| list-servers | Показывает настроенные соединения, их статус и активный профиль гвардов |
| list-ssh-hosts | Показывает алиасы из SSH-конфига, доступные как connectionName |
| open-tunnel | Поднимает SOCKS5-прокси или проброс порта через соединение |
| close-tunnel | Закрывает туннель |
| list-tunnels | Показывает открытые туннели и счётчики соединений |
list-ssh-hosts появляется только с флагом --ssh-config-hosts, туннельные инструменты убираются флагом --disable-tunnels. Инструмента upload в списке нет: он публикуется только с флагом --enable-upload.
Факты о хосте без лишних команд
При подключении сервер один раз снимает состояние машины: имя, адреса, ОС, ядро, аптайм, диск, память, число процессов. Все пробы склеены маркерами в одну команду, так что это один заход по ssh, а не шесть.
Сервер держит результат в кеше и отдаёт его в list-servers:
[connected] prod-1 | [email protected]:22 | hostname=prod-1 | os=Linux | updated=2026-08-19T18:14:23Z
Raw JSON:
[{"name":"prod-1","connected":true,"guards":"guards=safe ruleset=2026.08.19 ...",
"status":{"reachable":true,"osVersion":"Ubuntu 24.04.1 LTS","kernelVersion":"6.8.0-51-generic",
"uptime":"12 days","diskSpace":{"free":"9.8G","total":"229.6G"},
"memory":{"free":"5.6G","total":"15.5G"},"processes":{"running":214}}}]То есть uname -a, df -h, free -h и uptime спрашивать не надо, ответы уже пришли. Агент вызывает list-servers один раз и читает статус оттуда.
Пробы проходят через гварды по одной. С белым списком в статусе останутся только разрешённые поля. Неполный статус не значит, что хост недоступен.
Успешная команда без вывода возвращает [exit code] 0, а не пустую строку. Пустой ответ модель читает как непонятный исход и идёт перепроверять через echo $?, а это лишний заход на сервер и лишние токены.
Быстрый старт: один сервер на весь парк
Конфиг MCP-клиента:
{
"mcpServers": {
"ssh": {
"command": "npx",
"args": [
"-y",
"@perhamm/ssh-mcp-server",
"--ssh-config-hosts",
"--guards-profile", "safe"
],
"env": {
"SSH_MCP_SUDO_PASSWORD": "..."
}
}
}
}Дальше агент работает так:
- Вызывает
list-ssh-hostsи находит нужный алиас, напримерprod-master. У больших конфигов список режется, поэтому агент передаётfilter: подстроку или шаблон видаprod-*. - Вызывает
execute-commandсconnectionName: "prod-master". - Сервер читает алиас из
~/.ssh/config, берёт оттудаHostName,User,Port,IdentityFileиProxyJump, поднимает соединение и выполняет команду.
Ключ при этом не покидает машину: сервер читает файл сам, в диалог попадает только путь из SSH-конфига. Если IdentityFile не указан, берётся ssh-agent из SSH_AUTH_SOCK. Алиас без HostName подключается по собственному имени, как это делает ssh.
Достижим только тот алиас, который объявлен в конфиге отдельным блоком Host. Блок Host * даёт умолчания, но не превращает произвольное имя в достижимый хост.
Список алиасов можно сузить:
"args": [
"-y", "@perhamm/ssh-mcp-server",
"--ssh-config-hosts",
"--allowed-hosts", "prod-*,*-stage-*",
"--ssh-config-file", "/home/user/.ssh/config_work"
]Шаблоны поддерживают * и ?. Если алиас не подходит ни под один шаблон, соединение не поднимается, а агент получает ошибку SSH_HOST_NOT_ALLOWED.
Безопасный режим и гварды
Гварды это версионированный набор правил, который проверяет каждую команду до отправки на сервер. Правила лежат в guards/default-guards.json и обновляются вместе с репозиторием.
Профили
| Профиль | Поведение |
|---|---|
| off | Правила профиля выключены, работает только запретное ядро. Значение по умолчанию |
| safe | Плюс запрет разрушительных команд, остальное разрешает |
| readonly | Разрешает только чтение и диагностику, наследует все запреты safe |
"args": ["-y", "@perhamm/ssh-mcp-server", "--ssh-config-hosts", "--guards-profile", "safe"]Что ловит safe сверх ядра: shutdown и reboot, сброс firewall, остановку sshd и kubelet, kubectl delete, helm uninstall, docker system prune, удаление пакетов, DROP DATABASE, curl | sh, git push --force, чистку логов, выгрузку модулей ядра, интерактивные редакторы. Полный список с причинами в JSON.
readonly дополнительно требует, чтобы каждая часть команды попадала в белый список: ls, cat, grep, find, ps, ss, df, journalctl, systemctl status, kubectl get/describe/logs, docker ps/logs и подобное. sudo в этом профиле запрещён целиком, вместе с su, doas и pkexec.
Запретное ядро
Часть операций не выполняется никогда: ни в профиле off, ни под sudo, ни через свой файл гвардов, ни через SFTP в обход команд. Список живёт в блоке forbidden.
| Категория | Что закрыто |
|---|---|
| Учётные записи | useradd, usermod, userdel, groupadd, passwd, chpasswd, chage, vipw и запись в /etc/passwd, /etc/shadow, /etc/group |
| sudo | Запись в /etc/sudoers и /etc/sudoers.d, visudo |
| Расписания | crontab кроме crontab -l, запись в /etc/cron*, /var/spool/cron, /etc/anacrontab, команды at и batch |
| systemd | Запись юнитов и таймеров в /etc/systemd, /lib/systemd, /usr/lib/systemd, systemctl edit, systemd-run |
| SSH | Правка /etc/ssh/*, ~/.ssh/*, authorized_keys, sshd_config, а также ssh-keygen, ssh-copy-id, ssh-add |
| Интерпретаторы | python, perl, ruby, node, php, lua, Rscript и запуск скрипта файлом: bash /tmp/x.sh, sh -s, source. Гварды не читают чужой код, поэтому такой запуск закрыт целиком |
| Массовое удаление | rm -r каталога первого уровня или системного подкаталога, rm -r по маске, find -delete, удаление через xargs rm, флаг --no-preserve-root |
| Диски и секреты | mkfs, wipefs, dd of=/dev/, запись в /dev/sd*, форк-бомба, чтение /etc/shadow и приватных ключей |
Обычная работа при этом остаётся: crontab -l, cat /etc/ssh/sshd_config, systemctl restart nginx, rm -rf /var/lib/myapp/cache/tmp проходят. Разбираемый bash -c "..." тоже работает: его содержимое проверяется теми же правилами.
Ядро закрывает и файловые инструменты. download не заберёт /etc/shadow и содержимое ~/.ssh, и allowedRemotePaths тут ничего не разрешает обратно. Локальная сторона тоже под защитой: download не положит файл в наш собственный ~/.ssh.
Загрузка файлов выключена совсем. Инструмент upload не публикуется, пока не передан --enable-upload, а профиль readonly отклоняет загрузку и с этим флагом.
Если сервер нужен именно для заведения пользователей или правки крона, ядро придётся править в форке осознанно: флага, который его снимает, нет.
Почему это не обходится точкой с запятой
Команда разбирается на части по ;, |, &&, ||, &, переводу строки и подстановкам $(...), и каждая часть проверяется отдельно. Кавычки при разборе учитываются. Так ls; rm -rf / не проходит ни в одном профиле, хотя целиком строка начинается с разрешённого ls.
Обёртки снимаются перед проверкой: sudo, env, timeout 5, nohup и присваивания вида LC_ALL=C не прячут команду от правил. Скрипт внутри bash -c "..." разбирается отдельно и проверяется теми же правилами. Длина команды ограничена 5000 символами.
Гварды закрывают ошибки агента, а не намеренный обход. Интерпретатор с произвольным кодом внутри, вроде python -c, правила не разберут. Там, где обход недопустим, ограничиваем права самого пользователя SSH.
Обновление правил
Три способа держать правила свежими:
- Мержим апстрим в свой форк. Файл правил версионирован полем
version, версия видна вlist-serversи в тексте отказа. - Держим свой файл и указываем его через
--guards-file /etc/ssh-mcp/guards.json. Правила из него добавляются к встроенным, версия становится2026.08.19+local-1. - Обновляем файл по расписанию:
node scripts/update-guards.js https://example.com/guards.json /etc/ssh-mcp/guards.jsonСкрипт проверяет JSON и компилирует каждое регулярное выражение и только потом заменяет файл. Битая загрузка не ломает работающий набор правил.
Формат своего файла:
{
"version": "local-1",
"profiles": {
"safe": {
"deny": [
{ "id": "no-ansible", "pattern": "^ansible-playbook\\b", "reason": "выкат идёт из CI" }
]
}
}
}Поле scope: "command" заставляет правило проверять команду целиком, а не по частям. Так работают правила про curl | sh и SQL. В блок forbidden своего файла можно дописать свои запреты, но встроенные из него не убрать: списки складываются.
Старые --whitelist и --blacklist никуда не делись и проверяются до гвардов.
sudo без пароля в диалоге
Пароль sudo лежит в переменной окружения процесса сервера. Агент передаёт sudo: true, но самого пароля не видит ни в аргументах вызова, ни в выводе.
{
"mcpServers": {
"ssh": {
"command": "npx",
"args": ["-y", "@perhamm/ssh-mcp-server", "--ssh-config-hosts", "--guards-profile", "safe"],
"env": {
"SSH_MCP_SUDO_PASSWORD": "..."
}
}
}
}Вызов инструмента:
{
"tool": "execute-command",
"params": {
"cmdString": "systemctl restart nginx",
"connectionName": "prod-master",
"sudo": true
}
}Команда уходит на сервер как sudo -S -k -p '' -u root -- /bin/sh -c '<команда>', пароль пишется в stdin канала. В командной строке его нет, поэтому он не попадает в ps и в историю. Псевдотерминал для таких команд отключается, иначе tty отразил бы ввод обратно в вывод. На всякий случай пароль вырезается из вывода и из текста ошибок.
Имя переменной и целевого пользователя меняются флагами --sudo-password-env и --sudo-user. Если переменная пустая, вызов падает с SUDO_PASSWORD_MISSING ещё до подключения. Профиль readonly запрещает sudo вообще.
В режиме shell пароль дописывается отдельной строкой сразу за командой, так как sudo читает тот же stdin, что и сам shell. Флаг -k гарантирует, что запрос пароля будет всегда и строка не выполнится как команда. Для sudo лучше режим exec.
Туннели
open-tunnel поднимает локальный слушатель и гонит трафик через SSH-соединение. Полезно, когда до сервисов кластера нет прямого доступа, а до узла есть.
SOCKS5 на порту 8777:
{
"tool": "open-tunnel",
"params": {
"type": "socks5",
"localPort": 8777,
"connectionName": "prod-master"
}
}Дальше любой клиент ходит через прокси, имена резолвятся на удалённой стороне:
curl --socks5-hostname 127.0.0.1:8777 http://prometheus.monitoring.svc:9090/api/v1/query?query=up
kubectl --request-timeout=30s ... # через HTTPS_PROXY=socks5h://127.0.0.1:8777Проброс одного порта, аналог ssh -L:
{
"tool": "open-tunnel",
"params": {
"type": "local",
"localPort": 15432,
"remoteHost": "pg-master.internal",
"remotePort": 5432
}
}Если localPort не указан, порт выбирает система и возвращает его в ответе. Туннели живут до close-tunnel, до обрыва SSH-соединения или до остановки сервера.
Ограничения задаются флагами:
| Флаг | Значение по умолчанию | Что делает |
|---|---|---|
| --tunnel-bind-address | 127.0.0.1 | Адрес, на котором слушают туннели |
| --allowed-tunnel-ports | нет ограничений | Список портов, которые разрешено занимать |
| --max-tunnels | 8 | Сколько туннелей держим одновременно |
| --disable-tunnels | выключено | Убирает туннельные инструменты из списка |
Слушатель по умолчанию поднимается на loopback. Адрес шире loopback открывает прокси в вашу сеть, поэтому меняем его осознанно.
Проверка хост-ключей
Ключ сервера сверяется с known_hosts при каждом подключении, включая промежуточные хосты цепочки ProxyJump. По умолчанию режим strict: хоста нет в known_hosts означает отказ.
| Режим | Поведение |
|---|---|
| strict | По умолчанию. Подключаемся только к хостам из known_hosts |
| accept-new | Незнакомый хост записывается при первом подключении, расхождение ключа по-прежнему отказ |
| off | Проверки нет, поведение апстрима |
Проверяются ~/.ssh/known_hosts, ~/.ssh/known_hosts2 и /etc/ssh/ssh_known_hosts, а для алиаса с UserKnownHostsFile тот файл, который указан в SSH-конфиге. Свой список задаётся флагом --known-hosts-file. Понимаются хешированные записи, шаблоны, форма [host]:port и метка @revoked.
Отказ приходит кодом SSH_HOST_KEY_REJECTED и текстом с отпечатком:
Host key of prod.example.com is not in known_hosts (~/.ssh/known_hosts): ssh-ed25519 SHA256:xxxx.
Verify that fingerprint, add the host to known_hosts, or start the server with --host-key-checking accept-new.Расхождение ключа не принимается никогда и ни в каком режиме: сервер отказывается подключаться и пишет, что хост либо пересоздан, либо кто-то встал посередине.
Для первого знакомства с парком удобно один раз пройти с --host-key-checking accept-new, а дальше вернуть strict.
Аудит-лог
Каждый вызов пишется строкой JSON: команда, соединение, флаг sudo, вердикт гварда, длительность, объём вывода. Содержимое вывода в лог не попадает, пароль sudo вырезается.
{"time":"2026-08-19T08:12:44.101Z","pid":8123,"event":"command","result":"blocked","connection":"prod-master","command":"useradd deploy","sudo":true,"code":"COMMAND_VALIDATION_FAILED","reason":"Blocked by the forbidden core ..."}
{"time":"2026-08-19T08:12:51.880Z","pid":8123,"event":"command","result":"ok","connection":"prod-master","command":"systemctl status nginx","sudo":false,"durationMs":412,"bytes":1840}Пишутся события connect, command, download, upload, tunnel-open, tunnel-close, host-key.
По умолчанию файл лежит в $XDG_STATE_HOME/ssh-mcp-server/audit.jsonl, то есть обычно ~/.local/state/ssh-mcp-server/audit.jsonl, права 0600.
| Флаг | По умолчанию | Что делает |
|---|---|---|
| --audit-log <path> | каталог состояния XDG | Путь к логу, значение off выключает запись |
| --audit-max-size <bytes> | 10485760 | Размер, после которого файл ротируется. 0 отключает встроенную ротацию |
| --audit-keep <count> | 10 | Сколько gzip-архивов держим |
Ротация встроенная: по достижении лимита текущий файл переезжает в audit.jsonl.1.gz, старые архивы сдвигаются, всё за пределами --audit-keep удаляется. Десять архивов по 10 МиБ это порядка сотни мегабайт в несжатом виде и заметно меньше после gzip.
Если логами уже управляет logrotate, ставим --audit-max-size 0 и настраиваем ротацию режимом copytruncate.
Ошибка записи не роняет команду: сервер один раз пишет об этом в stderr и продолжает работать.
Способы подключения
Ниже сценарии от простого к сложному. В args каждый флаг и его значение это два отдельных элемента массива: "--host", "192.168.1.1", а не "--host 192.168.1.1".
Логин и пароль
{
"mcpServers": {
"ssh-mcp-server": {
"command": "npx",
"args": [
"-y",
"@perhamm/ssh-mcp-server",
"--host", "192.168.1.1",
"--port", "22",
"--username", "root",
"--password", "pwd123456"
]
}
}
}Приватный ключ
"args": [
"-y", "@perhamm/ssh-mcp-server",
"--host", "192.168.1.1",
"--username", "root",
"--privateKey", "~/.ssh/id_rsa",
"--passphrase", "pwd123456"
]Пароль от ключа можно не писать в конфиг, а положить в переменную SSH_MCP_PASSPHRASE.
Один алиас из ~/.ssh/config
"args": ["-y", "@perhamm/ssh-mcp-server", "--host", "myserver"]Сервер читает HostName, Port, User, IdentityFile и ProxyJump из блока Host myserver, включая директивы Include и шаблоны. Флаги командной строки приоритетнее: --port 2222 перебивает порт из конфига.
Бастион и ProxyJump
Если у алиаса есть ProxyJump, цепочка поднимается сама:
Host prod-master
HostName 10.20.30.40
User ops
ProxyJump bastion
IdentityFile ~/.ssh/prod_keyКаждый следующий хоп подключается через канал предыдущего, как это делает ssh -J. Цепочку можно задать и вручную: --proxy-jump "bastion,gateway:2222". Глубина цепочки ограничена пятью хопами.
Форма ProxyCommand ssh bastion -W %h:%p понимается как эквивалент ProxyJump bastion: именно в неё ssh разворачивает короткую запись, а в сгенерированных конфигах она встречается чаще. Флаги -l и -p у хопа учитываются, %r подставляется пользователем целевого хоста. Остальные варианты ProxyCommand игнорируются: своего процесса сервер не запускает.
Если хост достижим только через хоп, которого в SSH-конфиге нет, цепочку задаёт сам вызов. Параметр proxyJump есть у execute-command, download и open-tunnel, он перекрывает цепочку из конфига. Это нужно там, где конфиг генерится автоматикой и править его бессмысленно, правку затрёт следующая выкатка.
{
"tool": "execute-command",
"params": {
"cmdString": "hostname",
"connectionName": "legacy-master",
"proxyJump": "prod-master-0"
}
}Такое соединение живёт под собственным именем вида алиас via цепочка, поэтому один алиас, поднятый двумя маршрутами, не схлопывается в одно соединение и виден в list-servers двумя строками. Каждый хоп обязан быть алиасом из конфига, иначе вызывающая сторона могла бы направить сервер на произвольный адрес. На чужой хоп приходит SSH_JUMP_NOT_ALLOWED.
Прокси
"args": [
"-y", "@perhamm/ssh-mcp-server",
"--host", "192.168.1.1",
"--username", "root",
"--password", "pwd123456",
"--proxy", "socks5://user:pwd@proxy-host:1080"
]Поддерживаются socks://, socks5://, http:// и https://. HTTP и HTTPS ходят методом CONNECT с Basic-аутентификацией, порт по умолчанию 80 и 443. Для SOCKS5 порт обязателен. Старый флаг --socksProxy работает, но принимает только SOCKS. Вместе --proxy и --proxy-jump не используются.
Джамп-хост с интерактивным shell
transportMode по умолчанию exec. Переключаемся на shell, если после успешного логина команды не выполняются или железка отдаёт только интерактивную сессию:
"args": [
"-y", "@perhamm/ssh-mcp-server",
"--host", "bastion.example.com",
"--username", "ops",
"--password", "pwd123456",
"--transport-mode", "shell",
"--shell-ready-timeout", "15000"
]В режиме shell команды идут по очереди через одну постоянную сессию, а upload и download не работают: SFTP там отключён.
Двухфакторная аутентификация
Флаг --try-keyboard включает keyboard-interactive. Пароль и ключ подставляются сами, код из второго фактора читается из переменной SSH_MCP_2FA_CODE.
Несколько соединений в одном сервере
Кроме алиасов из SSH-конфига остаётся старый способ: файл с описанием соединений.
[
{
"name": "dev",
"host": "1.2.3.4",
"port": 22,
"username": "alice",
"privateKey": "~/.ssh/dev_key",
"guardProfile": "safe",
"commandTimeoutMs": 120000
},
{
"name": "prod",
"host": "5.6.7.8",
"port": 22,
"username": "bob",
"privateKey": "~/.ssh/prod_key",
"guardProfile": "readonly",
"allowedRemotePaths": ["/var/log", "/tmp"]
}
]"args": ["-y", "@perhamm/ssh-mcp-server", "--config-file", "/abs/path/ssh-config.json"]Формат объекта, где ключ это имя соединения, тоже поддерживается. Соединение выбирается параметром connectionName, без него берётся первое.
Ограничения команд и путей
Белый и чёрный списки
"args": [
"-y", "@perhamm/ssh-mcp-server",
"--host", "192.168.1.1",
"--username", "root",
"--privateKey", "~/.ssh/id_rsa",
"--whitelist", "^ls( .*)?,^cat .*,^df.*",
"--blacklist", "^rm .*,^shutdown.*"
]Шаблоны это регулярные выражения через запятую. Команда сначала проверяется по белому списку, потом по чёрному, потом по профилю гвардов, и должна пройти все три проверки.
Шаблон команды
--command-template оборачивает каждую команду. <quotedCommand> подставляет команду как экранированный аргумент, <command> вставляет её как есть. Шаблон применяется после подстановки рабочего каталога.
su root -c <quotedCommand>
docker exec -i mycontainer sh -c <quotedCommand>Пути для файловых операций
--allowed-local-paths расширяет список локальных каталогов, доступных для upload и download (по умолчанию только текущий каталог). --allowed-remote-paths ограничивает удалённые пути, туда пишем абсолютные POSIX-пути через запятую. Без этого флага SFTP видит всю файловую систему хоста, о чём сервер предупреждает при старте.
Таймауты и лимит вывода
| Параметр | По умолчанию | Что ограничивает |
|---|---|---|
| timeout в вызове инструмента | нет | Одну команду, перебивает настройки соединения |
| commandTimeoutMs | 30000 | Команду в режиме exec |
| shellCommandTimeoutMs | 30000 | Команду в режиме shell |
| connectionTimeoutMs | 30000 | Установку соединения и хендшейк |
| sftpTimeoutMs | 300000 | Операции SFTP |
| maxOutputBytes | 10485760 | Захваченный вывод одной команды |
| keepaliveIntervalMs | 10000 | Интервал keepalive |
При превышении лимита вывода команда обрывается, а инструмент возвращает OUTPUT_LIMIT_EXCEEDED вместе с уже собранным куском. Ошибки приходят структурой из code, message и retriable.
Флаги командной строки
--config-file <path> Файл с описанием соединений
--ssh-config-file <path> Путь к SSH-конфигу (по умолчанию ~/.ssh/config)
--ssh <config> Соединение как JSON или пары key=value
-h, --host <host> Хост или алиас из SSH-конфига
-p, --port <port> Порт
-u, --username <name> Пользователь
-w, --password <password> Пароль
-k, --privateKey <path> Путь к приватному ключу
-P, --passphrase <passphrase> Пароль от ключа
-a, --agent <path> Сокет ssh-agent
-W, --whitelist <patterns> Белый список команд, через запятую
-B, --blacklist <patterns> Чёрный список команд, через запятую
--proxy <url> Прокси SOCKS5, HTTP или HTTPS
-s, --socksProxy <url> Старый флаг только для SOCKS5
--allowed-local-paths <paths> Локальные каталоги для upload и download
--allowed-remote-paths <paths> Удалённые каталоги для SFTP
--transport-mode <mode> exec или shell (по умолчанию exec)
--shell-ready-timeout <ms> Таймаут готовности shell (по умолчанию 10000)
--command-template <template> Шаблон с <command> или <quotedCommand>
--pty Псевдотерминал для exec (по умолчанию включён)
--try-keyboard Keyboard-interactive для 2FA
--pre-connect Подключиться ко всем хостам при старте
--ssh-config-hosts Разрешить хосты из SSH-конфига на лету
--allowed-hosts <patterns> Шаблоны разрешённых алиасов, через запятую
--proxy-jump <chain> Цепочка ProxyJump, через запятую
--guards-profile <name> off, safe или readonly (по умолчанию off)
--guards-file <path> Свой набор правил поверх встроенного
--sudo-password-env <var> Переменная с паролем sudo
--sudo-user <user> Пользователь для sudo (по умолчанию root)
--host-key-checking <mode> strict, accept-new или off (по умолчанию strict)
--known-hosts-file <paths> Свои файлы known_hosts, через запятую
--host-key-algorithms <list> Алгоритмы хост-ключа, через запятую
--enable-upload Опубликовать инструмент upload (по умолчанию выключен)
--audit-log <path|off> Путь к аудит-логу (по умолчанию каталог состояния XDG)
--audit-max-size <bytes> Порог ротации, 0 отключает (по умолчанию 10485760)
--audit-keep <count> Сколько архивов держим (по умолчанию 10)
--disable-tunnels Убрать туннельные инструменты
--tunnel-bind-address <addr> Адрес для туннелей (по умолчанию 127.0.0.1)
--allowed-tunnel-ports <ports> Разрешённые порты туннелей, через запятую
--max-tunnels <count> Лимит одновременных туннелей (по умолчанию 8)
--version, -v Версия пакета
--help СправкаБезопасность
- Для прода включаем
--guards-profile safe, для дежурного разбора инцидентов подходитreadonly. Сoffостаётся только запретное ядро: всё остальное выполнится, о чём сервер и пишет предупреждение в лог. - Ключ, его пароль и пароль sudo читаются из файлов и переменных окружения. В конфиге MCP-клиента храним путь к ключу, а не сам ключ.
- Туннели слушают loopback. У SOCKS5 нет аутентификации, поэтому прокси на
0.0.0.0открывает внутреннюю сеть всем, кто дотянется до порта, и сервер пишет об этом предупреждение при старте. - Без
--allowed-remote-pathsчерез SFTP читается и пишется любой путь на хосте, включая~/.ssh/authorized_keys. - Ключ хоста сверяется с
known_hostsв режимеstrict. Снимать проверку через--host-key-checking offстоит только в лаборатории. - Ограничений частоты вызовов нет.
Разработка
npm install
npm run build
npm testТесты запускаются встроенным раннером Node.js и лежат в test/.
Апстрим и лицензия
Проект вырос из classfang/ssh-mcp-server (автор junki.cn), лицензия ISC. Копирайт апстрима сохранён в LICENSE, там же ссылка на исходный репозиторий.
Набор гвардов частично собран по идеям tufantunc/ssh-mcp (MIT).
Пакет в NPM: @perhamm/ssh-mcp-server.
