waqur: (Евро)
[personal profile] waqur
Операционная система FreeBSD содержит механизм jail(8), позволяющий создавать изолированные группы процессов, вроде легковесных виртуальных машин.

Джейлы эффективнее Xen-клеток, практически не потребляют ресурсов (т.к. используют общее с хостом ядро). Процессы из джейлов легко управляются с хоста (ps -A, kill, jls, jexec и т.п.). Каждый джейл видит только часть файловой системы и может быть ограничен в ресурсах (rctl, zfs set quota). На джейлах часто строится FreeBSD VPS-хостинг (например RootBSD.net), где каждый клиент имеет свою маленькую ОС и набор приложений с правами рута (извращенцы даже могут поставить GNU/kFreeBSD), однако в этой статье мы рассмотрим другую задачу — запуск web-сервера в минимальном джейле.

Зачем это нужно? В web-серверах, как и в другом софте, написанном на C, время от времени находят уязвимости к атакам типа "переполнение буфера", позволяющие удалённо захватить контроль над системой. Эти ошибки являются следствием технологии разработки софта, поэтому не могут быть исправлены окончательно. Каждая из них — предпоследняя.

Представьте себе: джейл, у которого в папке /bin нет шелла, только бинарные nginx и php-fpm. В папке /dev только null, urandom и ещё пара подобных устройств. В папке /etc только конфиги nginx'а, php.ini и файл master.passwd, в котором хэши паролей обоих пользователей root и www заменены звёздочками. Также в джейле есть /www, /logs и /tmp, где нет ничего интересного. Такой джейл может быть взломан через переполнение стека web-сервера или другого серверного процесса, но нагадить в основной системе или украсть пароль рута из него нельзя. Системный вызов mount(2) не работает, ICMP не работает, ноды устройств создавать нельзя, список внешних процессов недоступен, сигналы им отправить нельзя и так далее.




1. Пересборка ядра для поддержки проброса портов в джейл


Для начала нужно сделать так, чтобы файерволл ipfw поддерживал проброс TCP/IP-портов внутрь джейла из основной системы (ip forwarding).

1. Выполнить команды
# cd /usr/src/sys/amd64/conf
# cp GENERIC MYCONF
# nano MYCONF


2. Дописать в конец файла следующие строки
options  IPFIREWALL 
options  IPFIREWALL_VERBOSE
options  IPFIREWALL_VERBOSE_LIMIT=100
options  IPFIREWALL_DEFAULT_TO_ACCEPT 
options  IPDIVERT 
options  IPFIREWALL_FORWARD


3. Пересобрать и переустановить ядро; перезагрузить сервер
# cd /usr/src
# make clean
# make buildkernel KERNCONF=MYCONF
# make installkernel KERNCONF=MYCONF
# make clean
# shutdown -r now


4. Проверить как файерволл загрузился:
# dmesg | grep ipfw



2. Сборка специального nginx для использования в джейле


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

1. Посетить web-страницу http://nginx.org/en/download.html и записать URL для скачивания исходных кодов самой свежей стабильной версии nginx. На момент написания этого руководства — http://nginx.org/download/nginx-1.2.4.tar.gz

2. Посетить web-страницу http://www.zlib.net/ и записать URL для скачивания исходных кодов самой свежей версии zlib. На момент написания этого руководства — http://zlib.net/zlib-1.2.7.tar.gz

3. Посетить web-страницу http://www.pcre.org/ и записать URL для скачивания исходных кодов самой свежей версии PCRE. На момент написания этого руководства —
ftp://ftp.csx.cam.ac.uk/pub/software/programming/pcre/pcre-8.31.tar.bz2

4. Скачать и распаковать nginx и его зависимости:
# cd ~
# mkdir nginx-build
# cd nginx-build
# wget http://nginx.org/download/nginx-1.2.4.tar.gz
# wget http://zlib.net/zlib-1.2.7.tar.gz
# wget ftp://ftp.csx.cam.ac.uk/pub/software/programming/pcre/pcre-8.31.tar.bz2
# ls -l
# tar zxf nginx-1.2.4.tar.gz
# tar zxf zlib-1.2.7.tar.gz
# tar jxf pcre-8.31.tar.bz2


5. Подготовить сборочный скрипт для nginx:
# cd nginx-1.2.4
# wget http://.../nginx_build.sh

#!/usr/local/bin/bash
mkdir /wwwjail
mkdir /wwwjail/bin
mkdir /wwwjail/etc
mkdir /wwwjail/logs
mkdir /wwwjail/www
./configure --sbin-path=/bin                          \
     --conf-path=/etc/nginx.conf                      \
     --http-log-path=/logs/access.log                 \
     --error-log-path=/logs/error.log                 \
     --pid-path=/logs/nginx.pid                       \
     --lock-path=/logs/nginx.lock                     \
     --user=www --group=www                           \
     --with-ipv6                                      \
     --without-select_module                          \
     --without-poll_module                            \
     --with-aio_module                                \
     --with-file-aio                                  \
     --with-http_ssl_module                           \
     --with-http_gzip_static_module                   \
     --without-http_ssi_module                        \
     --without-http_userid_module                     \
     --without-http_access_module                     \
     --without-http_geo_module                        \
     --without-http_map_module                        \
     --without-http_split_clients_module              \
     --without-http_referer_module                    \
     --without-http_uwsgi_module                      \
     --without-http_scgi_module                       \
     --without-http_memcached_module                  \
     --without-http_empty_gif_module                  \
     --without-http_upstream_ip_hash_module           \
     --http-client-body-temp-path=/tmp/client         \
     --http-proxy-temp-path=/tmp/proxy                \
     --http-fastcgi-temp-path=/tmp/fastcgi            \
     --http-uwsgi-temp-path=/tmp/uwsgi                \
     --http-scgi-temp-path=/tmp/scgi                  \
     --without-http-cache                             \
     --with-pcre=/root/nginx-build/pcre-8.31          \
     --with-pcre-jit                                  \
     --with-zlib=/root/nginx-build/zlib-1.2.7         \
     --with-cc-opt="-O2 -fomit-frame-pointer -static" \
     --with-ld-opt="-s -static"

if [ "$?" -ne "0" ]; then
    echo "=== build script: failed to configure nginx ==="
    exit 1
fi
make
if [ "$?" -ne "0" ]; then
    echo "=== build script: failed to make nginx ==="
    exit 1
fi
make DESTDIR=/wwwjail install
if [ "$?" -ne "0" ]; then
    echo "=== build script: failed to install nginx ==="
    exit 1
fi
echo "=== build script: OK ==="

Полагаю, что JIT внутри джейла — это вполне безопасно. Тем более, что внешний пользователь всё равно не может задавать регэкспы, которые JITятся: они есть только в конфигах web-сервера.

6. Скомпилировать статический nginx для использования в джейле
# chmod +x nginx_build.sh
# ./nginx_build.sh


7. Проверить, что nginx правильно собрался:
# file /wwwjail/bin/nginx



3. Испытание nginx'а в chroot-окружении


Для начала, можно не врубать ограничения джейла на всю катушку, а только ограничить корневой каталог с помощью chroot(2). Такие лёгкие ограничения не годятся для боевой системы, но позволяют проверить, что web-сервер — результат сборки — работает, не разгребая проблемы с firewall'ом и т.п.:
1. Удалить каталог /wwwjail/usr
2. Присвоить файлу /wwwjail/etc/nginx.conf следующее содержимое:
user              www;
worker_processes  1;
error_log         /logs/error.log;
pid               /logs/nginx.pid;

events {
    worker_connections  1024;
    use                 kqueue;
}

http {
    include            /etc/mime.types;
    default_type       application/octet-stream;
    access_log         /logs/access.log  combined;
    keepalive_timeout  65;
    gzip               on;

    server {
        listen       80;
        server_name  localhost;
        charset      utf-8;
        #access_log  /logs/localhost.access.log  combined;

        location / {
            root   /www;
            index  index.html index.htm;
        }
    }
}


3. Создать минимальную БД пользователей в джейле:
# cd /wwwjail/etc
# echo 'root:*:0:0::0:0:Root:/:/bin/nologin' > master.passwd
# echo 'www:*:80:80::0:0:WWW:/:/bin/nologin' >> master.passwd
# pwd_mkdb -d . master.passwd
# echo 'wheel:*:0:root' > group
# echo 'www:*:80:www' >> group
# chmod 644 group
# cd ..


4. Создать подкаталоги для журналов и временных файлов:
# mkdir tmp
# chmod 777 logs tmp


5. Настроить резолвинг DNS-имён в джейле:
# cp /etc/resolv.conf /wwwjail/etc/

6. Подготовить ограниченный devfs для джейла:
# mkdir /wwwjail/dev
# mount -t devfs devfs /wwwjail/dev
# devfs -m /wwwjail/dev rule apply hide
# devfs -m /wwwjail/dev rule apply path null unhide
# devfs -m /wwwjail/dev rule apply path zero unhide
# devfs -m /wwwjail/dev rule apply path random unhide
# devfs -m /wwwjail/dev rule apply path urandom unhide
# ls -l /wwwjail/dev


7. Подготовить пробный индексный файл:
# echo '<html><body>Test</body></html>' > /wwwjail/www/index.html

8. Испытать работоспособность nginx'а в chroot-окружении:
# cd ~
# chroot /wwwjail /bin/nginx
# wget http://localhost/
# cat index.html
# cat /wwwjail/logs/access.log
# chroot /wwwjail /bin/nginx -s quit



4. Перенос nginx'а в настоящий джейл


1. В файле /wwwjail/etc/nginx.conf параметр http/server/listen задать в виде "192.168.101.1:80"

2. Испытать работоспособность nginx'а в jail-окружении
# cd ~
# ifconfig -l
# ifconfig rl0 inet alias 192.168.101.1/32
# ifconfig
# jail -c path=/wwwjail host.hostname=example.com \
ip4.addr=192.168.101.1 command=/bin/nginx
# ps -A
# jls
# wget http://192.168.101.1/
# cat /wwwjail/logs/access.log

Замените example.com на доменное имя вашего сайта, rl — на имя драйвера сетевой карты (ifconfig -l, ifconfig).

3. В правила файерволла добавить следующие строки:
# port forwarding from internet to jails
/sbin/ipfw 100 add fwd 192.168.101.1,80 tcp from any to 1.1.1.1/32 80
/sbin/ipfw 101 add fwd 192.168.101.1,443 tcp from any to 1.1.1.1/32 443

Замените 1.1.1.1 на IP-адрес вашего сайта. Числа 100 и 101 задают относительный приоритет правил и не имеют абсолютного смысла; у меня эти два правила предпоследние.

4. Сделайте так, чтобы новые правила файерволла вступили в силу.
Следующие команды выполнить на внешней машине, например из Cygwin под Windows:
# cd /
# wget http://1.1.1.1/
# ls -l
# cat index.html
# rm index.html


5. Остановить временный джейл и включить в автозагрузку запуск джейла с nginx'ом
# kill -QUIT `cat /wwwjail/logs/nginx.pid`
# jls
# nano /etc/rc.conf


добавить следующие строки:
ifconfig_rl0_alias0="inet 192.168.101.1/32"
jail_enable="YES"
jail_list="wwwjail"
jail_wwwjail_hostname="example.com"
jail_wwwjail_ip="192.168.101.1"
jail_wwwjail_rootdir="/wwwjail"
jail_wwwjail_devfs_enable="YES"
jail_wwwjail_devfs_ruleset="devfsrules_jail"
jail_wwwjail_fdescfs_enable="NO"
jail_wwwjail_procfs_enable="NO"
jail_wwwjail_exec_start="/bin/nginx"
jail_wwwjail_exec_stop="/bin/nginx -s quit"


# umount /wwwjail/dev
# /etc/rc.d/jail start wwwjail
# jls


6. Ещё раз проверить работоспособность джейла wget'ом извне; после чего перезагрузить сервер и провести этот тест ещё раз. После этого удалить временный каталог, в котором происходила сборка nginx:
# cd ~
# ls -l
# rm -rf nginx-build



5. Сборка и включение сервера PHP-FPM


Собрать статический PHP непросто, но возможно. FreeBSDшный ld не понимает параметра "-all-static", в отличие от скрипта libtool. php-fpm и nginx будут обмениваться данными через UNIX domain socket по протоколу FastCGI.

# cd /usr/ports/graphics/gd
# make install clean
# cd ../../textproc/libxml2
# make install clean
# cd ../../ftp/curl
# make install clean
# cd ../../math/gmp
# make install clean
# cd ../../security/libmcrypt
# make install clean
# cd ../../databases/sqlite3
# make install clean
# cd ~
# mkdir php-fpm-build
# cd php-fpm-build
# wget http://us2.php.net/get/php-5.4.9.tar.bz2/from/this/mirror
# mv mirror php-5.4.9.tar.bz2
# wget http://.../build_php.sh
# chmod +x build_php.sh
#!/usr/local/bin/bash
cd /root/php-fpm-build
rm -rf php-5.4.9
tar jxf php-5.4.9.tar.bz2
cd php-5.4.9
LDFLAGS="-Wl,-s -Wl,-static -static-libgcc" \
   ./configure --enable-static --disable-shared \
               --enable-fpm \
               --with-fpm-user=www --with-fpm-group=www \
               --prefix=/ --with-config-file-path=/etc \
               --localstatedir=/logs \
               --without-pear --disable-phar \
               --with-gd --with-jpeg-dir --with-freetype-dir --enable-exif \
               --with-openssl --with-curl --with-mcrypt --with-mhash \
               --with-zlib --with-bz2 --enable-bcmath --with-gmp \
               --with-gettext --enable-mbstring --enable-calendar \
               --with-mysql=mysqlnd --with-mysqli=mysqlnd \
               --with-pdo-mysql=mysqlnd --with-pdo-sqlite
if [ "$?" -ne "0" ]; then
    echo "=== build script: failed to configure PHP ==="
    exit 1
fi
LDFLAGS="-Wl,-s -Wl,-static -all-static -static-libgcc" make all
if [ "$?" -ne "0" ]; then
    echo "=== build script: failed to make PHP ==="
    exit 1
fi
rm -rf /wwwjail2
mkdir /wwwjail2
make INSTALL_ROOT=/wwwjail2 install
if [ "$?" -ne "0" ]; then
    echo "=== build script: failed to install PHP ==="
    exit 1
fi
echo "=== build script: OK ==="


# ./build_php.sh
# cd /wwwjail2
# ls -l sbin
# file sbin/php-fpm
# cp sbin/php-fpm ../wwwjail/bin/
# cp etc/php-fpm.conf.default ../wwwjail/etc/
# cd ..
# rm -rf wwwjail2
# cd wwwjail/etc
# cp php-fpm.conf.default php-fpm.conf
# cp ~/php-fpm-build/php-5.4.9/php.ini-production php.ini.default
# cp php.ini.default php.ini
# nano php.ini

Задать следующие опции:
date.timezone = UTC
short_open_tag = On


# nano php-fpm.conf
Задать следующие опции:
pid = /logs/php-fpm.pid
error_log = /logs/php-fpm.log
daemonize = yes
events.mechanism = kqueue
listen = /tmp/fastcgi.socket
listen.owner = www
listen.group = www
listen.mode = 0666
pm = static
pm.max_children = 5
pm.max_requests = 100
request_terminate_timeout = 10m

# jls
# jexec 1 /bin/php-fpm
# ps -A
# cat /wwwjail/logs/php-fpm.log
# cd /wwwjail/etc
# nano nginx.conf

location ... {
    location ~ \.php$ {
        root           /www;
        fastcgi_pass   unix:/tmp/fastcgi.socket;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
        include        /etc/fastcgi_params;
    }
    location ~ \.inc$ {
        # Protect PHP include files from prying eyes
        return  404;
    }
}

# cd ..
# kill -HUP `cat logs/nginx.pid`
# cat logs/error.log
# echo '<?php phpinfo(); ?>' > www/index.php
# cat www/index.php
# cd ~
# wget http://192.168.101.1/index.php
# rm -rf php-fpm-build
# nano /etc/rc.conf

Добавить следующую строку:
jail_wwwjail_exec_afterstart1="/bin/php-fpm"

Проверить, что процесс php-fpm корректно завершается при останове джейла
и что джейл корректно запускается с nginx и php-fpm:
# /etc/rc.d/jail stop wwwjail
# ps -A
# /etc/rc.d/jail start wwwjail
# ps -A



6. Нюансы администрирования


nginx перечитывает свой конфиг по сигналу SIGHUP. Ошибки конфига пишутся в журнал error.log (если были ошибки, web-сервер остаётся со старыми настройками).

php-fpm перечитывает свой конфиг и php.ini по сигналу SIGUSR2. Дочерние процессы порождаются только с помощью fork(2), при этом execve(2) не применяется.

SIGUSR2 используется в nginx для других целей — для бинарного обновления web-сервера без прерывания обслуживания клиентов: http://wiki.nginx.org/CommandLine#Upgrading_To_a_New_Binary_On_The_Fly

Желательно настроить логротацию как описано в newsyslog(8). Оба сервера переоткрывают свои логи по SIGUSR1.

March 2024

S M T W T F S
     12
3456789
10111213141516
17181920212223
24252627282930
31      

Автор стиля

Развернуть

No cut tags
Page generated 2026-03-01 07:51 pm
Powered by Dreamwidth Studios