# Артефакт внедрения: п.2 — API без root + root-помощник Дата: 2026-09-30. Узел: 213.226.125.13 (Alpine, OpenRC). Приложение: `/opt/OpenVPN-Monitoring-Simple`. План: `PLAN-validation-and-privilege-drop.md`. Коммит в репозитории приложения на узле: `6f9e800` («Run API services unprivileged; add root helper…»; после `--amend`, ранее упоминался как `10020ca`), дополнение по CRL — `9ffdbfa`, пуш не выполнялся. Полное описание: `DOCS/Changes/2026-09-30_Privilege_Separation.md`. ## 1. Что внедрено | Компонент | Расположение | Суть | |---|---|---| | Пользователь | `ovpmon` (uid 109, nologin) | владелец данных: `/var/lib/ovpmon`, `/var/log/ovpmon`, `easy-rsa`, `client-config`, логи, `__pycache__` | | OpenRC | `/etc/init.d/ovpmon-{api,gatherer,profiler}` | `command_user="ovpmon:ovpmon"`; `/etc/ovpmon/env` остаётся root 600 | | Помощник | `/usr/local/sbin/ovpmon-helper` (root, 755) | `install-config` (allowlist директив, атомарная установка) и `service start\|stop\|restart\|status` | | doas | `/etc/doas.d/ovpmon.conf` | ровно 5 команд помощника, без других аргументов | | Код Profiler | `services/process.py`, `routers/server.py` | без root: staging → помощник; ветки root/контейнера не менялись | ## 2. Порядок внедрения 1. Бэкап: `/root/backup-p2-2026-09-30-1229.tar`, `/root/app-bak/p2/` (init-скрипты, `process.py`, `server.py`). 2. Создание `ovpmon`, установка помощника и doas-правил; проверка помощника от имени `ovpmon` (сервисы ещё root). 3. Патч `process.py` / `server.py` (+ `except HTTPException: raise`, чтобы отказ 400 не превращался в 500). 4. `chown` данных, `command_user` в init-скриптах; перезапуск gatherer → api → profiler с проверкой каждого. 5. Функциональные и негативные тесты; рестарт OpenVPN через API. 6. Документация и коммит (исправлен эпизод с порчей `Deployment_Native.md`: восстановлен из предыдущего коммита и переделан, коммит переписан `--amend`, пуша не было). ## 3. Доказательства | Проверка | Результат | |---|---| | Процессы | gunicorn, uvicorn, gatherer — `ovpmon`; root остались только supervise-daemon | | Помощник принимает рабочий `server.conf` | да, файл в `/etc/openvpn` не изменился (sha256 совпал) | | 17 инъекций директив, отсутствие `user nobody`, cert вне PKI, CR, симлинк staged-файла | все отклонены, боевой конфиг не тронут | | doas: произвольная команда, другие аргументы помощника | `Operation not permitted` | | `ovpmon` не может: читать `/etc/shadow`, `/root`, `/etc/hysteria/*.yaml`, `/etc/ovpmon/env`; писать в `/etc/openvpn`, `/etc/init.d`, `authorized_keys`, код приложения; запускать `iptables` | все отказы | | API: мониторинг, config, process stats, `server/configure` (конфиг идентичен), создание/скачивание/отзыв профиля | всё 200 | | Рестарт OpenVPN через API (helper) | 200; OpenVPN и `tun0` подняты, статус-лог читается gatherer, `ip rule 102` и MASQUERADE на месте, ошибок в логе gatherer нет | Побочный эффект тестов: один рестарт OpenVPN (~секунды разрыва VPN-сессий; клиент переподключается сам). ## 4. Откат ```sh cp /root/app-bak/p2/ovpmon-{api,gatherer,profiler} /etc/init.d/ chown -R root:root /var/lib/ovpmon /var/log/ovpmon /opt/OpenVPN-Monitoring-Simple/APP_PROFILER/{easy-rsa,client-config} for s in ovpmon-gatherer ovpmon-api ovpmon-profiler; do rc-service $s restart; done ``` (помощник и doas-правила можно оставить; код Profiler при root работает по прежней ветке). ## 5. Оговорки - `crl_verify`: ограничение снято (2026-09-30, коммит `9ffdbfa`) — см. раздел 6 ниже. - Проверка путей PKI в помощнике выполняется на момент установки; каталог PKI принадлежит `ovpmon` — держать его только за этим пользователем. - Скрипты подключения по-прежнему требуют `/etc/openvpn/scripts/` (root, 755) — каталога нет по умолчанию. ## 6. Дополнение: публикация CRL для `crl_verify` (2026-09-30) Проблема: OpenVPN после старта работает от `nobody`, а `pki/` создаётся с правами 700 (владелец `ovpmon`), поэтому при включённом `crl_verify` все клиенты получили бы отказ. Решение: новая команда помощника `publish-crl` (6-е правило doas) копирует `pki/crl.pem` в `/etc/openvpn/crl.pem` (`root:root 644`) с проверками (файл внутри PKI, не симлинк, владелец `ovpmon`, ≤ 1 МиБ, PEM-маркеры и разбор `openssl crl`). Вызывается после `gen-crl` (init/revoke), при `server/configure` и при каждом `service start|restart`; ошибка публикации при отзыве возвращается как ошибка API. Генератор без root пишет `crl-verify /etc/openvpn/crl.pem`, помощник разрешает для `crl-verify` только этот путь. Проверка (отдельный временный экземпляр OpenVPN на другом порту и подсети, от `nobody`, тот же PKI и CRL; боевой OpenVPN не перезапускался): | Проверка | Результат | |---|---| | `publish-crl` от `ovpmon`; права копии; чтение `nobody` | ok; `root:root 644`; читается; исходный `pki/crl.pem` для `nobody` по-прежнему недоступен | | мусорный файл / поддельные PEM-маркеры / симлинк вместо CRL | отклонены | | `crl_verify=true` через API + `server/configure` | 200, в конфиге `crl-verify /etc/openvpn/crl.pem`, помощник принял; настройка возвращена, боевой `server.conf` идентичен исходному | | валидный клиент при активной проверке CRL | подключается | | отзыв через API и повторное подключение | `certificate revoked`, клиент отклонён, ошибок чтения CRL нет | Побочное: в списке профилей остались записи со статусом revoked от тестов (`ok-user1`, `p2test-*`, `crl-e2e-*`). Тестовый экземпляр и файлы удалены, боевой OpenVPN (pid 8884) и правила выхода через hel не затронуты. Значение `crl_verify` в настройках оставлено выключенным, как было.