Небольшое лирическое введение
Появился как-то у меня заказчик, который захотел странного, а именно простой в управлении хостинг, который позволил бы пользователям загружать и изолированно запускать веб-приложения на базе микрофреймворка 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 — отказался нормально работать с rack-приложениями на 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 "Username\tApplication state\tPID\tMemory usage\n" 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_}\t\trunning\t\t${pid_}\t\t${mem}Mb\n" else printf "${user_}\t\tnot running\n" fi else printf "${user_}\t\tnot running\n" fi done echo "-----------------------------------------------------------------" printf "Total memory usage: ${total_mem}Mb\n" ;; *) 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. Но если отговорите — не буду.
Спасибо всем.
