chroot не работает в OpenVPN: правило AppArmor или capability?
Добавление пути в профиль AppArmor никогда не выдаёт CAP_SYS_CHROOT, а отказ chroot может приходить из трёх разных слоёв. Разберитесь, какой из них менять.
Демон пишет об ошибке chroot, а профиль уже выглядит щедрым, поэтому соблазнительное исправление — расширить правило пути и перезапустить. Обычно это ничего не меняет, потому что chroot закрыт привилегией, которую профиль никогда не мог передать. Чтобы решить, какой слой трогать, нужны три проверки, и они дешёвые: что процессу разрешено держать, что допускает политика и что убрал запускающий до того, как это стало иметь значение.
Две отдельные преграды, и только одна принадлежит AppArmor
chroot требует CAP_SYS_CHROOT в пространстве имён пользователя, в котором работает процесс. Это обычная привилегия Linux, и ядро проверяет её до того, как вызов начнёт что-либо делать с путями. Отдельно от этого профиль AppArmor в режиме enforcing может отказать в capability, даже когда процесс ею обладает, потому что обязательная политика учитывается в дополнение к учётным данным, а не вместо них. Различие важно на практике: два слоя живут в разных файлах, и обычно их поддерживают разные люди.
Обе эти преграды отказывают с одним и тем же errno. Обе дают EPERM, и обе выглядят в журнале демона как общая жалоба на права доступа, поэтому угадывание содержимого профиля отнимает столько времени. Правило пути, которое вы собирались добавить, управляет доступом к файлам уже после успешного chroot; к тому, разрешена ли операция вообще, оно отношения не имеет.
Третья преграда стоит выше обеих. Если менеджер служб или обёртка запустили процесс без capability в его bounding set, никакое изменение AppArmor не вернёт её обратно, а добавление capability в профиль лишь документирует намерение, которое ядро не исполнит.
Сначала прочитайте состояние привилегий, потом правьте политику
Начинайте с работающего процесса, а не с конфигурации, которую вы считаете активной. Файл состояния показывает пять масок capability и флаг no-new-privileges, и они отвечают на разные вопросы:
grep -E '^(CapInh|CapPrm|CapEff|CapBnd|CapAmb|NoNewPrivs):' /proc/$(pidof openvpn)/status
sudo aa-status | grep -i openvpn
CapEff — это набор, важный для вызова: capability, присутствующая в CapBnd, но отсутствующая в CapEff, разрешена потолком и в данный момент не удерживается. CapAmb важна, когда бинарник не является привилегированным в обычном смысле setuid, потому что ambient-возможности — это способ, которым служба сохраняет привилегию через exec без файловых capability. CapInh хранит биты, которые могли бы наследоваться через exec, но только если наследуемый набор исполняемого файла это позволяет, так что бит там сам по себе ничего не даёт.
Затем убедитесь, какой профиль действительно подключён. Процесс может быть ограничен профилем с именем, которого вы не ждали, особенно когда дистрибутив поставляет свой unit-файл и свой профиль вместе, и чтение /proc/PID/attr/current решает это одной строкой. Сравнение вашего отредактированного файла с профилем, который показывает ядро, — это разница между исправлением причины и правкой файла, который никто не загружает.
Добавляйте правило capability только тогда, когда так говорит аудит
Записи ядра и аудита называют слой, который отказал. Ищите отказ, в котором есть операция capable и имя sys_chroot, вместе с профилем, который его породил. Если такая запись есть, процесс уже обладал capability, а AppArmor отказала, и точечное изменение — это правило capability внутри настоящего профиля:
capability sys_chroot,
Держите его фрагментом в профиле, который действительно подключён, и сверьте ключевое слово с справочником по профилям apparmor.d, а не копируйте правило из постороннего руководства. Если положить его в общий include, оно выдаст право каждой ограниченной программе, которая подключает этот файл, а перевод профиля в режим complain скроет следующий отказ вместо того, чтобы устранить этот. Правило в профиле снимает отказ политики; оно не создаёт привилегию, поэтому процесс, у которого CAP_SYS_CHROOT никогда не было в эффективном наборе, продолжит падать даже с этим правилом на месте.
Если запись аудита вообще не показывает отказа AppArmor, прекратите править профиль. Вы имеете дело с проблемой учётных данных, и оставшаяся работа — в unit-файле или в родительском процессе, а не в языке политики.
Менеджер служб держит собственный потолок
systemd задаёт для каждой службы bounding set capability, и по умолчанию этот набор узкий. Всё, что там исключено, недоступно процессу, что бы ни говорил профиль, поэтому усиленные unit-файлы могут падать на операции, которая работала, когда демон запускали вручную. Нужные директивы легко прочитать в unit-файле и легко не заметить, когда об ошибке сообщает само приложение. Drop-in, поставляемые пакетом дистрибутива, — обычный источник потолка, который никто не помнит настраивавшим.
Поднять потолок и выдать capability — это два изменения, а не одно. Запись в bounding set говорит, что процесс может держать capability; она не добавляет её в эффективный набор, а NoNewPrivs может помешать последующему повышению, даже когда всё остальное сходится. Добавляйте только то, что нужно операции, сохраняйте остальное усиление и не идите на компромисс, выдавая все capability, чтобы прошёл один вызов.
Порядок здесь тоже важен. Сначала измените потолок в unit-файле, проверьте, что CapEff теперь содержит нужный бит, и только потом решайте, отказывает ли ещё AppArmor. Если сделать оба изменения сразу, при сохранении сбоя вы снова будете гадать.
Когда отказ уйдёт, проверьте саму тюрьму
Успешный chroot переносит корень, поэтому каждый путь, к которому демон обращается после этого, разрешается внутри нового дерева. Отсутствующая конфигурация, пропавший узел устройства или каталог сертификатов, который не скопировали, теперь проявятся как ошибка файла, и это ошибка раскладки, а не capability. Читайте следующее событие, а не добавляйте ещё одно широкое разрешение. Перенос меняет и смысл относительного пути, поэтому руководства по усилению настаивают на абсолютных путях в конфигурации демона.
Полезно помнить и о том, чего chroot не делает. Это граница путей, а не песочница: процесс, сохранивший привилегию, может из неё выйти, поэтому capability обычно сбрасывают после вызова и поэтому ограничение должно оставаться вокруг демона. Сохранять вместе профиль AppArmor, bounding set capability и ограничения файловой системы — в этом и смысл упражнения, а не остаток, который надо убрать после запуска службы.
Правило решения короткое. Если запись аудита называет AppArmor, добавьте одно правило capability в подключённый профиль. Если эффективный набор пуст, исправьте потолок unit-файла и порядок сброса привилегий. Если профиль не тот, который вы правили, прекратите править и найдите настоящий.