Некоторое время назад я сделал для себя решение, которое позволяет эффективно изолировать процессы apache2. Теперь он может обрабатывать каждый запрос от имени своего системного пользователя. Сегодня я хочу поделиться этим решением.

Вот о чем речь:

<VirtualHost *:80>
    ServerName host1.example.com
    ServerAdmin webmaster1@example.com
    ServerUserGroup user1 group1
    DocumentRoot /var/www/host1
</VirtualHost>
<VirtualHost *:80>
    ServerName host2.example.com
    ServerAdmin webmaster2@example.com
    ServerUserGroup user2 group2
    DocumentRoot /var/www/host2
</VirtualHost>

При этом корневые директории виртуальных хостов могут быть доступны только соответсвующим пользователям:

# ls -la /var/www
total 16
drwxr-xr-x   4 root  root   4096 Oct 26 16:10 .
drwxr-xr-x  21 root  root   4096 Oct 26 01:13 ..
drwxr-x---   2 user1 group1 4096 Oct 26 16:10 host1
drwxr-x---   2 user1 group2 4096 Oct 26 16:10 host2

Это не очередные танцы с бубном вогруг многопоточности, запуска процессов от рута и т.п. Основная идея в том, чтобы процесс самостоятельно решил, с какими правами ему необходимо обработать запрос, взял себе эти права, обработал, и снова вернул себе права основного пользователя apache.

При этом я хотел, чтобы apache изначально был ограничен списком пользователей, которые описаны в его конфигурации при запуске, а во время обработки запроса было бы крайне сложно (либо практически невозможно) процессу «взломать» механизм переключения и присвоить себе чужие права.

Основное применение для меня — это linux, но существующих механизмов в его ядре не хватает, чтобы реализовать мою идею только в окружении пользователя. Поэтому решение состоит из 2-х частей — модуль ядра и модуль apache. Модуль ядра предоставляет устройство /dev/ugidctl, через которое можно системным вызовом ioctl(2) управлять списками разрешенных пользователей для процесса в привилегированном режиме, и, собственно, переключать пользователей в непривелигерованном режиме. Со стороны apache этим механизмом пользуется другой модуль, который реализует весь этот фунционал.

Чуть более подробно алгоритм работы следующий:

Корневой процесс apache:

  1. Открывает устройство /dev/ugidctl
  2. В процессе чтения конфигурационного файла собирает список необходимых пользователей
  3. Загружает этот список в ugidctl
  4. Получает случайный ключ от ядра для переключания между пользователями

Дочерний процесс:

  1. Сбрасывает права на основного пользователя
  2. Ждет запроса, смотрит какими правами его надо обслужить
  3. Используя ключ, полученный при инициализации, переключает пользователя
  4. Обрабатывает запрос
  5. Используя ключ, возвращает себе права основного пользователя
  6. goto 2

В случае взлома дочернего процесса, даже при получении ключа для переключений, ограничения списком пользователей не позволят злоумышленнику повысить привилегии в системе. Как максимум — можно будет залезть в любую корневую директорию любого виртуального хоста, что равносильно тому, как немодифицированный apache с MPM prefork обрабатывает все запросы. При этом сложность нахождения ключа в памяти дочернего процесса аналогична сложности нахождения приватных ключей в памяти модуля mod_ssl.

Естественно, согласно всей моей идее, модуль рассчитан только на работу с MPM prefork. Также стоит по возможности избегать многопоточности при обработке запросов внутри процесса apache — мой модуль переключает права только основной нити процесса.

Скачать можно тут:

Модуль ядра
Модуль apache

Небольшая инструкция для сборки на CentOS 7:

Ставим необходимые для сборки пакеты:

[root@el7 ~]# yum install -y gcc httpd-devel kernel-devel

Скачиваем и распаковываем исходники:

[root@el7 ~]# wget -q -O - https://ibuffed.com/pub/ugidctl/ugidctl-0.1.1.tar.gz | tar -xz
[root@el7 ~]# wget -q -O - https://ibuffed.com/pub/ugidctl/mod_ugidctl-1.0.1.tar.gz | tar -xz

Собираем и загружаем модуль ядра:

[root@el7 ~]# cd ~/ugidctl-0.1.1/
[root@el7 ugidctl-0.1.1]# make
[root@el7 ugidctl-0.1.1]# insmod ugidctl.ko

Собираем и загружаем модуль apache2:

[root@el7 ~]# cd ~/mod_ugidctl-1.0.1/
[root@el7 mod_ugidctl-1.0.1]# apxs -i -c mod_ugidctl.c 
[root@el7 mod_ugidctl-1.0.1]# echo 'LoadModule ugidctl_module modules/mod_ugidctl.so' > /etc/httpd/conf.modules.d/99-ugidctl.conf
[root@el7 mod_ugidctl-1.0.1]# systemctl restart httpd

Или можно воспользоваться готовыми сборками для el6 (RHEL 6u2 + / CentOS 6.2 +, только x86_64) и для el7 (RHEL 7 / CentOS 7). На нескольких el6 серверах это решение безотказно работает уже больше года.

Комментарии (22)


  1. xytop
    30.10.2015 11:37
    +1

    А что скажете про mpm-itk?
    Ну и неплохо было бы исходники на github выложить.


    1. Ahcai5oh
      30.10.2015 11:59

      Поддерживаю вопрос про небходимость велосипеда в свете существования MPM-ITK. А исходники обнаружить несложно:
      https://ibuffed.com/pub/ugidctl/ugidctl-0.1.1-preview/
      https://ibuffed.com/pub/ugidctl/mod_ugidctl-1.0.1-preview/


      1. xytop
        30.10.2015 16:39

        Несложно, но github вроде как стандарт уже. За архивы на малоизвестных сайтах уже давно не чествуют.


        1. 004helix
          31.10.2015 17:57

          Отправил зеркала на гитхаб:
          https://github.com/004helix/ugidctl
          https://github.com/004helix/mod_ugidctl


        1. Ahcai5oh
          03.11.2015 03:33
          +2

          Это такой же «стандарт» как винда или андроид. То есть, не стандарт вообще. Пользуются многие, конечно, но навязывать (не путать с принуждением) не очень этично. У гитхаба есть несомненный плюс — найти нужный проект проще, чем где-то ещё, но этот плюс является следствием монополизации рынка. А ведь это даже не инструмент, а, по сути, коммерческий сервис. А решение выложить дерево исходников как есть на HTTP-сервере действительно несколько странное, но автору, видимо, для себя достаточно. Извиняюсь за оффтоп, конечно.


          1. 004helix
            03.11.2015 15:01

            Тоже не понимаю, почему гитхаб это уже «стандарт». Ссылки на дерево исходников на http, кстати, я даже не публиковал. В статье просто ссылки на архивы с исходниками.


            1. xytop
              03.11.2015 21:39

              Открытые проекты принято размещать на специализированных хостингах.
              Преимущество таких спецхостингов в том, что человек может не только код просмотреть онлайн, но и завести issue или прочитать документацию к проекту онлайн, подправить код или попросить добавить какие-то фичи. Это очень удобно, социализация во все поля :)
              Раньше это были SourceForge, Google Code и т.д.
              SourceForge себя скомпрометировал, Google Code закрылся и сам предложил перекинуть все проекты на Github.
              Еще как вариант есть Bitbucket.
              Вот тут можно прочитать про opensource best practices: opensource.com/business/14/9/community-best-practices-new-era-open-source


    1. 004helix
      30.10.2015 13:14
      +3

      Обратите внимание на «Quirks and warnings» на их сайте:

      Since mpm-itk has to be able to setuid(), it runs as root (although restricted with POSIX capabilities and seccomp v2 where possible) until the request is parsed and the vhost determined. This means that any code execution hole before the request is parsed will be a potential root security hole. (The most likely place is probably in mod_ssl.) This is not likely to change in the near future, as socket passing, the most likely alternative solution, is very hard to get to work properly in a number of common use cases (e.g. SSL).

      Т.е. осноное — все дочерние процессы висят от рута. Второе — после обработки запроса процесс может только завершиться, т.к. не способен обрабытывать запросы к других хостам. Сильный удар по производительности, не говоря уже про keep-alive соединения, в котором последующие запросы просто невозможны, если идут на другой хост.


    1. BAV_Lug
      30.10.2015 21:45

      Тоже поддержу вопрос. Уже лет пять точно юзаю mpm-itk. Чем принципиально ваше решение лучше?


      1. xytop
        30.10.2015 22:06

        Вот прямо над вами ответили :)
        mpm-itk делает switch на юзера только после парсинга запроса (т.е. в случае какого-то эксплойта для обработки запроса можно получить root), а эта библиотека делает всю обработку от юзера изначально.


        1. equand
          01.11.2015 17:03

          Есть же perser, оно и chroot умеет.


          1. 004helix
            01.11.2015 17:11

            Полагаю вы имели ввиду peruser MPM. Проект скорее мертв чем жив. Да и большие проблемы с масшрабированием у него были.


            1. equand
              01.11.2015 17:42

              Да, через некоторое время процессы сегфолтятся, однако единственная проблема. Но всегд можно запускать несколько апачей на юзеров.


              1. 004helix
                01.11.2015 19:44

                Да, через некоторое время процессы сегфолтятся, однако единственная проблема.

                Там не только в сегфолтах проблемы, поверьте. Я там где-то даже в списке авторов поскакивал.

                Но всегд можно запускать несколько апачей на юзеров.

                Конечно можно. Просто иногда запуск сотни апачей не лучший выход.


  1. kelevra
    30.10.2015 15:19
    -2

    во FreeBSD-порте апача (не только второго, но и первого) уже лет 10 изоляция реализована стартовым скриптом. всё остальное — это изобретения дырявых велосипедов.


    1. 004helix
      30.10.2015 15:56

      Прямо какая-то космическая технология заставить MPM prefork для каждого виртуального хоста выбирать себе соответствующего пользователя. Да ещё и работает автоматически из стартового скрипта.
      Вы читали статью?


  1. jazzl0ver
    30.10.2015 18:43

    Может кому-нибудь пригодится. Довольно давно (уже лет 10 как) решил похожую задачу (разрешить доступ через веб к репозиториям CVS только юзерам, имеющим туда доступ на уровне файловой системы) с помощью скрипта securecgi (http://sourceforge.net/projects/securecgi/). Сам скрипт суидный, но он запускается только для того, чтобы запустить заданный cgi-скрипт (в моему случае perl-скрипт) под id пользователя из переменной окружения REMOTE_USER. Так что даже, если cgi-скрипт окажется дырявым, система не окажется полностью скомпрометированной.

    В то время пытался через список рассылки апача уговорить разработчиков реализовать такую функциональность к коре или через модуль, но поддержки у них моя инициатива не нашла. Аргументация была более чем странная (на мой взгляд): если взломают http-юзера, то смогут запустить любой suid- файл под этим юзером, а это — полшага до рута.


    1. 004helix
      30.10.2015 22:18

      Для безопасного запуска cgi-скриптов в apache давно есть suexec


      1. jazzl0ver
        30.10.2015 22:38

        suexec — отличный механизм. проблема в том, что он может работать только под тем юзером, который жестко прописан в конфиге апача. динамически становиться нужным юзером он не умеет.


  1. cvss
    30.10.2015 19:33

    Хороший способ. Мы в нет.ру использовали похожую схему под freebsd 4 и apache 1, пока не перестали использовать эту платформу в 2011-м. Похожую вплоть до обмена куками (ключем) между ядром и апачем.

    У вас даже лучше реализация тем, что сначала рутовый процесс передает в ядро список uid, на которые можно менять euid дочерних apache. В нашем случае использовался не список uid, а принадлежность диапазону выделенных для веб-пользователей uid.


    1. cvss
      30.10.2015 19:52
      +1

      Но я бы хотел предостеречь от применения этой схемы при наличии стандартных, хоть и более сложных, альтернатив. Мы это делали только для работы mod_php с uid пользователя, так как не было выбора — во времена php3 и php4 не было ни php-fpm, ни других нормально работающих. Все остальные проблемы изоляции (запрет доступа к файлам других пользователей и запуск CGI/FCGI скриптов) решаются стандартными средствами unix и apache.

      Для mod_perl и многих других интерпретаторов, встроенных в apache, изоляция через смену uid ничего не дает, так как пространство глобальных имен переменных является общим для всех пользователей. Соответственно, это обеспечивает и возможность доступа к чужим приложениям, и вероятность конфликта при использовании одинаковых имен.


      1. 004helix
        30.10.2015 22:26

        Согласен, в первую очередь моя реализация рассчитана для mod_php. В случае запуска приложений отдельно с помощью php-fpm, uwsgi и т.п. сильно возрастают накладные расходы на память, если виртуальных хостов много, а по одельности у них небольшая посещаемость. Плюс можно использовать общий opcache для php, например, что тоже хорошо экономит память.