unicorn-lockdown 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e3ce59cddf9d01567fd8a064530c8d1cc54e45a96eecb54fbacfc376ed73c030
4
+ data.tar.gz: 7e52c5cc4e6ed7673e84fdce2b67516284a49409d1e5a5adcc0a489a401f52e6
5
+ SHA512:
6
+ metadata.gz: 42147643aa1eb541874fc61ef91152800617b91fb69e57f3765d0ea33f372f787a1fa9074eb9eb74400fe2f4ec4f699213875a0ec9446ac08ae6b8f1baad3e08
7
+ data.tar.gz: c74fb916793633f0da277b1541cce8d9773a985035b0f36bfef9f545abf28e1c8f38796d0fd18da5525ab49ff2a46e4be428cc25384461e53c8615095b9a1ec9
data/MIT-LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ Copyright (c) 2018 Jeremy Evans
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to
5
+ deal in the Software without restriction, including without limitation the
6
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7
+ sell copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16
+ THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,212 @@
1
+ = unicorn-lockdown
2
+
3
+ unicorn-lockdown is a helper library for running Unicorn on OpenBSD in a way
4
+ that supports security features such as chroot, privdrop, fork+exec,
5
+ and pledge.
6
+
7
+ With the configuration unicorn-lockdown uses, the unicorn process executes as root,
8
+ and the unicorn master process continues to run as root. The master process
9
+ forks worker processes, which re-exec (fork+exec) so that a new memory layout
10
+ is used in each worker process. The work process then loads the application,
11
+ after which it chroot's to the application's directory, drops root privileges
12
+ and then runs as the application user (privdrop), then runs pledge to limit
13
+ the allowed system calls to the minimum required to run the application.
14
+
15
+ == Assumptions
16
+
17
+ unicorn-lockdown assumes you are using OpenBSD 6.2+ with the nginx and
18
+ rubyXY-unicorn packages installed, and that you have a unicorn symlink in
19
+ the PATH to the appropriate unicornXY executable.
20
+
21
+ It also assumes you have a SMTP server listening on localhost port 25 to
22
+ receive notification emails of worker crashes, if you are notifying for those.
23
+
24
+ == Usage
25
+
26
+ === unicorn-lockdown-setup
27
+
28
+ To start the process of setting up your system to use unicorn-lockdown, run
29
+ the following as root after reading the file and understanding what it does.
30
+
31
+ unicorn-lockdown-setup
32
+
33
+ Briefly, the configuration this uses the following directories:
34
+
35
+ /var/www/sockets :: Stores unix sockets that Unicorn listens on and Nginx uses.
36
+ /var/www/requests :: Stores temporary files for each request with request info,
37
+ used for crash notifications
38
+ /var/log/unicorn :: Stores unicorn log files, one per application
39
+ /var/log/nginx :: Stores nginx log files, two per application, one for access
40
+ and one for errors
41
+
42
+ This adds a _unicorn group that all per-application users will use as their
43
+ group, as well as a /etc/rc.d/rc.unicorn file that the per application
44
+ /etc/rc.d/unicorn_* files will use.
45
+
46
+ === unicorn-lockdown-add
47
+
48
+ For each application you want to run with unicorn lockdown, run the following
49
+ as root, again after reading the file and understanding what it does:
50
+
51
+ unicorn-lockdown-add
52
+
53
+ Here's an excerpt of the usage:
54
+
55
+ Usage: unicorn-lockdown-add [options] app_name
56
+ Options:
57
+ -c rackup_file rackup configuration file
58
+ -d dir application directory name
59
+ -f unicorn_file unicorn configuration file relative to application directory
60
+ -o owner operating system application owner
61
+ -u user operating system user to run application
62
+ --uid uid user id to use if creating the user when -U is specified
63
+
64
+ It is a very good idea to specify -o and -u, the other options can be ignored
65
+ if you are OK with the default values. The owner (-o) and the user (-u) should be
66
+ different. The user is the user the application runs as, and should have very limited
67
+ access to the application directory. The owner is the user that owns the application
68
+ directory and can make modifications to the application.
69
+
70
+ === unicorn-lockdown
71
+
72
+ unicorn-lockdown is the library required in your unicorn configuration
73
+ file for the application, to handle configuring unicorn to run the app
74
+ with chroot, privdrop, fork+exec, and pledge.
75
+
76
+ When you run unicorn-lockdown-add, it will create the unicorn configuration
77
+ file for the app if one does not already exist, looking similar to:
78
+
79
+ require 'unicorn-lockdown'
80
+
81
+ Unicorn.lockdown(self,
82
+ :app=>"app_name",
83
+ :user=>"user_name", # Set application user here
84
+ :pledge=>'rpath prot_exec inet unix' # More may be needed
85
+ :email=>'root' # update this with correct email
86
+ )
87
+
88
+ Unicorn.lockdown options:
89
+
90
+ :app :: (required) a short string for the name of the application, used
91
+ for socket/log file names and in notifications
92
+ :user :: (required) the user to drop privileges to
93
+ :pledge :: (optional) a pledge string to limit the allowed system calls
94
+ after privileges have been dropped
95
+ :email :: (optional) an email address to use for notifications when the
96
+ worker process crashes or an unhandled exception is raised by
97
+ the application or middleware.
98
+
99
+ With this example pledge:
100
+
101
+ * rpath is needed to read files in the chrooted file system
102
+ * prot_exec is needed in most cases
103
+ * unix is needed for the unix socket to nginx
104
+ * inet is not needed in all cases, but in most applications need some form
105
+ of network access. pf (OpenBSD's firewall) should be used to limit
106
+ access for the application's operating system user to the minimum
107
+ necessary access needed.
108
+
109
+ unicorn-lockdown has specific support for allowing for emails to be sent
110
+ for Unicorn worker crashes (e.g. pledge violations). It also has support
111
+ for using rack-unreloader to run your application in development mode
112
+ under the chroot while allowing for reloading files if they are modified.
113
+ Additionally, unicorn-lockdown modifies unicorn's process status line in a
114
+ way that allows it to be controllable via OpenBSD's rcctl program for
115
+ stopping/starting/reloading/restarting daemons.
116
+
117
+ By default, Unicorn.lockdown limits the client_body_buffer_size to 11MB,
118
+ with the expectation of an Nginx limit of 10MB, such that all client
119
+ requests will be buffered in memory and unicorn will not need to write
120
+ temporary files to disk. If this limit is not correct for your
121
+ application, please call client_body_buffer_size after calling
122
+ Unicorn.lockdown to set an appropriate limit.
123
+
124
+ When Unicorn.lockdown is used with the :email option, if the worker
125
+ process crashes, it will email the address using the contents specified
126
+ by the request file. To make sure there is useful information to email
127
+ in the case of a crash, you need to populate the request information for
128
+ all requests. If you are using Roda, one way to do this is to use the
129
+ error_email or error_mail plugins:
130
+
131
+ plugin :error_email, :from=>'foo@example.com', :to=>'foo@example.com',
132
+ :prefix=>'[app_name]'
133
+ # or
134
+ plugin :error_mail, :from=>'foo@example.com', :to=>'foo@example.com',
135
+ :prefix=>'[app_name]'
136
+
137
+ and then at the top of the route block, do:
138
+
139
+ if defined?(Unicorn) && Unicorn.respond_to?(:write_request)
140
+ Unicorn.write_request(error_email_content("Unicorn Worker Process Crash"))
141
+ end
142
+
143
+ If you don't have useful information in the request file, an email will
144
+ still be sent for notification purposes, but it will not include request
145
+ related information, which could make it difficult to diagnose the
146
+ underlying problem.
147
+
148
+ === roda-pg_disconnect
149
+
150
+ If you are using PostgreSQL as the database for the application, and using
151
+ unix sockets to connect the application to the database, if the database
152
+ is restarted, the application will no longer be able to connect to it. The
153
+ only way to fix this is to kill the worker process and have the master
154
+ process spawn a new worker. The roda-pg_disconnect plugin is a plugin
155
+ for the roda web toolkit to kill the worker if it detects the database
156
+ connection has been lost. This plugin assumes the use of the Sequel database
157
+ library and postgres adapter with the pg driver.
158
+
159
+ In your Roda application:
160
+
161
+ # Sometime before loading the error_handler plugin
162
+ plugin :pg_disconnect
163
+
164
+ === rack-email_exceptions
165
+
166
+ rack-email_exceptions is a rack middleware designed to be the first
167
+ middleware loaded into applications. It rescues unhandled exceptions
168
+ raised by subsequent middleware or the application itself.
169
+
170
+ Unicorn.lockdown will automatically setup this middleware if the :email
171
+ option is used and the RACK_ENV environment variable is set to production,
172
+ such that it wraps the application and all other middleware.
173
+
174
+ It is possible to use this middleware manually:
175
+
176
+ require 'rack/email_exceptions'
177
+ use Rack::EmailExceptions, "app_name", 'foo@example.com'
178
+
179
+ === chrooter
180
+
181
+ chrooter is a library designed to help with testing applications both in
182
+ chroot mode and non-chroot mode. If you are running your application
183
+ chrooted, you must support testing while chrooted otherwise it is very
184
+ difficult to find problems that only occur when chrooted before putting
185
+ the application into production, such as a file being read from outside
186
+ the chroot.
187
+
188
+ chrooter assumes you are using minitest for testing. To use chrooter:
189
+
190
+ require 'minitest/autorun'
191
+ require 'chrooter'
192
+ at_exit{Chrooter.chroot('user_name', 'rpath prot_exec inet unix')}
193
+
194
+ If you run your specs as a regular user, it will execute them without
195
+ chrooting, but in a way that can still catch some problems that occur
196
+ when chrooted. If you run your specs as root, it will chroot to
197
+ the current directory after loading the specs, then drop
198
+ privileges to the user given (and optionally pledging using the given
199
+ pledge string), then run the specs.
200
+
201
+ == autoload
202
+
203
+ As you'll find out if you try to run your applications with chroot,
204
+ autoload is the enemy. Both unicorn-lockdown and chrooter have support
205
+ for handling common autoloaded constants in the rack and mail gems.
206
+ If you use other gems that use autoload, you'll have to add code that
207
+ references the autoloaded constants after the application is loaded but
208
+ before chrooting.
209
+
210
+ == Author
211
+
212
+ Jeremy Evans <code@jeremyevans.net>
data/files/rc.unicorn ADDED
@@ -0,0 +1,21 @@
1
+ [ -z "${unicorn_conf}" ] && unicorn_conf=unicorn.conf
2
+
3
+ daemon="/usr/local/bin/unicorn"
4
+ daemon_flags="-c ${unicorn_conf} -D ${rackup_file}"
5
+
6
+ [ -n "${unicorn_dir}" ] || rc_err "$0: unicorn_dir is not set"
7
+ [ -n "${unicorn_app}" ] || rc_err "$0: unicorn_app is not set"
8
+
9
+ . /etc/rc.d/rc.subr
10
+
11
+ pexp="ruby[0-9][0-9]: unicorn-$unicorn_app-master .*"
12
+
13
+ rc_start() {
14
+ ${rcexec} "cd ${unicorn_dir} && ${daemon} ${daemon_flags}"
15
+ }
16
+
17
+ rc_stop() {
18
+ pkill -QUIT -T "${daemon_rtable}" -xf "${pexp}"
19
+ }
20
+
21
+ rc_cmd $1
data/lib/chrooter.rb ADDED
@@ -0,0 +1,110 @@
1
+ require 'etc'
2
+ require 'pledge'
3
+
4
+ # Loading single_byte encoding
5
+ "\255".force_encoding('ISO8859-1').encode('UTF-8')
6
+
7
+ # Load encodings
8
+ ''.force_encoding('UTF-16LE')
9
+ ''.force_encoding('UTF-16BE')
10
+
11
+ # Don't run external diff program for failures
12
+ Minitest::Assertions.diff = false
13
+
14
+ if defined?(SimpleCov) && Process.euid == 0
15
+ # Prevent coverage testing in chroot mode. Chroot vs
16
+ # non-chroot should not matter in terms of lines covered,
17
+ # and running coverage tests in chroot mode will probable fail
18
+ # when it comes time to write the coverage files.
19
+ SimpleCov.at_exit{}
20
+ raise "cannot run coverage testing in chroot mode"
21
+ end
22
+
23
+ # Chrooter allows for testing programs in both chroot and non
24
+ # chroot modes.
25
+ module Chrooter
26
+ # If the current user is the super user, change to the given
27
+ # user/group, chroot to the given directory, and pledge
28
+ # the process with the given permissions (if given).
29
+ #
30
+ # If the current user is not the super user, freeze
31
+ # $LOADED_FEATURES to more easily detect problems with
32
+ # autoloaded constants, and just pledge the process with the
33
+ # given permissions (if given).
34
+ #
35
+ # This will reference common autoloaded constants in the
36
+ # rack and mail libraries if they are defined. Other
37
+ # autoloaded constants should be referenced before calling
38
+ # this method.
39
+ #
40
+ # In general this should be called inside an at_exit block
41
+ # after loading minitest/autorun, so it will run after all
42
+ # specs are loaded, but before running specs.
43
+ def self.chroot(user, pledge=nil, group=user, dir=Dir.pwd)
44
+ # Work around autoload issues in libraries.
45
+ # autoload is problematic when chrooting because if the
46
+ # constant is not referenced before chrooting, an
47
+ # exception is raised if the constant is raised
48
+ # after chrooting.
49
+ #
50
+ # The constants listed here are the autoloaded constants
51
+ # known to be used by any applications. This list
52
+ # may need to be updated when libraries are upgraded
53
+ # and add new constants, or when applications start
54
+ # using new features.
55
+ if defined?(Rack)
56
+ Rack::MockRequest if defined?(Rack::MockRequest)
57
+ Rack::Auth::Digest::Params if defined?(Rack::Auth::Digest::Params)
58
+ if defined?(Rack::Multipart)
59
+ Rack::Multipart
60
+ Rack::Multipart::Parser
61
+ Rack::Multipart::Generator
62
+ Rack::Multipart::UploadedFile
63
+ end
64
+ end
65
+ if defined?(Mail)
66
+ Mail::Address
67
+ Mail::AddressList
68
+ Mail::Parsers::AddressListsParser
69
+ Mail::ContentTransferEncodingElement
70
+ Mail::ContentDispositionElement
71
+ Mail::MessageIdsElement
72
+ Mail::MimeVersionElement
73
+ Mail::OptionalField
74
+ Mail::ContentTypeElement
75
+ end
76
+
77
+ if Process.euid == 0
78
+ uid = Etc.getpwnam(user).uid
79
+ gid = Etc.getgrnam(group).gid
80
+ if Process.egid != gid
81
+ Process.initgroups(user, gid)
82
+ Process::GID.change_privilege(gid)
83
+ end
84
+ Dir.chroot(dir)
85
+ Dir.chdir('/')
86
+ Process.euid != uid and Process::UID.change_privilege(uid)
87
+ puts "Chrooted to #{dir}, running as user #{user}"
88
+ else
89
+ # Load minitest plugins before freezing loaded features,
90
+ # so they don't break.
91
+ Minitest.load_plugins
92
+
93
+ # Emulate chroot not working by freezing $LOADED_FEATURES
94
+ # This allows to more easily catch bugs that only occur
95
+ # when chrooted, such as referencing an autoloaded constant
96
+ # that wasn't loaded before the chroot.
97
+ $LOADED_FEATURES.freeze
98
+ end
99
+
100
+ unless defined?(SimpleCov)
101
+ if pledge
102
+ # If running coverage tests, don't run pledged as coverage
103
+ # testing can require many additional permissions.
104
+ Pledge.pledge(pledge)
105
+ end
106
+ end
107
+
108
+ nil
109
+ end
110
+ end
@@ -0,0 +1,44 @@
1
+ require 'net/smtp'
2
+
3
+ # Rack::EmailExceptions is designed to be the first middleware loaded in production
4
+ # applications. It rescues errors and emails about them. It's very similar to the
5
+ # Roda email_error plugin, except that it catches errors raised outside of the
6
+ # application, such as by other middleware. There should be very few of these types
7
+ # of errors, but it is important to be notified of them if they occur.
8
+ module Rack
9
+ class EmailExceptions
10
+ # Store the prefix to use in the email in addition to the next application.
11
+ def initialize(app, prefix, email)
12
+ @app = app
13
+ @prefix = prefix
14
+ @email = email
15
+ end
16
+
17
+ # Rescue any errors raised by calling the next application, and if there is an
18
+ # error, email about it before reraising it.
19
+ def call(env)
20
+ @app.call(env)
21
+ rescue StandardError, ScriptError => e
22
+ body = <<END
23
+ From: #{@email}\r
24
+ To: #{@email}\r
25
+ Subject: [#{@prefix}] Unhandled Error Raised by Rack App or Middleware\r
26
+ \r
27
+ \r
28
+ Error: #{e.class}: #{e.message}
29
+
30
+ Backtrace:
31
+
32
+ #{e.backtrace.join("\n")}
33
+
34
+ ENV:
35
+
36
+ #{env.map{|k, v| "#{k.inspect} => #{v.inspect}"}.sort.join("\n")}
37
+ END
38
+
39
+ Net::SMTP.start('127.0.0.1'){|s| s.send_message(body, @email, @email)}
40
+
41
+ raise
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,39 @@
1
+ # frozen-string-literal: true
2
+
3
+ class Roda
4
+ module RodaPlugins
5
+ # The pg_disconnect plugin recognizes any disconnection type errors, and kills the process
6
+ # if those errors are received. This is designed to be used only when using Unicorn as the
7
+ # web server, since Unicorn will respawn a new worker process. This kills the process with
8
+ # the QUIT signal, allowing Unicorn to finish handling the current request before exiting.
9
+ #
10
+ # This is designed to be used with applications that cannot connect to the database
11
+ # after application initialization, either because they are using chroot and the database
12
+ # connection socket is outside the chroot, or because they are using a firewall and access
13
+ # to the database server is not allowed from the application the process is running as after
14
+ # privileges are dropped.
15
+ #
16
+ # This plugin must be loaded before the roda error_handler plugin, and it assumes usage of the
17
+ # Sequel database library with the postgres adapter and pg driver.
18
+ module PgDisconnect
19
+ def self.load_dependencies(app)
20
+ raise RodaError, "error_handler plugin already loaded" if app.method_defined?(:handle_error)
21
+ end
22
+
23
+ module InstanceMethods
24
+ # When database connection is lost, kill the worker process, so a new one will be generated.
25
+ # This is necessary because the unix socket used by the database connection is no longer available
26
+ # once the application is chrooted.
27
+ def call
28
+ super
29
+ rescue Sequel::DatabaseDisconnectError, Sequel::DatabaseConnectionError, PG::ConnectionBad
30
+ Process.kill(:QUIT, $$)
31
+ raise
32
+ end
33
+ end
34
+ end
35
+
36
+ register_plugin(:pg_disconnect, PgDisconnect)
37
+ end
38
+ end
39
+
@@ -0,0 +1,266 @@
1
+ # unicorn-lockdown is designed to be used with Unicorn's chroot support, and
2
+ # handles:
3
+ # * pledging the app to restrict allowed syscalls at the appropriate point
4
+ # * handling notifications of worker crashes
5
+ # * forcing loading of some common autoloaded constants
6
+ # * stripping path prefixes from the reloader in development mode
7
+
8
+ require 'pledge'
9
+
10
+ # Loading single_byte encoding
11
+ "\255".force_encoding('ISO8859-1').encode('UTF-8')
12
+
13
+ # Load encodings
14
+ ''.force_encoding('UTF-16LE')
15
+ ''.force_encoding('UTF-16BE')
16
+
17
+ class Unicorn::HttpServer
18
+ # The file name in which to store request information. The
19
+ # /var/www/requests folder is currently accessable only
20
+ # to root.
21
+ def request_filename(pid)
22
+ "/var/www/requests/#{Unicorn.app_name}.#{pid}.txt"
23
+ end
24
+
25
+ # Override the process name for the unicorn processes, both master and
26
+ # worker. This gives all applications a consistent prefix, which can
27
+ # be used to pkill processes by name instead of using pidfiles.
28
+ def proc_name(tag)
29
+ ctx = self.class::START_CTX
30
+ $0 = ["unicorn-#{Unicorn.app_name}-#{tag}"].concat(ctx[:argv]).join(' ')
31
+ end
32
+ end
33
+
34
+ #
35
+ class << Unicorn
36
+ # The Unicorn::HttpServer instance in use. This is only set once when the
37
+ # unicorn server is started, before forking the first worker.
38
+ attr_accessor :server
39
+
40
+ # The name of the application. All applications are given unique names.
41
+ # This name is used to construct the log file, listening socket, and process
42
+ # name.
43
+ attr_accessor :app_name
44
+
45
+ # A File instance open for writing. This is unique per worker process.
46
+ # Workers should write all new requests to this file before handling the
47
+ # request. If a worker process crashes, the master process will send an
48
+ # notification email with the previously logged request information,
49
+ # to enable programmers to debug and fix the issue.
50
+ attr_accessor :request_logger
51
+
52
+ # The user and group name to run as.
53
+ attr_accessor :user_name
54
+
55
+ # The pledge string to use.
56
+ attr_accessor :pledge
57
+
58
+ # The address to email for crash and unhandled exception notifications
59
+ attr_accessor :email
60
+
61
+ # Helper method to write request information to the request logger.
62
+ # +email_message+ should be an email message including headers and body.
63
+ # This should be called at the top of the Roda route block for the
64
+ # application.
65
+ def write_request(email_message)
66
+ request_logger.seek(0, IO::SEEK_SET)
67
+ request_logger.truncate(0)
68
+ request_logger.syswrite(email_message)
69
+ request_logger.fsync
70
+ end
71
+
72
+ # Helper method that sets up all necessary code for chroot/pledge support.
73
+ # This should be called inside the appropriate unicorn.conf file.
74
+ # The configurator should be self in the top level scope of the
75
+ # unicorn.conf file, and this takes options:
76
+ #
77
+ # Options:
78
+ # :app :: The name of the application (required)
79
+ # :email : The email to notify for worker crashes
80
+ # :user :: The user/group to run as (required)
81
+ # :pledge :: The string to use when pledging
82
+ def lockdown(configurator, opts)
83
+ Unicorn.app_name = opts.fetch(:app)
84
+ Unicorn.user_name = opts.fetch(:user)
85
+ Unicorn.email = opts[:email]
86
+ Unicorn.pledge = opts[:pledge]
87
+
88
+ configurator.instance_exec do
89
+ listen "/var/www/sockets/#{Unicorn.app_name}.sock"
90
+
91
+ # Buffer all client bodies in memory. This assumes an Nginx limit of 10MB,
92
+ # by using 11MB this ensures that client bodies are always buffered in
93
+ # memory, preventing file uploading causing a program crash if the
94
+ # pledge does not allow wpath and cpath.
95
+ client_body_buffer_size(11*1024*1024)
96
+
97
+ # Run all worker processes with unique memory layouts
98
+ worker_exec true
99
+
100
+ # Only change the log path if daemonizing.
101
+ # Otherwise, continue to log to stdout/stderr.
102
+ if Unicorn::Configurator::RACKUP[:daemonize]
103
+ stdout_path "/var/log/unicorn/#{Unicorn.app_name}.log"
104
+ stderr_path "/var/log/unicorn/#{Unicorn.app_name}.log"
105
+ end
106
+
107
+ after_fork do |server, worker|
108
+ server.logger.info("worker=#{worker.nr} spawned pid=#{$$}")
109
+
110
+ # Set the request logger for the worker process after forking. The
111
+ # process is still root here, so it can open the file in write mode.
112
+ Unicorn.request_logger = File.open(server.request_filename($$), "wb")
113
+ Unicorn.request_logger.sync = true
114
+ end
115
+
116
+ if wrap_app = Unicorn.email && ENV['RACK_ENV'] == 'production'
117
+ require 'rack/email_exceptions'
118
+ end
119
+
120
+ after_worker_ready do |server, worker|
121
+ server.logger.info("worker=#{worker.nr} ready")
122
+
123
+ # If an notification email address is setup, wrap the entire app in
124
+ # a middleware that will notify about any exceptions raised when
125
+ # processing that aren't caught by other middleware.
126
+ if wrap_app
127
+ server.instance_exec do
128
+ @app = Rack::EmailExceptions.new(@app, Unicorn.app_name, Unicorn.email)
129
+ end
130
+ end
131
+
132
+ # Before chrooting, reference all constants that use autoload
133
+ # that are probably needed at runtime. This must be done
134
+ # before chrooting as attempting to load the constants after
135
+ # chrooting will break things.
136
+ #
137
+ # This part is the most prone to breakage, as new versions
138
+ # of the rack or mail libraries (or new libraries that
139
+ # use autoload) could break things, as well as existing
140
+ # applications using new features at runtime that were
141
+ # not loaded at load time.
142
+ Rack::Multipart
143
+ Rack::Multipart::Parser
144
+ Rack::Multipart::Generator
145
+ Rack::Multipart::UploadedFile
146
+ Rack::CommonLogger
147
+ Rack::Mime
148
+ Rack::Auth::Digest::Params
149
+ if ENV['RACK_ENV'] == 'development'
150
+ Rack::Lint
151
+ Rack::ShowExceptions
152
+ end
153
+ if defined?(Mail)
154
+ Mail::Address
155
+ Mail::AddressList
156
+ Mail::Parsers::AddressListsParser
157
+ Mail::ContentTransferEncodingElement
158
+ Mail::ContentDispositionElement
159
+ Mail::MessageIdsElement
160
+ Mail::MimeVersionElement
161
+ Mail::OptionalField
162
+ Mail::ContentTypeElement
163
+ Mail::SMTP
164
+ end
165
+
166
+ # Strip path prefixes from the reloader. This is only
167
+ # really need in development mode for code reloading to work.
168
+ pwd = Dir.pwd
169
+ Unreloader.strip_path_prefix(pwd) if defined?(Unreloader)
170
+
171
+ # Drop privileges. This must be done after chrooting as
172
+ # chrooting requires root privileges.
173
+ worker.user(Unicorn.user_name, Unicorn.user_name, pwd)
174
+
175
+ if Unicorn.pledge
176
+ # Pledge after dropping privileges, because dropping
177
+ # privileges requires a separate pledge.
178
+ Pledge.pledge(Unicorn.pledge)
179
+ end
180
+ end
181
+
182
+ # the last time there was a worker crash and the request information
183
+ # file was empty. Set by default to 10 minutes ago, so the first
184
+ # crash will always receive an email.
185
+ last_empty_crash = Time.now - 600
186
+
187
+ after_worker_exit do |server, worker, status|
188
+ m = "reaped #{status.inspect} worker=#{worker.nr rescue 'unknown'}"
189
+ if status.success?
190
+ server.logger.info(m)
191
+ else
192
+ server.logger.error(m)
193
+ end
194
+
195
+ # Email about worker process crashes. This is necessary so that
196
+ # programmers are notified about any pledge violations. Pledge
197
+ # violations immediately abort the process, and are bugs in the
198
+ # application that should be fixed. This can also catch other
199
+ # crashes such as SIGSEGV or SIGBUS.
200
+ file = server.request_filename(status.pid)
201
+ if File.exist?(file)
202
+ if !status.success? && Unicorn.email
203
+ if File.size(file).zero?
204
+ # If a crash happens and the request information file is empty,
205
+ # it is generally because the crash happened during initialization,
206
+ # in which case it will generally continue to crash in a loop until the
207
+ # problem is fixed. In that case, only send an email if there hasn't
208
+ # been a similar crash in the last 5 minutes. This rate-limits the
209
+ # crash notification emails to 1 every 5 minutes instead of potentially
210
+ # multiple times per second.
211
+ if Time.now - last_empty_crash > 300
212
+ last_empty_crash = Time.now
213
+ else
214
+ skip_email = true
215
+ end
216
+ end
217
+
218
+ unless skip_email
219
+ # If the request filename exists and the worker process crashed,
220
+ # send a notification email.
221
+ Process.waitpid(fork do
222
+ # Load net/smtp early, before chrooting.
223
+ require 'net/smtp'
224
+
225
+ # When setting the email, first get the contents of the email
226
+ # from the request file.
227
+ body = File.read(file)
228
+
229
+ # Then get information from /etc and drop group privileges
230
+ uid = Etc.getpwnam(Unicorn.user_name).uid
231
+ gid = Etc.getgrnam(Unicorn.user_name).gid
232
+ if gid && Process.egid != gid
233
+ Process.initgroups(Unicorn.user_name, gid)
234
+ Process::GID.change_privilege(gid)
235
+ end
236
+
237
+ # Then chroot
238
+ Dir.chroot(Dir.pwd)
239
+ Dir.chdir('/')
240
+
241
+ # Then drop user privileges
242
+ Process.euid != uid and Process::UID.change_privilege(uid)
243
+
244
+ # Then use a restrictive pledge
245
+ Pledge.pledge('inet prot_exec')
246
+
247
+ # If body empty, crash happened before a request was received,
248
+ # try to at least provide the application name in this case.
249
+ if body.empty?
250
+ body = "Subject: [#{Unicorn.app_name}] Unicorn Worker Process Crash\r\n\r\nNo email content provided for app: #{Unicorn.app_name}"
251
+ end
252
+
253
+ # Finally send an email to localhost via SMTP.
254
+ Net::SMTP.start('127.0.0.1'){|s| s.send_message(body, Unicorn.email, Unicorn.email)}
255
+ end)
256
+ end
257
+ end
258
+
259
+ # Remove any request logger file if it exists.
260
+ File.delete(file)
261
+ end
262
+ end
263
+ end
264
+ end
265
+ end
266
+
metadata ADDED
@@ -0,0 +1,79 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: unicorn-lockdown
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.0
5
+ platform: ruby
6
+ authors:
7
+ - Jeremy Evans
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-05-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: pledge
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: unicorn
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description:
42
+ email: code@jeremyevans.net
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - MIT-LICENSE
48
+ - README.rdoc
49
+ - files/rc.unicorn
50
+ - lib/chrooter.rb
51
+ - lib/rack/email_exceptions.rb
52
+ - lib/roda/plugins/pg_disconnect.rb
53
+ - lib/unicorn-lockdown.rb
54
+ homepage: https://github.com/jeremyevans/unicorn-lockdown
55
+ licenses:
56
+ - MIT
57
+ metadata: {}
58
+ post_install_message:
59
+ rdoc_options: []
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ requirements: []
73
+ rubyforge_project:
74
+ rubygems_version: 2.7.6
75
+ signing_key:
76
+ specification_version: 4
77
+ summary: Helper library for running Unicorn with chroot/privdrop/fork+exec/pledge
78
+ on OpenBSD
79
+ test_files: []