Запускайте действие после подключения VPN от имени пользователя, а не демона
Переменная окружения с USER=alice не меняет, под какой учётной записью работает hook. Разделите событие, триггер и действие, а затем докажите UID.
Хуки подключения у демона VPN выполняются от имени демона, а обычно это значит, что они выполняются от имени root. В тот момент, когда один из таких хуков пишет в домашний каталог кого-то или перезапускает помощник рабочего стола, вы дали сетевому событию возможность действовать от имени любого локального пользователя. Обычный короткий путь, прочитать имя пользователя из переменной окружения и передать его дальше, этого не исправляет. Он лишь переименовывает действие, не меняя учётную запись, которая его выполняет. Работает трёхчастная передача, в которой привилегированный шаг сообщает, что произошло, а отдельный юнит выполняет действие под правильной идентичностью.
Имя пользователя в окружении — не переключатель идентичности
Unix решает, к чему может прикасаться процесс, по его действующему идентификатору пользователя, а не по строкам в его окружении. Установка USER=alice или экспорт LOGNAME меняет то, что неаккуратное программное обеспечение печатает в своём выводе; на проверки владения файлами это не влияет никак. Хук, работающий от root с этой переменной, всё равно открывает файлы Алисы от имени root.
И имена — тоже не одно и то же имя. Логин удалённого доступа, локальная учётная запись Unix и общее имя сертификата — три идентификатора, которые часто совпадают, а иногда нет. Пользователь, проходящий аутентификацию как a.smith, может вообще не иметь локальной учётной записи или отображаться на asmith через службу каталогов. Любой проект, предполагающий, что одна строка обслуживает все три роли, сломается на первом же подрядчике.
Значит, первый вопрос не «какой пользователь подключён», а «какой локальной учётной записи разрешено выполнять это действие». Отвечайте на него из собственной базы учётных записей, а не из того, что прислал клиент:
id -u alice
getent passwd alice | cut -d: -f1,3,6
stat -c '%U:%G %a %n' /home/alice/.local/state/vpn-status.json
Третья команда — та, которую люди пропускают. Если целевой файл принадлежит root, потому что его создал предыдущий хук, корректно настроенное действие пользователя завершится ошибкой доступа, которая выглядит как проблема systemd и ею не является.
Демон остаётся там, где он есть
Менять собственный User= службы VPN на Алису — неправильный рычаг. Это меняет то, как демон привязывает интерфейсы, читает ключи, пишет файл состояния и управляет маршрутами, и переносит весь этот риск в юнит, который работал. Демону нужны его привилегии; последующему действию — нет.
Посмотрите, что юнит на самом деле делает, прежде чем его трогать. Две команды показывают идентичность выполнения и точные действующие командные строки, включая всё, что добавило переопределение:
systemctl show -p User -p Group -p ExecStart openvpn-server@server
systemctl cat openvpn-server@server
При установке из пакета по умолчанию демон — root, и скрипт --up наследует это. Подтвердите это, а не предполагайте, потому что усиленный дистрибутив мог перенести его на отдельную учётную запись, а написанный вручную drop-in может запускать хук через обёртку с другими правилами.
Три именованные части, один узкий триггер
Схема, переживающая обслуживание, состоит из события, триггера и действия. Демон сообщает, что именованная сессия поднялась. Триггер принимает этот маленький факт и запрашивает одно фиксированное действие. Действие работает под локальной учётной записью, которой принадлежат затрагиваемые файлы. Ничто в цепочке не принимает произвольный путь команды.
Само действие — обычный сервисный юнит, а идентичность — одна директива внутри него:
[Service]
Type=oneshot
User=alice
Group=alice
ExecStart=/usr/local/bin/refresh-vpn-status --slot %i
Обратите внимание, что User=alice действует на весь юнит, и systemd документирует одно исключение, которое стоит знать: команда с префиксом + выполняется с полными привилегиями независимо от этого. Этот префикс — законный инструмент для одного привилегированного шага и удобный способ отменить только что построенное разделение, поэтому любой + в этом юните считайте тем, что нужно обосновать на ревью.
Держите вход триггера минимальным и не секретным. Идентификатор сессии, сопоставленная учётная запись и метка времени — обычно всё, что нужно. Никогда не передавайте через границу фрагмент оболочки, путь к исполняемому файлу или блок окружения от клиента, потому что принимающая сторона не может отличить законное значение от внедрённого.
Проверьте идентичность, которая действительно работала
Просмотр конфигурации — не доказательство. После одобренного тестового подключения подтвердите UID, который коснулся файла, и UID, который демон всё ещё использует:
stat -c '%U %G %y %n' /home/alice/.local/state/vpn-status.json
ps -o user=,pid=,cmd= -p "$(pgrep -o -f 'openvpn --daemon')"
systemd-run --uid=alice --pipe /usr/local/bin/refresh-vpn-status --dry-run
Первая строка должна показать Алису и метку времени внутри вашего тестового окна. Вторая должна по-прежнему показывать root или служебную учётную запись пакета, что доказывает, что демон не сдвинулся. Третья — самый дешёвый способ проверить права действия, не дожидаясь настоящего переподключения.
Если владелец файла — Алиса, но его группа или режим изменились между запусками, ищите UMask= или разницу в правах каталога, а не логическую ошибку. Если метка времени вообще не сдвинулась, триггер так и не сработал, и полезный следующий шаг — journalctl -u для юнита действия, а не ещё одно переподключение клиента.
Чего событие подключения всё ещё не доказывает
Установленный туннель — это один факт. Он не означает, что сеанс рабочего стола разблокирован, шина сеанса доступна или что приватный ресурс, нужный действию, уже доступен. Юнит oneshot под UID Алисы имеет её идентичность и ничего из её сеанса, поэтому всё, что требует графической шины, должно идти через поддерживаемый механизм пользовательского сеанса и определять, что происходит, когда она вышла из системы.
Переподключения тоже повторяются. Ноутбук, засыпающий в поезде, может выдать десяток событий подключения за час, а хук, который каждый раз дописывает строку, превращает файл состояния в журнал, который никто не может прочитать. Сделайте действие идемпотентным или позвольте ему заменять одну запись с ключом по сессии.
Наконец, ограничьте радиус поражения в самом юните: представление файловой системы только для чтения вне каталога состояния, никаких новых привилегий и явный тайм-аут. Разделение демона и действия пользователя ценно само по себе, но оно стоит гораздо больше, когда действие не может сделать ничего, кроме того единственного, что вы одобрили.