Использование Sandbox на Mac OS X Server для изоляции пользовательских веб-приложений

в 5:49, , рубрики: mac os x, nginx, ruby, sandbox, thin, информационная безопасность, метки: , , ,

Небольшое лирическое введение

Появился как-то у меня заказчик, который захотел странного, а именно простой в управлении хостинг, который позволил бы пользователям загружать и изолированно запускать веб-приложения на базе микрофреймворка Camping. И я ему сделал его на предложенном виртуальном сервере под управлением FreeBSD 9.0 с помощью nginx, thin server, и ezjail как средства управления jail'ами (все довольно тривиально, но если будет кому-нибудь интересно — опишу). А через неделю заказчик признался мне, что он вообще-то поклонник решений от Apple и хотел бы видеть ту же систему работающей на его основном сервере под управлением Mac OS X. И я с радостью согласился адаптировать решение, так как раньше не имел удовольствия соприкоснуться с этой системой и хотел ее хоть немного изучить. Было только одно «но» — на MacOS X Server нет jail(8). Так вот, в поисках решения для максимально безопасного запуска загружаемого пользователем приложения (я не мог и не хотел использовать chroot по ряду причин) я нашел чрезвычайно гибкий и прекрасно интегрированный в систему инструмент — Sandbox.

Построение основы для хостинга

Sandbox

Sandbox оказался удивительным инструментом. В чем-то напоминающий AppArmor, в чем-то SeLinux, а в чем-то совершенно уникальный способ держать приложение «в узде» и не давать ему больше возможностей, чем ему реально надо для работы. Способ, которым применяются политики Sandbox — это запуск приложения в «песочнице» с передачей в качестве опции пути к заранее написанному для этого приложения профилю (текстовому файлу, содержащему описание политик безопасности). К некоторому сожалению, Sandbox несколько беднее документирован, чем я привык (подробность FreeBSD Handbook развращает), однако в сети нашлось немало примеров написания конкретных профилей, что значительно облегчило задачу. Мне было необходимо написать профиль для легкого сервера ruby-приложений Thin, каким именно образом он используется, я опишу ниже. Любой профиль начинается с декларации версии языка разметки и, желательно, политики по умолчанию (очевидно запретительного характера в нашем случае). Все директивы или их наборы заключены в круглые скобки. Имена политик (или «операции» — operations) поддерживают маски (wildcard — *), расширяющие сферу применения правил. Фильтры (filters, их всего 6: path network file-mode xattr mach signal) задаются согласно правилам (о синтаксисе смотрите подробнее здесь). Например, path может задаваться строкой буквально (literal), регулярным выражением (regex) и, да простят меня за кальку с английского, «подпутем» (subpath). Все комментарии начинаются с символа ';':

;
; Sandbox profile for application owned by virtual (non-system) user XXXXXX
;

(version 1)

; Запрещаем по умолчанию все
(deny default)

; Я очень долго не хотел разрешать возможность открывать сетевой сокет 
; (надеясь, что найду отдельную политику для unix-сокетов). Однако
; не нашел, а серверу приложений нужна возможность слушать
; unix-сокет
(allow network-bind)

; Так как Thin с параметрами смены пользователя и группы (см. ниже)
; сбрасывает привилегии, ему нужен fork()
(allow process-fork)

; Без доступа с этим частям DirectoryService процесс не мог получить данные о 
; системном пользователе, от имени которого демон должен работать.
(allow mach-lookup
	(global-name "com.apple.system.DirectoryService.libinfo_v1")
	(global-name "com.apple.system.DirectoryService.membership_v1")
)

; Мы должны иметь возможность запускать сам Thin-сервер, а также ruby
; И по-моему что-то еще там, я уже забыл ;-)
(allow process-exec 
	(regex "^/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr")
	(regex "^/usr/bin/thin$")
)

; Эта избыточная секция, так как ниже мы разрешаем все операции file-read на все 
; директории, подпадающие под regex ^/opt/sandbox/apps/XXXXXX
; Оставил ее в каких-то отладочных целях, но раз оставил - покажу
(allow file-read-metadata
	(literal "/opt/sandbox/apps/XXXXXX/log")
	(literal "/opt/sandbox/apps/XXXXXX/tmp")
)

; Нам надо читать все gem'ы, все нужные разделяемые библиотеки и собственно директорию приложения
(allow file-read*
	(literal "/usr/bin/thin")
	(regex "^/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr")
	(regex "^/System/Library/PrivateFrameworks/TrustEvaluationAgent.framework/Versions/A/TrustEvaluationAgent")
	(regex "^/Library/Ruby/Gems/1.8/")
	(regex "^/usr/lib")
	(regex "^/opt/sandbox/apps/XXXXXX")
)

; Нам надо читать и писать в сокет, пид-файл и лог.
(allow file*
	(regex "^/opt/sandbox/apps/XXXXXX/tmp/thin.sock$")
	(regex "^/opt/sandbox/apps/XXXXXX/tmp/thin.pid$")
	(regex "^/opt/sandbox/apps/XXXXXX/log/thin.log$")
)
Thin

Для запуска пользовательских Camping-приложений был выбран Thin. Почему Thin, а не Mongrel, Passenger, uWSGI или что-то еще? Он поддерживал все необходимые функции и оказался не очень требовательным к ресурсам (серьезных исследований, впрочем, я не проводил). Кроме того, я не смог придумать как приготовить Passenger таким образом, чтобы он как-то изолированно запускал приложения, хотя вероятнее всего это как-то возможно (я не беру вариант с запуском многих копий nginx от лица разных пользователей, такой вариант рассматривался, но был отметен) и если кто-нибудь в комментариях предложит работающее решение, буду рад ознакомиться. Мой комбайн-фаворит для практически любых дел — uWSGI из последнего tip — отказался нормально работать на FreeBSD (о чем был оповещен разработчик и все было починено в течение пары дней, но, увы, поезд ушел), а на MacOS X вообще не собирался ни в какую. Mongrel попробовать не успели, остановившись на Thin, уж больно хорошо пошло с ним дело. Итак, вот строка запуска некоего основанного на Camping rack-приложения в контейнере Thin:

cd /opt/sandbox/apps/XXXXXX && 
sandbox-exec -f /opt/sandbox/profiles/XXXXXX.sb 
/usr/bin/thin --socket /opt/sandbox/apps/XXXXXX/tmp/thin.sock 
--rackup /opt/sandbox/apps/XXXXXX/approot/config.ru 
--environment production --timeout 4 --chdir /opt/sandbox/apps/XXXXXX/approot 
--log /opt/sandbox/apps/XXXXXX/log/thin.log 
--daemonize --pid /opt/sandbox/apps/XXXXXX/tmp/thin.pid  
--user thinbot --group thinbot --tag XXXXXX start

Опция 'tag' дает приятную возможность увидеть в top и ps кто именно скушал все ресурсы (системный пользователь используется один для всех запусков).

Nginx

Все тривиально. Никакой статики. Имя виртуального «пользователя» хостинга эквивалентно выделенному ему поддомену:

 server {
                server_name ~(.+).domain.tld;
                set $user $1; 

                location / { 
                        proxy_pass http://unix:/opt/sandbox/apps/$user/tmp/thin.sock:/;
                }   
        }   
Скриптовая обвязка

Для разработки обвязки я использовал sh, потому что люблю простые и переносимые вещи. Критика приветствуется, скрипты остались довольно сыроватыми. Предполагается, что скрипты запускаются от имени суперпользователя (root).
Управление виртуальными пользователями — users_management.sh:

#!/bin/sh

# Mike Kuznetsov 2012 mike4gg@gmail.com

user=$1
action=$2

usage() {
        echo "Usage: `basename $0` <username> <create|remove|list>"
        exit
}

if [ "${action}x" = "x" ]; then
        usage
fi

sb_app_dir=/opt/sandbox/apps/${user}
sb_app_root=${sb_app_dir}/approot
sb_profile=/opt/sandbox/profiles/${user}.sb
thin_sock=${sb_app_dir}/tmp/thin.sock
thin_pid=${sb_app_dir}/tmp/thin.pid
thin_log=${sb_app_dir}/log/thin.log
thinuser=thinbot
thingroup=thinbot

create_sandbox() {
        cat <<EOF > ${sb_profile}
;
; Sandbox profile for application owned by virtual (non-system) user ${user}
;

(version 1)

(deny default)

(allow network-bind)
(allow process-fork)
(allow mach-lookup
        (global-name "com.apple.system.DirectoryService.libinfo_v1")
        (global-name "com.apple.system.DirectoryService.membership_v1")
)
(allow process-exec 
        (regex "^/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr")
        (regex "^/usr/bin/thin$")
)

(allow file-read-metadata
        (literal "${sb_app_dir}/log")
        (literal "${sb_app_dir}/tmp")
)

(allow file-read*
        (literal "/usr/bin/thin")
        (regex "^/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr")
        (regex "^/System/Library/PrivateFrameworks/TrustEvaluationAgent.framework/Versions/A/TrustEvaluationAgent")
        (regex "^/Library/Ruby/Gems/1.8/")
        (regex "^/usr/lib")
        (regex "^${sb_app_dir}")
)

(allow file*
        (regex "^${thin_sock}$")
        (regex "^${thin_pid}$")
        (regex "^${thin_log}$")
)
EOF

        mkdir ${sb_app_dir}
        mkdir ${sb_app_root}
        mkdir ${sb_app_dir}/tmp
        mkdir ${sb_app_dir}/log
        chown -R ${thinuser}:${thingroup} ${sb_app_dir}
}

case ${action} in
        create)
        if [ -d ${sb_app_dir} ]; then
                echo "User's application directory ${sb_app_dir} exists. Exiting"
                usage
        elif [ -f ${sb_profile} ]; then
                echo "User's sandbox profile ${sb_profile} exists. Exiting"
                usage
        fi
        printf "Creating sandbox for user ${user}... "
        create_sandbox
        echo "done"
        ;;

        remove)
        printf "Removing sandbox for user ${user}... "
        if [ -f ${thin_pid} ]; then
                /usr/bin/thin --pid ${thin_pid} stop > /dev/null 2>&1
        fi
        if [ -d ${sb_app_dir} ]; then rm -r ${sb_app_dir}; fi
        if [ -f ${sb_profile} ]; then rm ${sb_profile}; fi
        echo "done"
        ;;

        list)
        printf "UsernametApplication statetPIDtMemory usagen"
        echo "-----------------------------------------------------------------"
        total_mem=0
        for user_ in `ls /opt/sandbox/apps`
                do
                if [ -f /opt/sandbox/apps/${user_}/tmp/thin.pid ]; then
                        pid_=`cat /opt/sandbox/apps/${user_}/tmp/thin.pid`
                        ps ax | grep ^${pid_} > /dev/null
                        if [ $? -eq 0 ]; then
                                mem_=`ps -p ${pid_} -o rss | tail -1 | awk '{ print $1 }'`
                                mem=`expr ${mem_} / 1024`
                                total_mem=`expr ${total_mem} + ${mem}`
                                printf "${user_}ttrunningtt${pid_}tt${mem}Mbn"
                        else
                                printf "${user_}ttnot runningn"
                        fi
                else
                        printf "${user_}ttnot runningn"
                fi
        done
        echo "-----------------------------------------------------------------"
        printf "Total memory usage: ${total_mem}Mbn"
        ;;

        *)
        usage
        ;;
esac

Управление пользовательскими приложениями — application_management.sh:

#!/bin/sh

# Mike Kuznetsov 2012 mike4gg@gmail.com

user=$1
action=$2

sb_app_dir=/opt/sandbox/apps/${user}
sb_app_root=${sb_app_dir}/approot
sb_profile=/opt/sandbox/profiles/${user}.sb
thin_sock=${sb_app_dir}/tmp/thin.sock
thin_pid=${sb_app_dir}/tmp/thin.pid
thin_log=${sb_app_dir}/log/thin.log
thinuser=thinbot
thingroup=thinbot
exitcode=0

usage() {
        echo "Usage: `basename $0` <username> <start|stop|restart>"
        exit 0
}

start_thin() {
        if [ -f ${thin_pid} ]; then
                pid_=`cat ${thin_pid}`
                ps ax | grep ^${pid_} > /dev/null
                if [ $? -eq 0 ]; then
                        echo "Thin instance for user ${user} is already running. Maybe try restart?"
                        usage
                fi
        fi
        printf "Starting thin instance for user ${user}..."
        if [ -f ${thin_pid} ]; then
                rm -f ${thin_pid}
        fi
        cd ${sb_app_dir}
        sandbox-exec -f ${sb_profile} /usr/bin/thin --socket ${thin_sock} --rackup ${sb_app_root}/config.ru 
        --environment production --timeout 4 --chdir ${sb_app_root} --log ${thin_log} --daemonize --pid ${thin_pid} 
        --user ${thinuser} --group ${thingroup} --tag ${user} start
        cd - > /dev/null
        sleep 1
        pid_=`cat ${thin_pid}`
        ps ax | grep ^${pid_} > /dev/null
        if [ $? -eq 0 ]; then
                echo "done"
        else
                echo "FAILED!"
                echo "Last 20 lines of logfile ${thin_log}:"
                tail -20 ${thin_log}
                exitcode=10
        fi
}

stop_thin() {
        if [ -f ${thin_pid} ]; then
                pid_=`cat ${thin_pid}`
                ps ax | grep ^${pid_} > /dev/null
                if [ $? -ne 0 ]; then
                        echo "Thin instance for ${user} user is already stopped or died. Maybe try start?"
                        usage
                fi
        else
                echo "Pid file ${thin_pid} not found. Nothing to stop."
                usage
        fi
        printf "Stopping thin instance for user ${user}..."
        /usr/bin/thin --pid ${thin_pid} stop > /dev/null
        if [ $? -eq 0 ]; then
                echo "done"
        else
                echo "FAILED!"
                echo "Last 20 lines of logfile ${thin_log}:"
                tail -20 ${thin_log}
                exitcode=20
        fi
}

if [ "${action}x" = "x" ]; then
        usage
fi

if [ ! -d ${sb_app_dir} ]; then
        echo "User's application directory ${sb_app_dir} doesn't exist. Exiting"
        usage
elif [ ! -f ${sb_profile} ]; then
        echo "User's sandbox profile ${sb_profile} doesn't exist. Exiting"
        usage
fi

case ${action} in
        start)
        start_thin
        ;;

        stop)
        stop_thin
        ;;

        restart)
        stop_thin
        start_thin
        ;;

        *)
        usage
        ;;
esac

exit ${exitcode}

Заключение

Sandbox это достаточно мощная «песочница», которая, я думаю, может послужить популяризации Mac OS X в качестве серверной платформы.

P.S.: Большое спасибо администрации сайта Хабрахабр, которая разрешила публиковать посты даже с отрицательной кармой. Очень надеюсь на не слишком строгое к этой статье отношение аудитории — это мой первый настоящий пост на Хабре — и надеюсь продолжить писать. Думаю в ближайшее время писать на такие темы: использование хуков git и императорского режима uWSGI для мгновенного веб-представления патча Django-приложения; uWSGI как универсальный контейнер веб-приложений для создания гибкого и не ограниченного одним языком хостинга; особенности национального деплоя СУБД Informix. Но если отговорите — не буду.
Спасибо всем.

Автор: mikevmk


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js