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 +7 -0
- data/MIT-LICENSE +18 -0
- data/README.rdoc +212 -0
- data/files/rc.unicorn +21 -0
- data/lib/chrooter.rb +110 -0
- data/lib/rack/email_exceptions.rb +44 -0
- data/lib/roda/plugins/pg_disconnect.rb +39 -0
- data/lib/unicorn-lockdown.rb +266 -0
- metadata +79 -0
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: []
|