295 lines
14 KiB
Markdown
295 lines
14 KiB
Markdown
# 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).
|