Трендовые github проекты в нашем телеграм канале. Подпишись → Как дать ИИ-агенту разбирать инциденты на сервере без SSH и sudo
Когда ночью заканчивается место на диске или тормозит сайт, ИИ-агент умеет быстро собрать картину: нагрузка, память, диски, процессы, журналы. Дальше ему часто нужно что-то поменять — перезапустить сервис, почистить файл, поправить конфиг. Самый простой способ дать ему такие возможности — SSH-ключ с sudo, и это же самый опасный способ: агент получает всё, что может аккаунт, плюс root.
linux-mcp-daemon (демон mcpd и клиент linuxctl) решает задачу через MCP: агент видит сервер как набор именованных инструментов, root получает только на явно разрешённые операции в заданных путях, а каждый вызов попадает в лог.
Чем плохи существующие MCP-серверы для Linux
Готовые решения делятся на три группы:
- Shell с белым списком (ssh-mcp и его форки). Инструмент «выполнить команду» проверяет обычно только первое слово, а сама команда уходит в
sh -c. Строкаls; rm -rf ~проходит проверку. Даже продвинутые форки с классификацией команд и подтверждением человеком опираются на разбор текста команды. - Только чтение (например, linux-mcp-server от Red Hat). Безопасно для диагностики, но исправить ничего нельзя, а доступ к удалённому хосту всё равно выдаётся SSH-ключом на весь аккаунт.
- Голый SSH. Все права аккаунта, с sudo — root.
Для разбора инцидентов, в которых участвуют несколько машин, нужен сетевой доступ с аутентификацией, разделение пользователей и выдача root по одному инструменту. Этого набора ни у кого из перечисленных нет.
Устройство mcpd
Демон написан на Go и собирается в один статический бинарник. Агент подключается к нему по HTTPS и получает 38 инструментов с именами вида группа/команда: processes/top, disks/usage, logs/journal-control, services/manage, files/read. У каждого есть описание и JSON-схема аргументов. Кроме инструментов, mcpd отдаёт 11 постоянных ресурсов (os://release, network://routes, devices://pci) и 6 шаблонов вроде service://{name}/status и process://{pid}/{target}.
Сетевой транспорт выбран сознательно. При stdio-транспорте клиент сам запускает сервер командой из своего конфига — и выполнит любую команду, если конфиг подменить. Эта проблема есть во всех официальных SDK, а у mcpd её нет: клиент у себя ничего не запускает.
Данные из ядра
Большую часть информации mcpd читает напрямую из /proc, /sys и у systemd по D-Bus, без вызова ps, df или systemctl с последующим парсингом вывода. Внешние программы запускаются там, где собственная реализация вышла бы хуже: smartctl, traceroute, journalctl, dmesg, last, find. Например, журнал systemd хранится в бинарном сжатом формате, а подключение libsystemd потребовало бы cgo и лишило бы проект статической сборки. Все внешние утилиты вызываются без shell, с отдельными аргументами.
Инструмент processes/top выдаёт вывод в формате top, но собирает его из /proc, а %CPU считает по двум замерам с интервалом. Размеры по умолчанию в байтах, чтобы агенту не пришлось разбирать строки вида 1.8G; для людей есть флаг human_readable.
Worker на каждый вызов
Главный процесс только принимает запрос, проверяет токен и права. Для каждого вызова стартует короткоживущий worker от имени Linux-пользователя с тем же именем, что у пользователя mcpd. Пользователь alice в mcpd — это аккаунт alice в системе, и всё, что ядро запрещает этому аккаунту, агент тоже сделать не сможет.
Root по списку
Привилегированные вызовы описываются в mcp-sudo.yaml:
users:
logs:
privileged:
tools:
files/read:
allowed: true
paths: [/var/log] # root только внутри /var/log
logs/journal-control:
allowed: true
network/curl:
network:
deny_private: true # никаких localhost, 10/8, 169.254.169.254
Для инструментов, работающих с путями, paths обязателен: без него конфиг не загрузится. Доступ ко всему диску придётся прописать явно как paths: ["/"], чтобы он бросался в глаза при ревью. Попытка выйти за границы заканчивается понятной ошибкой:
$ linuxctl tool files/read --path /root/.bashrc --privileged true
user mcp is not authorized to run files/read on path /root/.bashrc as root
TLS включён по умолчанию: при первом запуске генерируется самоподписанный сертификат, клиенты проверяют его по отпечатку. Каждый вызов пишется в journald или docker logs с пользователем, инструментом, аргументами (токены вырезаются) и результатом. Изменяющие систему вызовы помечаются audit и логируются на любом уровне логирования.
Установка и подключение
На хосте с systemd:
curl -fsSL https://raw.githubusercontent.com/nucleusv/linux-mcp-daemon/main/scripts/install.sh | sudo bash
Скрипт проверяет sha256 релиза, создаёт первого пользователя mcp и один раз печатает его токен (в конфиге хранится только солёный хеш) и отпечаток сертификата. Есть также пакеты .deb/.rpm и образ ghcr.io/nucleusv/linux-mcp-daemon.
Клиент linuxctl работает в стиле kubectl, есть сборка под macOS:
export MCP_SERVER=https://my-server:9091
export MCP_TLS_FINGERPRINT=sha256:...
export MCP_TOKEN=...
linuxctl get processes --sort_by mem --limit 3 --output table
linuxctl explain disks
Список команд клиент каждый раз запрашивает у демона, поэтому новый инструмент на сервере сразу появляется в linuxctl, а автодополнение в bash и zsh показывает пользователю только разрешённое ему.
Подключение к Claude Code:
export NODE_EXTRA_CA_CERTS=~/.config/mcpd/my-server.crt
claude mcp add --transport sse my-server https://my-server:9091/sse \
--header "Authorization: Bearer <token>"
Проверка на учебном инциденте
На VPS с Ubuntu 24.04 (1 CPU, 2 ГБ) сервис my-service писал отладочный лог без ротации. Агенту завели отдельного пользователя agent с root ровно на одно действие: files/update для /var/log/my-service.log.
За пару минут агент сделал 20 вызовов: свободное место, размеры каталогов, список логов, начало и хвост растущего файла, настройки журнала, процессы, код сервиса. На disks_usage с privileged: true mcpd отказал, и агент досчитал без root. Журнал systemd ему не отдало ядро: agent не состоит в группе systemd-journal. В итоге агент нашёл файл, посчитал скорость роста и предложил очистить лог через усечение. Удаление через rm здесь не помогло бы: сервис держит файл открытым, и место осталось бы занятым.
Узкие на вид права, которые равны root
Эти разрешения легко выдать по ошибке:
- Запись в
/etcчерезfiles/update: агент может добавить файл в/etc/sudoers.d/, задачу в cron или systemd-юнит. services/manageдействует на любой юнит, включаяsshи сам mcpd.kernel/system-controlбезwrite_keys: черезkernel.core_patternможно указать программу, которую ядро запустит от root при падении любого процесса.files/readсpaths: ["/"]открывает/etc/shadowи приватный ключ сертификата mcpd.- Группа
dockerу аккаунта агента даёт root без всяких грантов: достаточно запустить контейнер с примонтированным/. network/curlбез блокаnetworkходит на внутренние адреса и на169.254.169.254, где облако отдаёт временные ключи. Если в логе окажется подброшенная инструкция, агент может сходить туда и пересказать ответ. Закрывается это черезdeny_private: true.
Баги, найденные при разработке
- Пользователь mcpd с именем
rootполучал root на каждый вызов в обходmcp-sudo.yaml. Сейчас такой конфиг не загружается. - Симлинки обходили проверку
paths:/var/www/x → /etc/shadow. При ограниченных путях файлы теперь открываются покомпонентно сO_NOFOLLOW, и демон отвечаетrefusing to follow a symbolic link. - Контейнер без
--pid hostвидел собственный PID 1, и вызов сprivileged: trueмолча выполнялся от root внутри контейнера. Агент считал, что работает с хостом, хотя видел образ. С v0.3.4 такой вызов падает с ошибкойcannot switch to the host's filesystem - is the mcpd container running with --privileged --pid host?.
В планах — read-only инструменты для Docker, сводка состояния системы одним вызовом и аудит безопасности: SUID-файлы, каталоги с открытой записью, настройки sshd. Лицензия Apache 2.0, документация со страницей о правах и рисках лежит на nucleusv.github.io/linux-mcp-daemon.