Hook работает, когда вы запускаете его вручную от root, и возвращает Operation not permitted в тот момент, когда служба запускает его. Файл между этими запусками не изменился; изменилось окружение. systemd даёт процессу меньший мир: меньше возможностей, другой /dev, пути файловой системы только для чтения и потолок задач. Читайте сбой как границу, которая отклонила одну конкретную операцию, а не как сломанный скрипт.

Начните с операции, которая была отклонена

Строка журнала о том, что unit завершился с ошибкой, почти ничего не говорит. Полезная строка называет вызов, который был отклонён: узел устройства, который не удалось открыть, каталог, в который не удалось записать, пространство имён, которое не удалось создать. Найдите этот вызов, прежде чем трогать хотя бы одну директиву, потому что каждая соответствует своему ограничению. Затем прочитайте unit, который действительно работал, включая drop-in, а не файл, который поставил пакет.

Запрос ниже печатает путь фрагмента, каталоги drop-in и свойства, важные при отказе устройства или файловой системы. Прочитайте их все, прежде чем решать, какая граница важна:

systemctl show "[email protected]" -p FragmentPath -p DropInPaths -p PrivateDevices -p DevicePolicy -p DeviceAllow -p ProtectHome -p ProtectSystem -p TasksMax
journalctl -u "[email protected]" -b --no-pager | grep -iE 'operation not permitted|permission denied|cannot open|failed'

Ничто в этом блоке не пишет. Если отклонённого вызова нет в журнале, для одного запуска поднимите подробность клиента вместо того, чтобы угадывать директивы. Работающий процесс доказывает, что unit запустился, а не что hook работал с нужными привилегиями.

Пространству имён нужны права на mount, которые устройство TUN никогда не использует

Две операции, которые внутри одного hook выглядят одинаково, имеют разные профили разрешений. Открытие /dev/net/tun и создание интерфейса требует администрирования сети плюс доступа к этому узлу. Создание именованного сетевого пространства имён тоже требует работы с mount, потому что инструмент пространств имён монтирует свежий /sys вид и привязывает каталог состояния на его место. вышестоящий клиентский unit намеренно оставляет администрирование монтирования за пределами своего набора границ, поэтому шаг с пространством имён отклоняется, хотя сам туннель работал бы.

Поэтому выдача узла устройства не спасает скрипт. Разрешение на устройство отвечает на один вопрос, может ли этот процесс открыть этот узел, а вопрос пространства имён другой. Сопоставьте сбойный вызов с его возможностью, прежде чем что-то расширять.

Тест в вашей оболочке наследует все возможности неограниченного root, а unit наследует то, что сохранил его набор границ. Эти два запуска — разные эксперименты даже если двоичный файл один и тот же.

Устройства, защищённые пути и потолки задач

Когда отклонённая операция известна, три группы директив объясняют большинство сбоев. Доступ к устройствам регулирует PrivateDevices, который заменяет /dev минимальным набором псевдоустройств, и DevicePolicy с DeviceAllow, которые решают, что разрешает cgroup. /dev/net/tun не входит в этот минимальный набор, поэтому unit с PrivateDevices=yes не может открыть его, что бы ни говорила политика устройств.

Защита файловой системы отвечает на второй вопрос. ProtectHome=yes делает /home, /root и /run/user недоступными, а ProtectSystem=full делает /usr, /boot и /etc только для чтения. Поэтому hook, который пишет рядом со своим файлом конфигурации, работает в вашей оболочке и падает под unit, с той же формой разрешения, что и отсутствующая возможность. Перенесите записываемый ввод на путь, который разрешает политика, или добавьте узкую ReadWritePaths= запись.

Ограничения ресурсов — третья группа и наименее очевидная. TasksMax= ограничивает, сколько задач unit может создать, а потолок файловых дескрипторов ведёт себя так же для сокетов и дескрипторов. Развёртывание, переросшее потолок, падает с ошибкой ресурсов, а не с ошибкой разрешения, и именно поэтому точное сообщение всё ещё важно.

Вынесите шаг с пространством имён из hook

Подготовка, которой нужна привилегия, которой у клиента быть не должно, относится в собственный unit. Oneshot, который работает до клиента, сохраняет свой результат и упорядочен перед соединением, даёт шагу с пространством имён нужное, не передавая эти права процессу туннеля. Затем клиент ссылается на этот unit вместо вызова скрипта из своего up-hook.

Шаблон ниже — начальная форма, а не то, что применяют непрочитанным. Он держит подготовку там, где живёт привилегия, и оставляет unit туннеля нетронутым:

# /etc/systemd/system/[email protected]
[Unit]
Description=Namespace preparation for %i
Before=openvpn-client@%i.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/sbin/netns-up.sh %i
CapabilityBoundingSet=CAP_NET_ADMIN CAP_SYS_ADMIN
[Install]
WantedBy=multi-user.target

Добавьте порядок через drop-in, а не правкой пакетного unit, чтобы обновление не могло молча отбросить ваше изменение. Каталог привязан к имени экземпляра, поэтому второму профилю нужен свой файл:

sudo mkdir -p /etc/systemd/system/[email protected]
sudo tee /etc/systemd/system/[email protected]/10-namespace.conf >/dev/null <<'CONF'
[Unit]
[email protected]
[email protected]
CONF
sudo systemctl daemon-reload

Повторите тот же вызов после изменения

Исправление подтверждается только тогда, когда тот же вызов даёт другой результат. Перезапустите оба unit, выполните операцию, которая падала, и сравните её результат со строкой журнала, сохранённой до изменения. Unit, который запускается, когда hook больше никогда не работает, — не исправление.

Последовательность ниже держит сравнение честным, потому что проверяет отклонённый вызов, а не цвет unit. Сохраните её вывод рядом со строкой журнала, снятой до изменения:

sudo systemctl restart "[email protected]" "[email protected]"
systemctl status "[email protected]" --no-pager
journalctl -u "[email protected]" -b --no-pager | grep -c 'Operation not permitted'
ip -brief addr show tun0

Считайте отказы, а не читайте последнюю строку, потому что hook может упасть, повториться и сработать, и только число показывает, сдвинулась ли граница. Оставьте в drop-in комментарий с названием ослабленной директивы и причиной, так как следующее обновление сравнивает вашу конфигурацию с пакетной по умолчанию. Unit, который запускается только после отключения всей защиты, не исправлен.

Похожие статьи