[Из песочницы] Изоляция виртуальных серверов в apache2 — ugidctl
Некоторое время назад я сделал для себя решение, которое позволяет эффективно изолировать процессы 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:
- Открывает устройство /dev/ugidctl
- В процессе чтения конфигурационного файла собирает список необходимых пользователей
- Загружает этот список в ugidctl
- Получает случайный ключ от ядра для переключания между пользователями
Дочерний процесс:
- Сбрасывает права на основного пользователя
- Ждет запроса, смотрит какими правами его надо обслужить
- Используя ключ, полученный при инициализации, переключает пользователя
- Обрабатывает запрос
- Используя ключ, возвращает себе права основного пользователя
- 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 серверах это решение безотказно работает уже больше года.