📜 ⬆️ ⬇️

Using Sandbox on Mac OS X Server to isolate custom web applications

Slight lyrical introduction


Somehow, I had a customer who wanted a strange, namely easy-to-manage hosting service that would allow users to download and launch Camping mic frameworks in isolation. And I made it to him on the proposed virtual server running FreeBSD 9.0 using nginx, thin server, and ezjail as jail management tools (everything is rather trivial, but if anyone is interested, I will describe it). A week later, the customer admitted to me that he was actually a fan of Apple solutions and would like to see the same system running on his main server running Mac OS X. And I gladly agreed to adapt the solution, because I had not had the pleasure to touch This system and wanted to study it at least a little. There was only one “but” - there is no jail (8) on MacOS X Server. So, looking for a solution to run the user-loaded application as securely as possible (I couldn’t and didn’t want to use chroot for a number of reasons) I found an extremely flexible and well-integrated tool in the system - Sandbox .

Building the foundation for hosting


Sandbox

Sandbox turned out to be an amazing tool. In something resembling AppArmor, in something SeLinux, but in something completely unique way to keep the application "in check" and not to give him more opportunities than he really needs to work. The way Sandbox policies are applied is to launch an application in the sandbox, passing as an option a path to a profile previously written for this application (a text file containing a description of security policies). To some regret, Sandbox is somewhat poorer documented than I was used to (the details of the FreeBSD Handbook are corrupting), however many examples of writing specific profiles were found on the network, which made the task much easier. I needed to write a profile for a lightweight server of ruby-applications Thin , exactly how it is used, I will describe below. Any profile begins with the declaration of the markup language version and, preferably, the default policy (obviously prohibitive in our case). All directives or their sets are enclosed in parentheses. Names of policies (or "operations" - operations) support masks (wildcard - *), expanding the scope of the rules. Filters (filters, there are only 6 of them: path network file-mode xattr mach signal) are set according to the rules (for syntax see here for more details). For example, a path can be given literally (literal), a regular expression (regex) and, forgive me for tracing from English, “subpath”. All comments begin with a ';':
; ; 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

Thin was chosen to run custom Camping applications. Why Thin, not Mongrel, Passenger, uWSGI or something else? He supported all the necessary functions and was not very demanding of resources (serious research, however, I did not conduct). In addition, I couldn’t figure out how to prepare Passenger so that it would somehow launch applications in isolation, although this is most likely somehow possible (I don’t take the option of running many copies of nginx on behalf of different users, this option was considered, but It was noted) and if someone in the comments offers a working solution, I will be glad to read. My combine-favorite for almost any business - uWSGI from the last tip - refused to work normally with rack-based applications on FreeBSD (the developer was notified and everything was fixed in a couple of days, but, alas, the train left), and on MacOS X was not going to any. Mongrel did not have time to try, focusing on the Thin, it really painfully went well with him. So, here is the launch line of some Camping-based rack application in the Thin container:
')
 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 


The option 'tag' gives a pleasant opportunity to see who has eaten up all the resources in the top and ps (the system user uses one for all launches).

Nginx

Everything is trivial. No static. The name of the virtual “user” of the hosting is equivalent to the subdomain allocated to it:
  server { server_name ~(.+).domain.tld; set $user $1; location / { proxy_pass http://unix:/opt/sandbox/apps/$user/tmp/thin.sock:/; } } 


Scripting

I used sh to design the strapping, because I like simple and portable things. Criticism is welcome, the scripts have remained rather damp. It is assumed that the scripts are run on behalf of the superuser (root).
Virtual User Management - 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 


User application management - 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} 

Conclusion


Sandbox is a fairly powerful sandbox, which, I think, can serve to popularize Mac OS X as a server platform.

PS: Many thanks to the administration of the site Habrahabr, which allowed to publish posts even with negative karma. I really hope that the audience’s attitude to this article is not too strict - this is my first real post on Habré - and I hope to continue writing. I think in the near future to write on such topics: use git hooks and the imperial mode uWSGI for instant web presentation of the Django application patch; uWSGI as a universal web application container for creating flexible and not limited to one language hosting; Peculiarities of the national database deployment Informix. But if you dissuade - I will not.
Thank you all.

Source: https://habr.com/ru/post/144663/


All Articles