Proxmox ↔ NetBox Drift Detection: systemd, HTTPS API и Zabbix 7.0
Практическая организация ежедневной read-only сверки Proxmox с NetBox 4.7: TLS CA, API-токен без записи, systemd timer, отдельные безопасные метрики и четыре триггера Zabbix.
Проверено на: NetBox 4.7.3 · Zabbix Server 7.0.31 · Zabbix Agent 7.0.22 · Proxmox VE · systemd
От разовой инвентаризации к обнаружению расхождений
После сбора DMI, MAC и сетевой топологии Proxmox данные в NetBox были приведены в соответствие с реальным кластером. Однако разовый импорт не гарантирует актуальность: интерфейс можно переименовать, оборудование заменить, часть узлов перестанет отвечать, а аудит — вообще не запуститься.
В октябре 2026 года внедрён ежедневный read-only Drift Audit. Он сравнивает актуальное состояние девяти гипервизоров с NetBox, сохраняет результат локально и отдаёт Zabbix три целочисленные метрики. Сам NetBox инструментом аудита никогда не изменяется.
Исходники: netbox-inventory-toolkit. Реальные сетевые адреса, домены, hostnames и значения секретов в примерах заменены демонстрационными.
01 / Контур безопасности и архитектура
Proxmox host: pvesh + read-only SSH to cluster peers
│
▼
daily drift_audit.py
│ │
│ └── HTTPS GET → NetBox 4.7 API
▼
private JSON 0600
│
└── systemd ExecStopPost → sanitized metrics.json
(root:zabbix, 0640)
│
Zabbix Agent
│ TLS PSK, passive checks
▼
Zabbix Server
4 alert triggers
Права разделены: root получает инвентаризацию, а пользователь zabbix видит только числа. Приватный JSON и файл с NetBox API-токеном недоступны агенту. У Zabbix нет доступа к Django ORM или возможности менять NetBox.
Сервер NetBox использует доверенный корпоративный CA; клиент проверяет сертификат, без verify=False или curl -k.
02 / Проверка API без права записи
Для NetBox 4.7 создаём отдельный API v2 token с разрешениями просмотра Device и Interface, отключённым флажком Write enabled и, по возможности, ограничением по исходному IP. На гипервизоре конфигурация хранится вне репозитория:
# /etc/netbox-inventory/drift.env — mode 0600, root:root
NETBOX_URL=https://netbox.example.net
NETBOX_TOKEN=nbt_EXAMPLE_ID.EXAMPLE_SECRET
NETBOX_CA_FILE=/etc/netbox-inventory/ca.pem
Пример содержит заглушку, не рабочий токен. Публичный CA-файл переносится по доверенному административному каналу; приватный ключ CA передавать нельзя.
curl --cacert /etc/netbox-inventory/ca.pem \
-I https://netbox.example.net/
Возврат 302 на страницу логина после успешной TLS-проверки допустим: веб-страница без cookie переадресует пользователя. Это не заменяет проверку прав API-токена при первом аудите.
03 / Dry-run, затем свежий сбор
Первый запуск проверяли на уже снятом и сверенном снимке, чтобы отдельно протестировать NetBox API:
cd /root/netbox-inventory-toolkit
set -a
. /etc/netbox-inventory/drift.env
set +a
python3 netbox/drift_audit.py \
--inventory output/proxmox-inventory-v03.json \
--output-dir /var/lib/netbox-inventory \
--site-slug example-office \
--role-slug hypervisor \
--expected-nodes 9
unset NETBOX_TOKEN
Затем выполнили живую проверку, уже без параметра --inventory: скрипт сам обращается к pvesh и доверенному SSH на узлах.
python3 netbox/drift_audit.py \
--output-dir /var/lib/netbox-inventory \
--cluster-label example-pve \
--site-slug example-office \
--role-slug hypervisor \
--expected-nodes 9
Оба варианта вернули:
STATUS: CLEAN
devices_checked: 9
interfaces_source: 362
interfaces_netbox: 362
physical_macs_checked: 20
serials_checked: 7
differences: 0
Коды выхода: 0 — совпадение; 1 — найдены расхождения; 2 — ошибка сбора, API или формата данных. Код 2 нельзя интерпретировать как «расхождений нет».
Проверяются устройства, имена интерфейсов, доступные DMI-серийники и текущие MAC физических NIC. Не следует приписывать этому аудиту проверку конфигураций всех VM, физических кабелей или связности VLAN.
04 / Ежедневный systemd timer
В репозитории есть образцы deploy/systemd/netbox-inventory-drift.service и .timer. Пути к файлам, site slug и имя роли адаптируйте к своему окружению. Служба запускается однократно и читает закрытый EnvironmentFile.
install -m 0644 deploy/systemd/netbox-inventory-drift.service \
/etc/systemd/system/
install -m 0644 deploy/systemd/netbox-inventory-drift.timer \
/etc/systemd/system/
systemctl daemon-reload
systemd-analyze verify /etc/systemd/system/netbox-inventory-drift.service
systemctl start netbox-inventory-drift.service
journalctl -u netbox-inventory-drift.service -n 30 --no-pager
systemctl enable --now netbox-inventory-drift.timer
systemctl list-timers --all netbox-inventory-drift.timer
Проверенное расписание: ежедневно около 05:10, рандомизированная задержка до 20 минут, Persistent=true для пропущенного запуска. В работе наблюдали успешный запуск службы и планирование следующего запуска на утро следующего дня.
Обратите внимание на SuccessExitStatus=1: systemd воспринимает завершённый аудит с обнаруженными изменениями как выполненную проверку, а не ошибку запуска. Поэтому сам статус службы не подходит для мониторинга расхождений.
05 / Как безопасно отдать результат Zabbix
Файлы source-latest.json и drift-latest.json остаются 0600 root:root. Не нужно делать их доступными пользователю zabbix.
netbox/zabbix_bridge.py вызывается через ExecStopPost и атомарно пишет только три показателя в отдельный файл root:zabbix 0640:
| Key | Смысл |
|---|---|
netbox.audit.status |
0 CLEAN, 1 DRIFT, 2 ERROR |
netbox.audit.differences |
Число расхождений; при ERROR не показатель отсутствия drift |
netbox.audit.timestamp |
Unix timestamp последней попытки аудита |
Шаблон deploy/zabbix/netbox-inventory-drift.conf задаёт UserParameter для обычного пассивного Zabbix Agent. При отказе самого аудита мост экспортирует status=2, а не старое успешное значение.
zabbix_agentd -t netbox.audit.status
zabbix_agentd -t netbox.audit.differences
zabbix_agentd -t netbox.audit.timestamp
В реальной проверке получены 0, 0, корректный Unix timestamp; файл метрик имел права 640 root:zabbix.
06 / TLS PSK и четыре триггера
Для пассивного опроса сервер должен обращаться к zabbix-agent на порту 10050. Когда в агенте стоит TLSAccept=psk, обычный zabbix_get без PSK получает отказ unencrypted connections are not allowed. Это не неисправность UserParameter. В Zabbix для хоста должны совпадать PSK Identity и PSK, а тип трёх items должен быть именно Zabbix agent, не Zabbix agent (active).
Созданы четыре триггера в шаблоне (техническое имя шаблона в примере — Template NetBox Inventory Drift):
last(/Template NetBox Inventory Drift/netbox.audit.status)=1
last(/Template NetBox Inventory Drift/netbox.audit.status)=2
now()-last(/Template NetBox Inventory Drift/netbox.audit.timestamp)>93600
nodata(/Template NetBox Inventory Drift/netbox.audit.status,15m)=1
Уровни: Warning / High / Average / Average, соответственно. Тег всех событий: component=netbox-drift. После внедрения Zabbix показывал 4/4 включённых триггера, все в OK, а три метрики поступали каждые пять минут.
Чтобы создать триггеры через API, предусмотрен zabbix/configure_template_api.py с dry-run по умолчанию и явным --apply. Важная особенность Zabbix API: trigger.get без expandExpression=true может вернуть {functionid} вместо исходного выражения. Посимвольное сравнение таких строк породило ложные конфликты после успешного создания; исправленный скрипт использует expandExpression и повторно обнаруживает существующие четыре триггера без дубликатов.
07 / Где заканчивается текущая реализация
Zabbix здесь контролирует состояние, но отдельные Trigger actions для этого кейса намеренно не включали. В инфраструктуре уже используется Grafana/Prometheus/Loki для централизованных оповещений; дублировать их в Zabbix решили не делать. Экспорт трёх показателей в Prometheus и правило Grafana Alerting — возможный следующий этап, пока не заявленный как внедрённый.
Проверенный результат — read-only ежедневный аудит, защищённый экспорт трёх метрик, четыре корректно работающих триггера и отсутствие расхождений на момент теста. Это не означает, что доставка уведомлений Zabbix уже настроена.