Initial commit: forge-tools-ssh — MCP-сервер для администрирования по SSH
This commit is contained in:
@@ -0,0 +1,294 @@
|
||||
# forge-tools-ssh
|
||||
|
||||
> MCP-сервер, который даёт AI-ассистенту аккуратный доступ к серверам по SSH.
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
**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).
|
||||
Reference in New Issue
Block a user