Небольшое лирическое введение
Появился как-то у меня заказчик, который захотел странного, а именно простой в управлении
Построение основы для хостинга
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 как универсальный контейнер веб-приложений для создания гибкого и не ограниченного одним языком
Спасибо всем.
Автор: mikevmk