Initial commit: forge-tools-ssh — MCP-сервер для администрирования по SSH

This commit is contained in:
Maksim Totmin
2026-10-01 10:13:48 +07:00
commit 1821fe7968
30 changed files with 5342 additions and 0 deletions
+294
View File
@@ -0,0 +1,294 @@
# forge-tools-ssh
> MCP-сервер, который даёт AI-ассистенту аккуратный доступ к серверам по SSH.
![Go](https://img.shields.io/badge/Go-1.27-00ADD8?logo=go&logoColor=white)
![MCP](https://img.shields.io/badge/MCP-stdio-6E56CF)
![Tools](https://img.shields.io/badge/tools-45-success)
![License](https://img.shields.io/badge/license-Apache--2.0-blue)
**forge-tools-ssh** — это небольшая отдельная программа, которая превращает
обычный SSH в набор инструментов, понятных AI-ассистенту. Ассистент вызывает
инструмент по имени — `run`, `disk_usage`, `docker_ps`, `journal_read` — и
получает готовый результат, не выдумывая каждый раз синтаксис `ssh`, `grep`,
`docker` и `df`.
Типичные задачи: посмотреть состояние сервера, найти причину падения сервиса,
прочитать лог, проверить занятое место, разобраться с контейнерами или БД, а
при явном разрешении — отредактировать конфиг и перезапустить службу.
> ### Коротко про MCP
> [MCP](https://modelcontextprotocol.io) — стандарт подключения AI-ассистентов
> к инструментам. Сервер — это программа, которая читает запросы ассистента
> через стандартные ввод/вывод и возвращает результат. Один и тот же сервер
> работает с любым MCP-клиентом.
Модуль намеренно **не содержит адресов, логинов и ключей**: всё, к чему можно
подключиться, объявляет оператор. Ассистент лишь выбирает из разрешённого.
---
## Возможности
- **45 инструментов** — от запуска команды до разбора SIP-трафика.
- **Подключение через джамп-хост** — доступ к закрытому контуру через bastion.
- **PAM-шлюзы** — двухстадийная аутентификация через корпоративный шлюз.
- **Проверка синтаксиса** JSON/YAML/TOML/XML/INI/ENV/Dockerfile **до** записи
файла — на самом сервере MCP, без зависимостей на удалённой машине.
- **Политика подключений** (`ssh.json`): именованные профили и allowlist хостов,
которые оператор задаёт декларативно. Ошибка конфига — отказ, а не тихий обход.
- **Live-reload**: правки `ssh.json` подхватываются без перезапуска.
- **Устойчивость**: перезапуск связи при обрыве, таймауты, лимиты на размер и
длину вывода, корректная отмена задач.
---
## Архитектура
```mermaid
flowchart LR
A["AI-ассистент<br/>(MCP-клиент)"] -- "stdio · JSON-RPC" --> B["forge-tools-ssh"]
B -- "SSH / SFTP" --> C["Сервер"]
B -- "SSH → bastion" --> D["Сервер в закрытом контуре"]
B -- "SSH → PAM-шлюз" --> E["Целевой сервер"]
style B fill:#6E56CF,color:#fff
```
Сервер — stdio-процесс: он не открывает портов и не слушает сеть. Общение с
AI-клиентом идёт через потоки ввода/вывода, а наружу он ходит только по SSH.
---
## Быстрый старт
### Сборка
```bash
export GOPRIVATE=git.totmin.ru
go build -trimpath -o forge-tools-ssh .
```
Проверить, что бинарник живой:
```bash
./forge-tools-ssh --health # → ok
```
### Подключение к AI-клиенту
Сервер запускается клиентом как дочерний процесс. Пример конфигурации:
```json
{
"mcp": {
"servers": {
"ssh": {
"command": "/opt/forge-tools/ssh/forge-tools-ssh",
"env": {
"FORGE_TENANT_CONFIG": "/etc/forge/agents/my-agent",
"SSH_MCP_KEY_PATH": "/etc/forge/agents/my-agent/ssh/id_ed25519"
}
}
}
}
}
```
Дальше достаточно попросить ассистента, например: «посмотри, почему упал nginx на
`prod-web`, и покажи последние 100 строк журнала».
---
## Инструменты (45)
### Ядро — 5
| Инструмент | Назначение |
|---|---|
| `connect` | Установить SSH-соединение (по профилю или ad-hoc в рамках allowlist) |
| `disconnect` | Закрыть соединение (или все сразу) |
| `run` | Выполнить команду на удалённом хосте |
| `identity` | Показать публичный ключ сервера для `authorized_keys` |
| `info` | ОС, ядро, hostname, архитектура |
### Файлы — 6
| Инструмент | Назначение |
|---|---|
| `read` | Прочитать файл (лимит 10 МБ) |
| `write` | Записать файл с валидацией синтаксиса перед записью |
| `edit` | Правка файла: replace / regex / insert / append / prepend / delete / replace_line |
| `validate` | Проверить синтаксис файла на стороне MCP-сервера |
| `list_dir` | Список каталога |
| `sync` | Потоковая передача файла между двумя хостами |
### Мониторинг — 7
| Инструмент | Назначение |
|---|---|
| `usage` | Загрузка, память, диски |
| `ps` | Топ процессов по CPU или памяти |
| `logs` | Хвост лог-файла (опционально с фильтром) |
| `journal_read` | `journalctl` / syslog |
| `dmesg_read` | Кольцевой буфер ядра |
| `diagnose_system` | Экспресс-диагностика: load, OOM, диски, упавшие службы |
| `list_services` | Список служб (systemd / OpenRC и др.) |
### Диск — 2
| Инструмент | Назначение |
|---|---|
| `disk_usage` | Место на томе, содержащем указанный путь |
| `disk_usage_all` | Все точки монтирования, разделы ≥ 80% помечены |
### Сеть и поиск — 4
| Инструмент | Назначение |
|---|---|
| `net_stat` | Слушающие порты (`ss` / `netstat`) |
| `search_files` | Поиск файлов (`find`) |
| `search_text` | Поиск текста (`grep`) |
| `package_manage` | Пакеты: apt / apk / dnf / yum |
### Docker — 8
| Инструмент | Назначение |
|---|---|
| `docker_ps` | Список контейнеров |
| `docker_logs` | Логи контейнера |
| `docker_op` | start / stop / restart |
| `docker_ip` | IP-адреса контейнера |
| `docker_find_by_ip` | Найти контейнер по IP |
| `docker_networks` | Сети Docker |
| `docker_cp_from` | Копировать файл из контейнера на хост |
| `docker_cp_to` | Копировать файл с хоста в контейнер |
### Базы данных — 3
| Инструмент | Назначение |
|---|---|
| `db_query` | SQL/CQL/Mongo-запрос внутри контейнера (postgres, mysql, scylladb, cassandra, mongodb) |
| `db_schema` | Схема: таблицы / коллекции |
| `list_db_containers` | Найти контейнеры, похожие на БД |
### VoIP — 10
| Инструмент | Назначение |
|---|---|
| `voip_discover_containers` | Найти VoIP-контейнеры по имени/образу |
| `voip_sip_capture` | Захват SIP-сигналинга в PCAP (`sngrep`) |
| `voip_call_flow` | Разбор SIP call flow из PCAP |
| `voip_registrations` | REGISTER-диалоги и их исход |
| `voip_call_stats` | Агрегированная статистика вызовов |
| `voip_extract_sdp` | Кодеки и RTP-порты из SDP |
| `voip_packet_check` | Быстрая проверка наличия SIP-пакетов |
| `voip_network_capture` | Захват SIP через `tcpdump` |
| `voip_rtp_capture` | Захват RTP для проверки медиа-потока |
| `voip_network_diagnostics` | ping / traceroute / проверка TCP-портов |
---
## Конфигурация
Доступы задаются декларативно в `<FORGE_TENANT_CONFIG>/ssh.json`. Оператор
описывает профили (что и как подключать) и allowlist хостов; ассистент работает
только с этими именами. Файл перечитывается на лету при изменении.
```jsonc
{
// Ключ по умолчанию, если профиль или вызов не задали свой.
"default_key_path": "${SSH_KEY_PATH}",
// Glob-паттерны разрешённых хостов. Наличие списка включает проверку,
// пустой список — ad-hoc без ограничений. Держите список непустым.
"allowed_hosts": ["10.0.*", "*.internal.example.com"],
"profiles": [
{
"alias": "prod-web",
"host": "10.0.1.10",
"username": "deploy",
"port": 22,
"key": "${PROD_SSH_KEY}"
},
{
"alias": "bastion",
"host": "bastion.example.com",
"username": "deploy"
},
{
"alias": "db-via-pam",
"host": "pam-gateway.example.com",
"username": "operator",
"target": "10.0.2.20",
"target_password": "${DB_TARGET_PASSWORD}"
}
]
}
```
Готовый шаблон — [`ssh.json.example`](ssh.json.example).
| Поле профиля | Смысл |
|---|---|
| `alias` | Имя профиля, на которое ссылается ассистент |
| `host` | Хост или IP |
| `username` | Пользователь SSH |
| `port` | Порт (по умолчанию `22`) |
| `key` | Приватный ключ (иначе `default_key_path`, иначе системный ключ) |
| `via` | Алиас jump-хоста, через который подключаться |
| `target` | Хост за PAM-шлюзом (SafeInspect): шлюзу отправляется `username@target` |
| `target_password` | Пароль целевого аккаунта для второй стадии аутентификации PAM |
### Переменные окружения
| Переменная | Назначение |
|---|---|
| `FORGE_TENANT_CONFIG` | Каталог с `ssh.json`. Если не задан — режим без политики. |
| `SSH_MCP_KEY_PATH` | Путь к системному приватному ключу Ed25519. |
Значения в конфиге поддерживают подстановку `${VAR}` из окружения — реальные
адреса и ключи в репозиторий не попадают.
---
## Безопасность
- **Проваливается закрытым (fail-closed).** Если задан `allowed_hosts`, любой
хост вне списка получает отказ. Битый `ssh.json` — тоже отказ.
- **Креды задаёт оператор, а не модель.** Ассистент выбирает профиль по имени и
не может придумать произвольный хост, ключ или адрес.
- **Валидация вместо порчи.** `write` проверяет синтаксис перед записью, `edit`
— после правки, и предупреждает, если файл сломан.
- **Экранирование.** Все пользовательские строки, попадающие в shell-команды,
проходят кавычивание и санитизацию.
- **Лимиты.** Таймауты команд, ограничение размера файла и длины вывода,
отдельный лимит на чтение.
- **Ключи.** Каталог ключей создаётся с правами `0700`, приватный ключ — `0600`.
- **Осторожно:** host key удалённого сервера не проверяется
(`InsecureIgnoreHostKey`). Это осознанный компромисс для работы с динамической
инфраструктурой — за целостность канала отвечает сеть.
Перед публикацией убедитесь, что реальные ключи и конфиги с секретами не попали
в репозиторий (см. [`.gitignore`](.gitignore)).
---
## Разработка
```bash
go build ./... && go vet ./...
go test -race ./...
gofmt -l . # должно быть пусто
```
Требования: Go 1.27+ и доступ к приватному Go-модулю
[`forge-toolkit`](https://git.totmin.ru/en2zmax/forge-toolkit)
(`export GOPRIVATE=git.totmin.ru`).
---
## Лицензия
Apache-2.0 — см. [`LICENSE`](LICENSE).