unicorn-lockdown 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []