0x00. Інтро

До ери ШІ я керував Proxmox VE вручну, через веб інтерфейс. Так, є варіант керувати через ssh скрипти, але я вирішив спробувати інакше — дозволив AI-агенту керувати Proxmox напряму під моїм наглядом.

Клеєм тут виступає MCPModel Context Protocol. Простими словами, MCP — це стандартний спосіб дати LLM «меню» інструментів (функцій, які вона може викликати) плюс транспорт для їх виклику; модель обирає інструмент, заповнює аргументи й отримує результат. Існує community-сервер ProxmoxMCP-Plus, який загортає API Proxmox саме в такі інструменти (get_vms, create_container, clone_vm, execute_container_command тощо). Під’єднуєш до нього клієнт на кшталт Claude Code чи OpenCode — і агент може переглядати ноди, створювати LXC-контейнери, виконувати команди всередині них — увесь життєвий цикл.

Трохи термінів:

  • LXC / CT — Linux-контейнер у Proxmox: легкий, ізольований Linux-простір користувача, що ділить ядро з хостом (дешевший за повноцінну VM). Непривілейований CT відображає свій root на не-root користувача хоста — заради безпеки.
  • Bearer-токен — довгий рандомний секрет, який передається в HTTP-заголовку Authorization; хто його має — той «авторизований». Вважайте це паролем до API.

Цей допис — це як розгорнути й захистити MCP-сервер, як під’єднати до нього клієнт і невелике демо керування життєвим циклом контейнера з промпта. Усе нижче перевірено на хості з Debian 13 та ProxmoxMCP-Plus v0.5.8. Усі IP-адреси — приклади у приватній мережі (192.168.x.x); підставте свої.

Зауваження щодо безпеки: віддавати LLM ключі від вашого гіпервізора рівно настільки небезпечно, як це й звучить. Уся конструкція нижче — про те, щоб тримати це під контролем: API-токен з мінімальними правами, сервер лише на localhost, проксі з перевіркою bearer-токена попереду й політика команд у режимі лише-аудиту. HTTP тільки в межах хосту, під’єднання ззовні тільки через HTTPS із авторизацією по токену. Але якщо плануєте розгортати десь на підприємстві/офісі - маєте додати ще механізмів захисту - обмеження по ip адресам, використання certificates pinning тощо. Але це виходить за межі цієї статті.

0x01. Архітектура

Сервер недосяжний з мережі напряму. Є три шари, кожен зі своєю автентифікацією:

┌─────────────────────┐     HTTP + Bearer Token      ┌───────────────────────┐
│  AI Agent / Client  │ ──────────────────────────── │  Caddy (:8443)        │
│  (Claude / OpenCode)│                              │  - Token validation   │
└─────────────────────┘                              │  - reverse proxy      │
                                                     └──────────┬────────────┘
                                                                │ http://127.0.0.1:8000
                                                     ┌──────────▼────────────┐
                                                     │  ProxmoxMCP-Plus      │
                                                     │  - Streamable HTTP    │
                                                     │  - localhost only     │
                                                     └──────────┬────────────┘
                                                                │ HTTPS (verify_ssl=false)
                                                     ┌──────────▼────────────┐
                                                     │  Proxmox VE API       │
                                                     │  (self-signed cert)   │
                                                     └───────────────────────┘

Три шари безпеки:

  1. Caddy перевіряє Bearer-токен і відхиляє все неавтентифіковане.
  2. ProxmoxMCP-Plus слухає лише на 127.0.0.1:8000 — він ніколи не досяжний з мережі напряму.
  3. Proxmox автентифікує сервер власним API-токеном, повністю окремим від MCP bearer-токена.

Сам MCP-сервер працює як невеликий LXC-контейнер або VM (Debian 13, 1 vCPU / 1 ГБ цілком достатньо). У прикладах нижче він живе за адресою 192.168.1.10 і спілкується з API Proxmox за адресою 192.168.1.2:8006.

Крок 1 — встановлення ProxmoxMCP-Plus

На MCP-хості (звичайний Debian 13) встановлюємо Python і клонуємо застосунок у /opt/ProxmoxMCP-Plus. Зверніть увагу на curl та git — їх немає в мінімальному образі Debian за замовчуванням:

apt update
apt install -y python3 python3-venv python3-pip git curl

# uv — швидкий менеджер пакетів Python; встановлюємо й підхоплюємо в PATH
curl -LsSf https://astral.sh/uv/install.sh | sh
source ~/.bashrc

# Отримуємо ProxmoxMCP-Plus
cd /opt
git clone https://github.com/RekklesNA/ProxmoxMCP-Plus.git
cd ProxmoxMCP-Plus
uv venv
uv pip install -e ".[dev]"

Далі — systemd-юніт у /etc/systemd/system/proxmox-mcp.service, щоб сервіс стартував під час завантаження й перезапускався при збої:

# /etc/systemd/system/proxmox-mcp.service
cat > /etc/systemd/system/proxmox-mcp.service << 'EOF'
[Unit]
Description=Proxmox MCP Server (ProxmoxMCP-Plus)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=root
WorkingDirectory=/opt/ProxmoxMCP-Plus
Environment="PROXMOX_MCP_CONFIG=/opt/ProxmoxMCP-Plus/proxmox-config/config.json"
Environment="PYTHONPATH=/opt/ProxmoxMCP-Plus/src"
ExecStart=/opt/ProxmoxMCP-Plus/.venv/bin/python -m proxmox_mcp.server
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable proxmox-mcp.service

Крок 2 — конфіг MCP-сервера

ProxmoxMCP-Plus керується єдиним конфіг-файлом — /opt/ProxmoxMCP-Plus/proxmox-config/config.json (саме на цей шлях systemd-юніт вище вказує через PROXMOX_MCP_CONFIG). Найважливіші частини:

{
    "proxmox": {
        "host": "192.168.1.2",
        "port": 8006,
        "verify_ssl": false,
        "service": "PVE"
    },
    "auth": {
        "user": "mcp@pve",
        "token_name": "claude",
        "token_value": "<PROXMOX_API_TOKEN>"
    },
    "mcp": {
        "host": "127.0.0.1",
        "port": 8000,
        "transport": "STREAMABLE"
    },
    "security": {
        "dev_mode": true
    },
    "command_policy": {
        "mode": "audit_only",
        "deny_patterns": [
            "(^|\\s)rm\\s+-rf(\\s|$)",
            ":\\(\\)\\{:\\|:\\&\\};:"
        ]
    }
}

Кілька неочевидних рішень:

ПараметрЗначенняЧому
mcp.host127.0.0.1Досяжний лише через проксі Caddy, не з мережі
mcp.transportSTREAMABLEСучасний транспорт MCP (замінює застарілий SSE)
proxmox.verify_sslfalseProxmox за замовчуванням має самопідписаний сертифікат
security.dev_modetrueПотрібен, щоб застосунок прийняв verify_ssl: false
command_policy.modeaudit_onlyЛогує команди, але не блокує; deny-список усе одно вбиває rm -rf / fork-бомби

Крок 3 — API-токен Proxmox

Блок auth вище посилається на API-токен Proxmox. ProxmoxMCP-Plus потребує ролі Administrator з вимкненим розділенням привілеїв — ролі PVEAdmin не достатньо, бо їй бракує Sys.Modify, потрібного для створення контейнерів/VM:

# На хості Proxmox:
pveum user add mcp@pve
pveum acl modify / --users mcp@pve --roles Administrator
pveum user token add mcp@pve claude -privsep 0
# privsep=0 → токен успадковує права користувача

Proxmox показує секрет токена рівно один раз. Підставте це значення в auth.token_value у config.json (використайте свій справжній токен там, де у прикладі стоїть <PROXMOX_API_TOKEN>), потім запустіть сервіс:

systemctl start proxmox-mcp.service
ss -tlnp | grep 8000        # очікуємо: LISTEN 127.0.0.1:8000  (лише localhost)

Крок 4 — Caddy: гейт з bearer-токеном

Caddy (невеликий сучасний веб-сервер / реверс-проксі) стоїть попереду й робить рівно дві речі — термінує TLS (тобто робить https->http “перетворення”) і додає автентифікацію по токену, щоб виключити неавторизване використання MCP. Встановлюємо його з офіційного репозиторію:

apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
  | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
  | tee /etc/apt/sources.list.d/caddy-stable.list
apt update
apt install -y caddy

Для внутрішньої мережі самопідписаного сертифіката цілком достатньо — генеруємо сам сертифікат у /etc/caddy/mcp.crt, а приватний ключ — у /etc/caddy/mcp.key, для IP сервера (використайте свою справжню адресу замість 192.168.1.10):

# записує /etc/caddy/mcp.crt (сертифікат) і /etc/caddy/mcp.key (приватний ключ)
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
  -days 3650 -nodes \
  -keyout /etc/caddy/mcp.key -out /etc/caddy/mcp.crt \
  -subj '/CN=mcp-server' \
  -addext 'subjectAltName=IP:192.168.1.10,IP:127.0.0.1,DNS:localhost'
chown caddy:caddy /etc/caddy/mcp.key /etc/caddy/mcp.crt
chmod 640 /etc/caddy/mcp.key

Далі — сам Caddyfile, у /etc/caddy/Caddyfile:

# /etc/caddy/Caddyfile
:8443 {
	tls /etc/caddy/mcp.crt /etc/caddy/mcp.key

	@unauthorized {
		not header Authorization "Bearer {env.MCP_TOKEN}"
	}
	respond @unauthorized 401 {
		body "Unauthorized"
		close
	}

	reverse_proxy 127.0.0.1:8000 {
		header_up Host 127.0.0.1:8000
	}
}

Токен — це просто 32 випадкові байти, збережені у /etc/caddy/mcp_token, а потім передані в Caddy через systemd environment-файл /etc/caddy/mcp_env:

# токен → /etc/caddy/mcp_token ; env-файл → /etc/caddy/mcp_env
openssl rand -hex 32 > /etc/caddy/mcp_token
chmod 600 /etc/caddy/mcp_token
echo "MCP_TOKEN=$(cat /etc/caddy/mcp_token)" > /etc/caddy/mcp_env
chmod 600 /etc/caddy/mcp_env

Кажемо systemd підвантажити цей env-файл через drop-in override у /etc/systemd/system/caddy.service.d/override.conf і запускаємо Caddy:

# /etc/systemd/system/caddy.service.d/override.conf
mkdir -p /etc/systemd/system/caddy.service.d
cat > /etc/systemd/system/caddy.service.d/override.conf << 'EOF'
[Service]
EnvironmentFile=/etc/caddy/mcp_env
EOF
systemctl daemon-reload
systemctl enable --now caddy

Рядок header_up Host 127.0.0.1:8000 — не косметика. MCP Python SDK (v1.24+) має захист від DNS-rebinding: прив’язаний до 127.0.0.1, він відхиляє будь-який запит, у якого заголовок Host не localhost/127.0.0.1. Без переписування Caddy передав би оригінальний Host клієнта (напр. 192.168.1.10:8443), і сервер відповів би 421 Misdirected Request. Переписування заголовка на те, що очікує сервер, це виправляє.

Швидка перевірка з самого сервера:

# Без токена — має бути відхилено:
curl -sk https://127.0.0.1:8443/mcp
# Unauthorized

# З токеном — 406 означає «автентифікація пройшла, тепер надішли правильні MCP-заголовки»:
curl -sk -H "Authorization: Bearer $(cat /etc/caddy/mcp_token)" https://127.0.0.1:8443/mcp

0x02. Під’єднання клієнта

Наш MCP-сервер комунікує по Streamable HTTP за bearer-токеном, тож будь-який MCP-клієнт, що підтримує віддалені HTTP-сервери, може ним користуватися. Нижче — два, якими я користуюся: Claude Code та OpenCode. В обох URL вказує на порт Caddy (8443), а токен іде в заголовку Authorization: Bearer.

Claude Code

Claude Code читає MCP-сервери з файлу .mcp.json у корені проєкту (для сервера на всю машину використайте глобальний ~/.claude.json). В моєму варіанті токен береться зі змінної середовища, тож ніколи не потрапляє у файл:

{
  "mcpServers": {
    "proxmox": {
      "type": "http",
      "url": "https://192.168.1.10:8443/mcp",
      "headers": {
        "Authorization": "Bearer ${PROXMOX_MCP_TOKEN}"
      }
    }
  }
}

OpenCode

OpenCode читає свій конфіг з opencode.jsonc (Linux/macOS: ~/.config/opencode/opencode.jsonc; Windows: C:\Users\<USERNAME>\.config\opencode\opencode.jsonc). Додайте блок mcp типу remote (тут <MCP_TOKEN> вже явно вказаний як є - підставте ваш):

// ~/.config/opencode/opencode.jsonc
// (Windows: C:\Users\<USERNAME>\.config\opencode\opencode.jsonc)
{
  "mcp": {
    "proxmox": {
      "type": "remote",
      "url": "https://192.168.1.10:8443/mcp",
      "enabled": true,
      "headers": {
        "Authorization": "Bearer <MCP_TOKEN>"
      }
    }
  }
}

Порада: замість вставляння токена у файл OpenCode може прочитати його зі змінної середовища — задайте заголовок "Bearer {env:MCP_TOKEN}" і експортуйте MCP_TOKEN у своїй оболонці.

Перезапустіть OpenCode (він кешує MCP-з’єднання під час старту) і перевірте з CLI:

opencode mcp list
# Має показати: proxmox connected

Тонкість із самопідписаним сертифікатом (обидва клієнти)

Claude Code і OpenCode обидва збудовані на Node.js, а Node за замовчуванням суворо перевіряє TLS — окремого прапорця insecure на сервер немає. Із самопідписаним сертифікатом обхід — змінна середовища Node, що вимикає перевірку сертифіката для цього процесу:

# Windows (постійно, у області User)
[Environment]::SetEnvironmentVariable("NODE_TLS_REJECT_UNAUTHORIZED", "0", "User")
# Linux/macOS — додайте до ~/.bashrc / ~/.zshrc
export NODE_TLS_REJECT_UNAUTHORIZED=0

Це вимикає перевірку TLS у Node глобально для цього користувача, тож на машині, доступній з інтернету, це було б поганою ідеєю. У внутрішній / довіреній мережі це прийнятний компроміс. (Якщо взагалі не хочете чіпати Node, Caddy може працювати й по звичайному HTTP на :8443; bearer-токен усе одно контролює доступ, просто трафік буде нешифрований. Або, маючи справжній домен, перейдіть на сертифікат від CA й приберіть цю змінну зовсім.)

Після цього перезапустіть клієнт — і сервер з’явиться як під’єднаний.

MCP-сервер під’єднаний у клієнті, з доступними інструментами Proxmox

0x03. Які інструменти отримує агент

Після під’єднання агент бачить API Proxmox як каталог типізованих інструментів. Ті, на які я найбільше спирався:

ІнструментЩо робить
get_nodes, get_cluster_status, get_node_statusСтан кластера / нод
get_storageСписок пулів сховища та їхні типи
get_vms, get_containersІнвентар VM та CT
create_container, create_vmСтворення нового CT / VM
clone_vmКлонування шаблону в нову VM
start_container / stop_container, start_vm / stop_vmКерування живленням
delete_container, delete_vmЗнесення CT / VM
execute_container_commandВиконання команди всередині CT (через pct exec по SSH до хоста)
update_container_resourcesЗміна CPU / RAM / диска
create_snapshot, rollback_snapshot, create_backupЗнімки та резервні копії
download_iso, list_templatesКерування ISO / шаблонами

execute_container_command — саме той інструмент, що перетворює це з «інвентарного API» на «агента, який реально щось будує». Він не йде через API Proxmox — MCP-сервер заходить по SSH на хост Proxmox і виконує pct exec усередині цільового контейнера. Тож MCP-хост тримає SSH-ключ, чия публічна частина лежить в authorized_keys хоста Proxmox:

# На MCP-хості
ssh-keygen -t ed25519 -f /root/.ssh/id_ed25519 -N ''
cat /root/.ssh/id_ed25519.pub   # → додати до authorized_keys хоста Proxmox

Саме цей шлях через SSH до хоста дозволяє агенту не просто створити контейнер, а зайти в нього й налаштувати від початку до кінця.

0x04. Керування Proxmox через MCP — швидке демо

Коли сервер під’єднано, ви керуєте всім з промпта: «створи невеликий Debian-контейнер, запусти його, потім знеси». Агент перекладає це на виклики MCP-інструментів. Ось нейтральний, відтворюваний прохід на одноразових значеннях — VMID 9000, ім’я хоста demo-ct. (Запис викликів нижче ілюструє аргументи, які приймає кожен інструмент; агент заповнює їх за вас.)

Створюємо зразковий контейнер:

create_container(
  node='<PROXMOX_NODE>',
  vmid='9000',
  ostemplate='local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst',
  hostname='demo-ct',
  cores=1,
  memory=512,
  disk_size=8,
  storage='local-lvm',
  network_bridge='vmbr0',
  start_after_create=false,
  onboot=false
)

Запускаємо, потім зупиняємо його:

start_container(node='<PROXMOX_NODE>', vmid='9000')
stop_container(node='<PROXMOX_NODE>', vmid='9000')

Кожен виклик повертає невеликий об’єкт статусу — приблизно VMID і його новий стан роботи — тож агент може підтвердити крок, перш ніж рухатися далі. Виклик get_containers після цього показує demo-ct в інвентарі з його поточним статусом.

Видаляємо його, коли закінчили — жодних залишків:

delete_container(node='<PROXMOX_NODE>', vmid='9000')

Життєвий цикл VM дзеркалить це точно так само з VM-версіями інструментів: create_vm, start_vm, stop_vm, delete_vm (плюс clone_vm, щоб «штампувати» нову VM із шаблону). Той самий патерн — створити, увімкнути, вимкнути, знищити — повністю через захищений MCP-ендпоінт.

Створення, запуск, зупинка та видалення зразкового контейнера з AI-клієнта через MCP

Ось і весь цикл: промпт у клієнті, типізовані виклики інструментів до Proxmox, зразковий контейнер створено й прибрано — без жодного дотику до веб-інтерфейсу й без власного SSH на хост.

0x05. Підсумок

Поставивши AI-агента перед Proxmox через MCP, ви перетворюєте купу ручних кліків в UI на розмову: «створи Debian CT, запусти його, знеси». Агент робить рутину; за вами лишаються архітектурні рішення й межі безпеки.

Здається - “і все? Що тут такого” - але ні, тепер ви можете задіяти усю міць ШІ для розгортання серверів від нуля. Тут головне правильно пояснити ШІ, що треба, і обов’язково мати способи автоматичного контролю того, що має бути на виході. По аналогії із програмуванням - не просто кажете “напишити програму сортування”, але також - напиши тесткейси для перевірки, із усіма можливими edge cases… (і далі вмикаєте фантазію, але то інша тема)

Безпека: межі — це і є вся суть. MCP-сервер лише на localhost, за проксі з bearer-токеном, з виділеним Proxmox-токеном з мінімальними правами, з політикою команд у режимі лише-аудиту, що логує все. Це мінімум, який я хотів би мати, перш ніж пускати будь-що автоматизоване до гіпервізора.

Дякую, що дочитали аж сюди. Якщо збираєтесь спробувати те саме: токен з мінімальними правами, прив’язка до localhost, токен попереду. Саме в такому порядку.

Refs