unicorn-lockdown 0.9.0 → 1.0.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 +4 -4
- data/CHANGELOG +39 -0
- data/MIT-LICENSE +1 -1
- data/README.rdoc +157 -84
- data/bin/unicorn-lockdown-add +222 -0
- data/bin/unicorn-lockdown-setup +68 -0
- data/lib/roda/plugins/pg_disconnect.rb +14 -5
- data/lib/unicorn-lockdown.rb +77 -86
- data/lib/unveiler.rb +38 -0
- metadata +11 -8
- data/lib/chrooter.rb +0 -110
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 931209c4514dbb9f15da327fac6ed81f82fed2a504c500a34aa5f6e2624cde5a
|
|
4
|
+
data.tar.gz: 160c9cf285e6c4f8abeb38bce3d1b7006f236607e6f18e606fdcbd8b53f73baa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9f457aefb1571441815165ef9fb032f2de71547debfa81d9a7d4a441019e0671e00258b77bdb4fdca29dca677a5b5e795c65a7f53483cbf67bd12ef2950e4545
|
|
7
|
+
data.tar.gz: de3ee5209b1cb08dd0918448689868cc7b5c423ec07c929df7a4475d222436b32ea271f972f789a7ee13ffcffc8a7bbafd78950741c40392d49b8c69af69473e
|
data/CHANGELOG
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
= 1.0.0 (2020-11-09)
|
|
2
|
+
|
|
3
|
+
* Require unicorn-lockdown-add -o and -u options, and require options have arguments (jeremyevans)
|
|
4
|
+
|
|
5
|
+
* Switch to starting unicorn master process as application user, drop chroot support, require unveil (jeremyevans)
|
|
6
|
+
|
|
7
|
+
* Remove chrooter library (jeremyevans)
|
|
8
|
+
|
|
9
|
+
* Add unveiler library for testing pledged/unveiled applications, similar to chrooter but smaller (jeremyevans)
|
|
10
|
+
|
|
11
|
+
* Add :master_execpledge option to Unicorn.lockdown, for initial pledge of worker processes (jeremyevans)
|
|
12
|
+
|
|
13
|
+
* Add :master_pledge option to Unicorn.lockdown, for pledging the master process (jeremyevans)
|
|
14
|
+
|
|
15
|
+
= 0.13.0 (2019-07-09)
|
|
16
|
+
|
|
17
|
+
* Add Chrooter.unveil for using unveil in tests (jeremyevans)
|
|
18
|
+
|
|
19
|
+
* Support Unicorn.lockdown :unveil and :dev_unveil options for use of unveil instead of chroot (jeremyevans)
|
|
20
|
+
|
|
21
|
+
= 0.12.0 (2019-04-29)
|
|
22
|
+
|
|
23
|
+
* Do not reference the rack middleware unicorn loads by default if unicorn is set to not load default middleware (jeremyevans)
|
|
24
|
+
|
|
25
|
+
= 0.11.0 (2019-03-18)
|
|
26
|
+
|
|
27
|
+
* Support separate log group and process primary group on Unicorn 5.5.0+ using :group option (jeremyevans)
|
|
28
|
+
|
|
29
|
+
* Make Roda pg_disconnect plugin support new Roda dispatch API (jeremyevans)
|
|
30
|
+
|
|
31
|
+
= 0.10.0 (2018-05-21)
|
|
32
|
+
|
|
33
|
+
* Use Mail.eager_autoload! if using the mail gem (jeremyevans)
|
|
34
|
+
|
|
35
|
+
* Add bin files to gemspec (jeremyevans)
|
|
36
|
+
|
|
37
|
+
= 0.9.0 (2018-05-02)
|
|
38
|
+
|
|
39
|
+
* Initial public release
|
data/MIT-LICENSE
CHANGED
data/README.rdoc
CHANGED
|
@@ -1,22 +1,20 @@
|
|
|
1
1
|
= unicorn-lockdown
|
|
2
2
|
|
|
3
|
-
unicorn-lockdown is a helper library for running Unicorn on OpenBSD
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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.
|
|
3
|
+
unicorn-lockdown is a helper library for running Unicorn on OpenBSD with pledge,
|
|
4
|
+
unveil, and fork+exec for increased security.
|
|
5
|
+
|
|
6
|
+
With unicorn-lockdown, unicorn should be started as the application user, which
|
|
7
|
+
should be different than the user that owns the application's files. unicorn
|
|
8
|
+
will pledge the master process, then fork worker processes. The worker
|
|
9
|
+
processes will re-exec (fork+exec), then load the application, then set unveil
|
|
10
|
+
to restrict file system access, then pledge to limit the allowed system calls
|
|
11
|
+
at runtime.
|
|
14
12
|
|
|
15
13
|
== Assumptions
|
|
16
14
|
|
|
17
|
-
unicorn-lockdown assumes you are using OpenBSD 6.
|
|
18
|
-
rubyXY-unicorn packages installed, and that
|
|
19
|
-
the PATH to the appropriate unicornXY executable.
|
|
15
|
+
unicorn-lockdown assumes you are using OpenBSD 6.6+ with the nginx and
|
|
16
|
+
<tt>rubyXY-unicorn</tt> and <tt>rubyXY-pledge</tt> packages installed, and that
|
|
17
|
+
you have a +unicorn+ symlink in the PATH to the appropriate +unicornXY+ executable.
|
|
20
18
|
|
|
21
19
|
It also assumes you have a SMTP server listening on localhost port 25 to
|
|
22
20
|
receive notification emails of worker crashes, if you are notifying for those.
|
|
@@ -33,14 +31,14 @@ the following as root after reading the file and understanding what it does.
|
|
|
33
31
|
Briefly, the configuration this uses the following directories:
|
|
34
32
|
|
|
35
33
|
/var/www/sockets :: Stores unix sockets that Unicorn listens on and Nginx uses.
|
|
36
|
-
/var/www/
|
|
37
|
-
|
|
34
|
+
/var/www/request-error-info :: Stores temporary files for each request with request info,
|
|
35
|
+
used for crash notifications
|
|
38
36
|
/var/log/unicorn :: Stores unicorn log files, one per application
|
|
39
37
|
/var/log/nginx :: Stores nginx log files, two per application, one for access
|
|
40
38
|
and one for errors
|
|
41
39
|
|
|
42
|
-
This adds a _unicorn group that all
|
|
43
|
-
|
|
40
|
+
This adds a _unicorn group that all application users will use as one of their
|
|
41
|
+
groups, as well as a /etc/rc.d/rc.unicorn file that the application
|
|
44
42
|
/etc/rc.d/unicorn_* files will use.
|
|
45
43
|
|
|
46
44
|
=== unicorn-lockdown-add
|
|
@@ -48,30 +46,38 @@ group, as well as a /etc/rc.d/rc.unicorn file that the per application
|
|
|
48
46
|
For each application you want to run with unicorn lockdown, run the following
|
|
49
47
|
as root, again after reading the file and understanding what it does:
|
|
50
48
|
|
|
51
|
-
unicorn-lockdown-add
|
|
49
|
+
unicorn-lockdown-add -o $owner -u $user $app_name
|
|
52
50
|
|
|
53
|
-
Here's
|
|
51
|
+
Here's the usage:
|
|
54
52
|
|
|
55
|
-
Usage: unicorn-lockdown-add [options] app_name
|
|
53
|
+
Usage: unicorn-lockdown-add -o owner -u user [options] app_name
|
|
56
54
|
Options:
|
|
57
|
-
-c
|
|
58
|
-
-d
|
|
59
|
-
-f
|
|
60
|
-
-o
|
|
61
|
-
-u
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
55
|
+
-c RACKUP_FILE rackup configuration file
|
|
56
|
+
-d DIR application directory name
|
|
57
|
+
-f UNICORN_FILE unicorn configuration file relative to application directory
|
|
58
|
+
-o OWNER operating system application owner
|
|
59
|
+
-u USER operating system user to run application
|
|
60
|
+
--uid UID user id to use if creating the user when -U is specified
|
|
61
|
+
-h, -?, --help Show this message
|
|
62
|
+
|
|
63
|
+
The <tt>-o</tt> and <tt>-u</tt> options are required. Default values for other options are:
|
|
64
|
+
|
|
65
|
+
<tt>-c</tt> :: None, Unicorn will use <tt>config.ru</tt> by default.
|
|
66
|
+
<tt>-d</tt> :: Same as +app_name+. The value provided should a relative path under <tt>/var/www</tt>.
|
|
67
|
+
<tt>-f</tt> :: <tt>unicorn.conf</tt>. This file should be relative to +dir+.
|
|
68
|
+
<tt>--uid</tt> :: The uid automatically generated by +useradd+.
|
|
69
|
+
|
|
70
|
+
The owner <tt>-o</tt> and the user <tt>-u</tt> should be different. The user is the user the
|
|
71
|
+
application runs as, and should have read-only access to the application directory,
|
|
72
|
+
other than locations where you want the application user to be able to modify files
|
|
73
|
+
at runtime. The owner is the user that owns the application directory and can make
|
|
74
|
+
modifications to the application.
|
|
69
75
|
|
|
70
76
|
=== unicorn-lockdown
|
|
71
77
|
|
|
72
78
|
unicorn-lockdown is the library required in your unicorn configuration
|
|
73
79
|
file for the application, to handle configuring unicorn to run the app
|
|
74
|
-
with
|
|
80
|
+
with fork+exec, unveil, and pledge.
|
|
75
81
|
|
|
76
82
|
When you run unicorn-lockdown-add, it will create the unicorn configuration
|
|
77
83
|
file for the app if one does not already exist, looking similar to:
|
|
@@ -80,37 +86,75 @@ file for the app if one does not already exist, looking similar to:
|
|
|
80
86
|
|
|
81
87
|
Unicorn.lockdown(self,
|
|
82
88
|
:app=>"app_name",
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
:email=>'root'
|
|
89
|
+
|
|
90
|
+
# Update this with correct email
|
|
91
|
+
:email=>'root',
|
|
92
|
+
|
|
93
|
+
# More pledges may be needed depending on application
|
|
94
|
+
:pledge=>'rpath prot_exec inet unix',
|
|
95
|
+
:master_pledge=>'rpath prot_exec cpath wpath inet proc exec',
|
|
96
|
+
:master_execpledge=>'stdio rpath prot_exec inet unix cpath wpath unveil',
|
|
97
|
+
|
|
98
|
+
# More unveils may be needed depending on application
|
|
99
|
+
:unveil=>{
|
|
100
|
+
'views'=>'r'
|
|
101
|
+
},
|
|
102
|
+
:dev_unveil=>{
|
|
103
|
+
'models'=>'r'
|
|
104
|
+
},
|
|
86
105
|
)
|
|
87
106
|
|
|
88
107
|
Unicorn.lockdown options:
|
|
89
108
|
|
|
90
109
|
:app :: (required) a short string for the name of the application, used
|
|
91
110
|
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
111
|
:email :: (optional) an email address to use for notifications when the
|
|
96
112
|
worker process crashes or an unhandled exception is raised by
|
|
97
113
|
the application or middleware.
|
|
114
|
+
:pledge :: (optional) a pledge string to limit the allowed system calls
|
|
115
|
+
after privileges have been dropped
|
|
116
|
+
:master_pledge :: (optional) The string to use when pledging the master process before
|
|
117
|
+
spawning worker processes
|
|
118
|
+
:master_execpledge :: (optional) The pledge string for processes spawned by the master
|
|
119
|
+
process (i.e. worker processes before loading the app)
|
|
120
|
+
:unveil :: (required) a hash of paths to limit file system access, passed
|
|
121
|
+
to +Pledge.unveil+.
|
|
122
|
+
:dev_unveil :: (optional) a hash of paths to limit file system, merged into the :unveil
|
|
123
|
+
option paths if in the development environment. Useful if you are
|
|
124
|
+
allowing more access in development, such as access needed
|
|
125
|
+
for file reloading.
|
|
98
126
|
|
|
99
127
|
With this example pledge:
|
|
100
128
|
|
|
101
|
-
* rpath is needed to read files
|
|
129
|
+
* rpath is needed to read files
|
|
102
130
|
* prot_exec is needed in most cases
|
|
103
131
|
* unix is needed for the unix socket to nginx
|
|
104
|
-
* inet is not needed in all cases, but
|
|
105
|
-
of network access
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
132
|
+
* inet is not needed in all cases, but most applications need some form
|
|
133
|
+
of network access, and it is needed by default for emailing about
|
|
134
|
+
exceptions that occur without process crashes. pf (OpenBSD's firewall)
|
|
135
|
+
should be used to limit access for the application's operating system
|
|
136
|
+
user to the minimum necessary access needed.
|
|
137
|
+
|
|
138
|
+
With this example master pledge:
|
|
139
|
+
|
|
140
|
+
* rpath is needed to read files
|
|
141
|
+
* prot_exec is needed in most cases
|
|
142
|
+
* cpath and wpath are needed to unlink the request error files
|
|
143
|
+
* inet is needed to send emails for worker crashes
|
|
144
|
+
* proc and exec are needed to spawn worker processes
|
|
145
|
+
|
|
146
|
+
With this examle master exec pledge:
|
|
147
|
+
|
|
148
|
+
* stdio must be added because ruby-pledge doesn't add it automatically
|
|
149
|
+
to execpromises, and Ruby requires it
|
|
150
|
+
* rpath, prot_exec, unix, inet are needed for the worker (see above)
|
|
151
|
+
* cpath and wpath are needed to create the request error files
|
|
152
|
+
* unveil is needed to restrict file system access
|
|
153
|
+
|
|
154
|
+
unicorn-lockdown has specific support for allowing emails to be sent
|
|
155
|
+
for Unicorn worker crashes (e.g. pledge violations) and unhandled
|
|
156
|
+
application exceptions (e.g. pledge violations). Additionally,
|
|
157
|
+
unicorn-lockdown modifies unicorn's process status line in a
|
|
114
158
|
way that allows it to be controllable via OpenBSD's rcctl program for
|
|
115
159
|
stopping/starting/reloading/restarting daemons.
|
|
116
160
|
|
|
@@ -119,7 +163,12 @@ with the expectation of an Nginx limit of 10MB, such that all client
|
|
|
119
163
|
requests will be buffered in memory and unicorn will not need to write
|
|
120
164
|
temporary files to disk. If this limit is not correct for your
|
|
121
165
|
application, please call client_body_buffer_size after calling
|
|
122
|
-
Unicorn.lockdown to set an appropriate limit.
|
|
166
|
+
Unicorn.lockdown to set an appropriate limit. Note that rack still
|
|
167
|
+
creates temporary files for file uploads by default, you'll need to
|
|
168
|
+
configure rack to disallow file uploads if your application does not
|
|
169
|
+
need to accept uploaded files and you don't want file upload attempts
|
|
170
|
+
to cause pledge violations. With Roda, you can use the
|
|
171
|
+
disallow_file_uploads plugin to prevent file upload attempts.
|
|
123
172
|
|
|
124
173
|
When Unicorn.lockdown is used with the :email option, if the worker
|
|
125
174
|
process crashes, it will email the address using the contents specified
|
|
@@ -138,6 +187,8 @@ and then at the top of the route block, do:
|
|
|
138
187
|
|
|
139
188
|
if defined?(Unicorn) && Unicorn.respond_to?(:write_request)
|
|
140
189
|
Unicorn.write_request(error_email_content("Unicorn Worker Process Crash"))
|
|
190
|
+
# or
|
|
191
|
+
Unicorn.write_request(error_mail_content("Unicorn Worker Process Crash"))
|
|
141
192
|
end
|
|
142
193
|
|
|
143
194
|
If you don't have useful information in the request file, an email will
|
|
@@ -149,10 +200,20 @@ underlying problem.
|
|
|
149
200
|
|
|
150
201
|
If you are using PostgreSQL as the database for the application, and using
|
|
151
202
|
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
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
203
|
+
is restarted, the application will no longer be able to connect to it unless
|
|
204
|
+
you unveil the path the database socket (stored in /tmp by default). It can
|
|
205
|
+
be a better approach security wise not to allow this, to prevent the
|
|
206
|
+
application from being able to establish new database connections with
|
|
207
|
+
potentially different credentials, as a mitigation in case the server is
|
|
208
|
+
compromised.
|
|
209
|
+
|
|
210
|
+
To allow the application to handle cases where the database is disconnected,
|
|
211
|
+
such as due to a restart of PostgreSQL, you can kill the worker process if
|
|
212
|
+
a disconnect error is detected, and have the master process then spawn a new
|
|
213
|
+
worker.
|
|
214
|
+
|
|
215
|
+
The roda-pg_disconnect plugin is a plugin for the roda web toolkit to kill the
|
|
216
|
+
worker process after handling the connection if it detects the database
|
|
156
217
|
connection has been lost. This plugin assumes the use of the Sequel database
|
|
157
218
|
library and postgres adapter with the pg driver.
|
|
158
219
|
|
|
@@ -161,51 +222,63 @@ In your Roda application:
|
|
|
161
222
|
# Sometime before loading the error_handler plugin
|
|
162
223
|
plugin :pg_disconnect
|
|
163
224
|
|
|
225
|
+
To specifically restrict access to the database socket even when access to
|
|
226
|
+
/tmp is allowed, you can unveil the database socket path with no permissions:
|
|
227
|
+
|
|
228
|
+
'/tmp/.s.PGSQL.5432'=>''
|
|
229
|
+
|
|
230
|
+
Note that there are potentially other security issues with unveiling access
|
|
231
|
+
to /tmp beyond granting access to the database server, so it is recommended
|
|
232
|
+
you do not unveil it. If the application needs a directory for temporary
|
|
233
|
+
files (e.g. for handling uploaded files with rack), you can set the +TMPDIR+
|
|
234
|
+
environment variable to an appropriate directory that is writable by the
|
|
235
|
+
application user and not other users, and most web applications will respect
|
|
236
|
+
that (assuming they use the tmpfile/tmpdir libraries in the standard library).
|
|
237
|
+
|
|
164
238
|
=== rack-email_exceptions
|
|
165
239
|
|
|
166
|
-
rack-email_exceptions is a rack middleware designed to
|
|
167
|
-
middleware
|
|
240
|
+
rack-email_exceptions is a rack middleware designed to wrap all other
|
|
241
|
+
middleware and the application. It rescues unhandled exceptions
|
|
168
242
|
raised by subsequent middleware or the application itself.
|
|
169
|
-
|
|
170
243
|
Unicorn.lockdown will automatically setup this middleware if the :email
|
|
171
|
-
option is used
|
|
172
|
-
such that it wraps the application and all other middleware.
|
|
244
|
+
option is used.
|
|
173
245
|
|
|
174
246
|
It is possible to use this middleware manually:
|
|
175
247
|
|
|
176
248
|
require 'rack/email_exceptions'
|
|
177
249
|
use Rack::EmailExceptions, "app_name", 'foo@example.com'
|
|
178
250
|
|
|
179
|
-
===
|
|
251
|
+
=== unveiler
|
|
180
252
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
the application into production, such as a file being read from outside
|
|
186
|
-
the chroot.
|
|
253
|
+
unveiler is a library designed to help with testing applications that
|
|
254
|
+
use pledge and unveil. If you are running your application pledged and
|
|
255
|
+
unveiled, you want your tests to run pledged and unveiled to find
|
|
256
|
+
problems.
|
|
187
257
|
|
|
188
|
-
|
|
258
|
+
unveiler assumes you are using minitest for testing. To use unveiler:
|
|
189
259
|
|
|
190
260
|
require 'minitest/autorun'
|
|
191
|
-
require '
|
|
192
|
-
at_exit
|
|
193
|
-
|
|
194
|
-
|
|
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.
|
|
261
|
+
require 'unveiler'
|
|
262
|
+
at_exit do
|
|
263
|
+
Unveiler.pledge_and_unveil('rpath prot_exec inet unix', 'views' => 'r')
|
|
264
|
+
end
|
|
200
265
|
|
|
201
266
|
== autoload
|
|
202
267
|
|
|
203
|
-
As you'll find out if you try to run your applications with
|
|
204
|
-
autoload
|
|
205
|
-
|
|
206
|
-
If you use other gems that use
|
|
207
|
-
|
|
208
|
-
|
|
268
|
+
As you'll find out if you try to run your applications with unveil,
|
|
269
|
+
autoload and other forms of runtime requires are the enemy. Both
|
|
270
|
+
unicorn-lockdown and unveiler have support for handling common autoloaded
|
|
271
|
+
constants in the rack and mail gems. If you use other gems that use
|
|
272
|
+
autoload or runtime requires, you'll have to add unveils for the appropriate
|
|
273
|
+
gems:
|
|
274
|
+
|
|
275
|
+
Unicorn.lockdown(self,
|
|
276
|
+
# ...
|
|
277
|
+
:unveil=>{
|
|
278
|
+
'views' => 'r',
|
|
279
|
+
'gem-name' => :gem,
|
|
280
|
+
}
|
|
281
|
+
)
|
|
209
282
|
|
|
210
283
|
== Author
|
|
211
284
|
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require 'etc'
|
|
4
|
+
require 'optparse'
|
|
5
|
+
|
|
6
|
+
def sh(*args)
|
|
7
|
+
puts "Running: #{args.join(' ')}"
|
|
8
|
+
system(*args) || raise("Error while running: #{args.join(' ')}")
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
unicorn = ''
|
|
12
|
+
rackup = ''
|
|
13
|
+
unicorn_file = 'unicorn.conf'
|
|
14
|
+
dir = nil
|
|
15
|
+
user = nil
|
|
16
|
+
new_user_uid = nil
|
|
17
|
+
owner = nil
|
|
18
|
+
owner_uid = nil
|
|
19
|
+
owner_gid = nil
|
|
20
|
+
|
|
21
|
+
options = OptionParser.new do |opts|
|
|
22
|
+
opts.banner = "Usage: unicorn-lockdown-add -o owner -u user [options] app_name"
|
|
23
|
+
opts.separator "Options:"
|
|
24
|
+
|
|
25
|
+
opts.on_tail("-h", "-?", "--help", "Show this message") do
|
|
26
|
+
puts opts
|
|
27
|
+
exit
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
opts.on("-c RACKUP_FILE", "rackup configuration file") do |v|
|
|
31
|
+
rackup = "rackup_file=#{v}\n"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
opts.on("-d DIR", "application directory name") do |v|
|
|
35
|
+
dir = v
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
opts.on("-f UNICORN_FILE", "unicorn configuration file relative to application directory") do |v|
|
|
39
|
+
unicorn_file = v
|
|
40
|
+
unicorn = "unicorn_conf=#{v}\n"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
opts.on("-o OWNER", "operating system application owner") do |v|
|
|
44
|
+
owner = v
|
|
45
|
+
ent = Etc.getpwnam(v)
|
|
46
|
+
owner_uid = ent.uid
|
|
47
|
+
owner_gid = ent.gid
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
opts.on("-u USER", "operating system user to run application") do |v|
|
|
51
|
+
user = v
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
opts.on("--uid UID", "user id to use if creating the user when -U is specified") do |v|
|
|
55
|
+
new_user_uid = Integer(v, 10)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
options.parse!
|
|
59
|
+
|
|
60
|
+
unless user && owner
|
|
61
|
+
$stderr.puts "Must pass -o and -u options when calling unicorn_lockdown_add"
|
|
62
|
+
puts options
|
|
63
|
+
exit(1)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
app = ARGV.shift
|
|
67
|
+
dir ||= app
|
|
68
|
+
|
|
69
|
+
root_id = 0
|
|
70
|
+
bin_id = 7
|
|
71
|
+
www_id = 67
|
|
72
|
+
|
|
73
|
+
www_root = '/var/www'
|
|
74
|
+
dir = "#{www_root}/#{dir}"
|
|
75
|
+
rc_file = "/etc/rc.d/unicorn_#{app.tr('-', '_')}"
|
|
76
|
+
nginx_file = "/etc/nginx/#{app}.conf"
|
|
77
|
+
unicorn_conf_file = "#{dir}/#{unicorn_file}"
|
|
78
|
+
nonroot_dir = "/var/www/request-error-data/#{app}"
|
|
79
|
+
unicorn_log_file = "/var/log/unicorn/#{app}.log"
|
|
80
|
+
nginx_access_log_file = "/var/log/nginx/#{app}.access.log"
|
|
81
|
+
nginx_error_log_file = "/var/log/nginx/#{app}.error.log"
|
|
82
|
+
|
|
83
|
+
# Add application user if it doesn't exist
|
|
84
|
+
passwd = begin
|
|
85
|
+
Etc.getpwnam(user)
|
|
86
|
+
rescue ArgumentError
|
|
87
|
+
args = ['/usr/sbin/useradd', '-d', '/var/empty', '-g', '=uid', '-G', '_unicorn', '-L', 'daemon', '-s', '/sbin/nologin']
|
|
88
|
+
if new_user_uid
|
|
89
|
+
args << '-u' << new_user_uid.to_s
|
|
90
|
+
end
|
|
91
|
+
args << user
|
|
92
|
+
sh(*args)
|
|
93
|
+
Etc.getpwnam(user)
|
|
94
|
+
end
|
|
95
|
+
app_uid = passwd.uid
|
|
96
|
+
|
|
97
|
+
# Create the subdirectory used for request error info when not running as root
|
|
98
|
+
unless File.directory?(nonroot_dir)
|
|
99
|
+
puts "Creating #{nonroot_dir}"
|
|
100
|
+
Dir.mkdir(nonroot_dir)
|
|
101
|
+
File.chmod(0700, nonroot_dir)
|
|
102
|
+
File.chown(app_uid, app_uid, nonroot_dir)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Create application public directory if it doesn't exist (needed by nginx)
|
|
106
|
+
dirs = [dir, "#{dir}/public"]
|
|
107
|
+
dirs.each do |d|
|
|
108
|
+
unless File.directory?(d)
|
|
109
|
+
puts "Creating #{d}"
|
|
110
|
+
Dir.mkdir(d)
|
|
111
|
+
File.chmod(0755, d)
|
|
112
|
+
File.chown(owner_uid, owner_gid, d)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# DRY up file ownership code
|
|
117
|
+
setup_file_owner = lambda do |file|
|
|
118
|
+
File.chmod(0644, file)
|
|
119
|
+
File.chown(owner_uid, owner_gid, file)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Setup unicorn configuration file
|
|
123
|
+
unless File.file?(unicorn_conf_file)
|
|
124
|
+
unicorn_conf_dir = File.dirname(unicorn_conf_file)
|
|
125
|
+
unless File.directory?(unicorn_conf_dir)
|
|
126
|
+
puts "Creating #{unicorn_conf_dir}"
|
|
127
|
+
Dir.mkdir(unicorn_conf_dir)
|
|
128
|
+
File.chmod(0755, unicorn_conf_dir)
|
|
129
|
+
File.chown(owner_uid, owner_gid, unicorn_conf_dir)
|
|
130
|
+
end
|
|
131
|
+
puts "Creating #{unicorn_conf_file}"
|
|
132
|
+
File.binwrite(unicorn_conf_file, <<END)
|
|
133
|
+
require 'unicorn-lockdown'
|
|
134
|
+
|
|
135
|
+
Unicorn.lockdown(self,
|
|
136
|
+
:app=>#{app.inspect},
|
|
137
|
+
|
|
138
|
+
# Update this with correct email
|
|
139
|
+
:email=>'root',
|
|
140
|
+
|
|
141
|
+
# More pledges may be needed depending on application
|
|
142
|
+
:pledge=>'rpath prot_exec inet unix',
|
|
143
|
+
:master_pledge=>'rpath prot_exec cpath wpath inet proc exec',
|
|
144
|
+
:master_execpledge=>'stdio rpath prot_exec inet unix cpath wpath unveil',
|
|
145
|
+
|
|
146
|
+
# More unveils may be needed depending on application
|
|
147
|
+
:unveil=>{
|
|
148
|
+
'views'=>'r'
|
|
149
|
+
},
|
|
150
|
+
:dev_unveil=>{
|
|
151
|
+
'models'=>'r'
|
|
152
|
+
},
|
|
153
|
+
)
|
|
154
|
+
END
|
|
155
|
+
setup_file_owner.call(unicorn_conf_file)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Setup /etc/nginx/* file for nginx configuration
|
|
159
|
+
unless File.file?(nginx_file)
|
|
160
|
+
puts "Creating #{nginx_file}"
|
|
161
|
+
File.binwrite(nginx_file, <<END)
|
|
162
|
+
upstream #{app}_unicorn {
|
|
163
|
+
server unix:/sockets/#{app}.sock fail_timeout=0;
|
|
164
|
+
}
|
|
165
|
+
server {
|
|
166
|
+
server_name #{app};
|
|
167
|
+
access_log #{nginx_access_log_file} main;
|
|
168
|
+
error_log #{nginx_error_log_file} warn;
|
|
169
|
+
root #{dir}/public;
|
|
170
|
+
error_page 500 503 /500.html;
|
|
171
|
+
error_page 502 504 /502.html;
|
|
172
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
173
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
174
|
+
proxy_set_header Host $http_host;
|
|
175
|
+
proxy_redirect off;
|
|
176
|
+
add_header X-Content-Type-Options nosniff;
|
|
177
|
+
add_header X-Frame-Options deny;
|
|
178
|
+
add_header X-XSS-Protection "1; mode=block";
|
|
179
|
+
try_files $uri @#{app}_unicorn;
|
|
180
|
+
location @#{app}_unicorn {
|
|
181
|
+
proxy_pass http://#{app}_unicorn;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
END
|
|
185
|
+
|
|
186
|
+
setup_file_owner.call(nginx_file)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Setup nginx log file
|
|
190
|
+
[nginx_access_log_file, nginx_error_log_file].each do |f|
|
|
191
|
+
unless File.file?(f)
|
|
192
|
+
puts "Creating #{f}"
|
|
193
|
+
File.binwrite(f, '')
|
|
194
|
+
File.chmod(0644, f)
|
|
195
|
+
File.chown(www_id, root_id, f)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Setup unicorn log file
|
|
200
|
+
unless File.file?(unicorn_log_file)
|
|
201
|
+
puts "Creating #{unicorn_log_file}"
|
|
202
|
+
File.binwrite(unicorn_log_file, '')
|
|
203
|
+
File.chmod(0640, unicorn_log_file)
|
|
204
|
+
File.chown(app_uid, Etc.getgrnam('_unicorn').gid, unicorn_log_file) if app_uid
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Setup /etc/rc.d/unicorn_* file for daemon management
|
|
208
|
+
unless File.file?(rc_file)
|
|
209
|
+
puts "Creating #{rc_file}"
|
|
210
|
+
File.binwrite(rc_file, <<END)
|
|
211
|
+
#!/bin/ksh
|
|
212
|
+
|
|
213
|
+
daemon_user=#{user}
|
|
214
|
+
unicorn_app=#{app}
|
|
215
|
+
unicorn_dir=#{dir}
|
|
216
|
+
#{unicorn}#{rackup}
|
|
217
|
+
. /etc/rc.d/rc.unicorn
|
|
218
|
+
END
|
|
219
|
+
|
|
220
|
+
File.chmod(0755, rc_file)
|
|
221
|
+
File.chown(root_id, bin_id, rc_file)
|
|
222
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require 'etc'
|
|
4
|
+
|
|
5
|
+
def sh(*args)
|
|
6
|
+
puts "Running: #{args.join(' ')}"
|
|
7
|
+
system(*args) || raise("Error while running: #{args.join(' ')}")
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
request_dir = '/var/www/request-error-data'
|
|
11
|
+
socket_dir = '/var/www/sockets'
|
|
12
|
+
unicorn_log_dir = '/var/log/unicorn'
|
|
13
|
+
nginx_log_dir = "/var/log/nginx"
|
|
14
|
+
rc_unicorn_file = '/etc/rc.d/rc.unicorn'
|
|
15
|
+
|
|
16
|
+
unicorn_group = '_unicorn'
|
|
17
|
+
root_id = 0
|
|
18
|
+
daemon_id = 1
|
|
19
|
+
www_id = 67
|
|
20
|
+
|
|
21
|
+
# Add _unicorn group if it doesn't exist
|
|
22
|
+
group = begin
|
|
23
|
+
Etc.getgrnam(unicorn_group)
|
|
24
|
+
rescue ArgumentError
|
|
25
|
+
sh('groupadd', unicorn_group)
|
|
26
|
+
Etc.getgrnam(unicorn_group)
|
|
27
|
+
end
|
|
28
|
+
unicorn_group_id = group.gid
|
|
29
|
+
|
|
30
|
+
# Setup requests directory to hold per-request information for crash notifications
|
|
31
|
+
unless File.directory?(request_dir)
|
|
32
|
+
puts "Creating #{request_dir}"
|
|
33
|
+
Dir.mkdir(request_dir)
|
|
34
|
+
File.chmod(0710, request_dir)
|
|
35
|
+
File.chown(root_id, unicorn_group_id, request_dir)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Setup sockets directory to hold nginx connection sockets
|
|
39
|
+
unless File.directory?(socket_dir)
|
|
40
|
+
puts "Creating #{socket_dir}"
|
|
41
|
+
Dir.mkdir(socket_dir)
|
|
42
|
+
File.chmod(0770, socket_dir)
|
|
43
|
+
File.chown(www_id, unicorn_group_id, socket_dir)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Setup log directory to hold unicorn application logs
|
|
47
|
+
unless File.directory?(unicorn_log_dir)
|
|
48
|
+
puts "Creating #{unicorn_log_dir}"
|
|
49
|
+
Dir.mkdir(unicorn_log_dir)
|
|
50
|
+
File.chmod(0755, unicorn_log_dir)
|
|
51
|
+
File.chown(root_id, daemon_id, unicorn_log_dir)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Setup log directory to hold nginx logs
|
|
55
|
+
unless File.directory?(nginx_log_dir)
|
|
56
|
+
puts "Creating #{nginx_log_dir}"
|
|
57
|
+
Dir.mkdir(nginx_log_dir)
|
|
58
|
+
File.chmod(0775, nginx_log_dir)
|
|
59
|
+
File.chown(www_id, root_id, nginx_log_dir)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Setup rc.unicorn file
|
|
63
|
+
unless File.file?(rc_unicorn_file)
|
|
64
|
+
puts "Creating #{rc_unicorn_file}"
|
|
65
|
+
File.binwrite(rc_unicorn_file, File.binread(File.join(File.dirname(__dir__), 'files', 'rc.unicorn')))
|
|
66
|
+
File.chmod(0644, rc_unicorn_file)
|
|
67
|
+
File.chown(root_id, root_id, rc_unicorn_file)
|
|
68
|
+
end
|
|
@@ -8,10 +8,9 @@ class Roda
|
|
|
8
8
|
# the QUIT signal, allowing Unicorn to finish handling the current request before exiting.
|
|
9
9
|
#
|
|
10
10
|
# This is designed to be used with applications that cannot connect to the database
|
|
11
|
-
# after application initialization, either because they are
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# privileges are dropped.
|
|
11
|
+
# after application initialization, either because they are restricting access to the database
|
|
12
|
+
# socket using unveil, or because they are using a firewall and access to the database server is
|
|
13
|
+
# not allowed from the application the process is running as after privileges are dropped.
|
|
15
14
|
#
|
|
16
15
|
# This plugin must be loaded before the roda error_handler plugin, and it assumes usage of the
|
|
17
16
|
# Sequel database library with the postgres adapter and pg driver.
|
|
@@ -23,13 +22,23 @@ class Roda
|
|
|
23
22
|
module InstanceMethods
|
|
24
23
|
# When database connection is lost, kill the worker process, so a new one will be generated.
|
|
25
24
|
# This is necessary because the unix socket used by the database connection is no longer available
|
|
26
|
-
# once the application is
|
|
25
|
+
# once the application is unveiled or pledged.
|
|
27
26
|
def call
|
|
28
27
|
super
|
|
29
28
|
rescue Sequel::DatabaseDisconnectError, Sequel::DatabaseConnectionError, PG::ConnectionBad
|
|
30
29
|
Process.kill(:QUIT, $$)
|
|
31
30
|
raise
|
|
32
31
|
end
|
|
32
|
+
|
|
33
|
+
# When database connection is lost, kill the worker process, so a new one will be generated.
|
|
34
|
+
# This is necessary because the unix socket used by the database connection is no longer available
|
|
35
|
+
# once the application is unveiled or pledged.
|
|
36
|
+
def _roda_handle_main_route
|
|
37
|
+
super
|
|
38
|
+
rescue Sequel::DatabaseDisconnectError, Sequel::DatabaseConnectionError, PG::ConnectionBad
|
|
39
|
+
Process.kill(:QUIT, $$)
|
|
40
|
+
raise
|
|
41
|
+
end
|
|
33
42
|
end
|
|
34
43
|
end
|
|
35
44
|
|
data/lib/unicorn-lockdown.rb
CHANGED
|
@@ -1,25 +1,38 @@
|
|
|
1
|
-
# unicorn-lockdown is designed to
|
|
2
|
-
#
|
|
1
|
+
# unicorn-lockdown is designed to handle fork+exec, unveil, and pledge support
|
|
2
|
+
# when using Unicorn, including:
|
|
3
|
+
# * restricting file system access using unveil
|
|
3
4
|
# * pledging the app to restrict allowed syscalls at the appropriate point
|
|
4
|
-
# * handling notifications of worker crashes
|
|
5
|
-
#
|
|
6
|
-
# * stripping path prefixes from the reloader in development mode
|
|
5
|
+
# * handling notifications of worker crashes (which are likely due to pledge
|
|
6
|
+
# violations)
|
|
7
7
|
|
|
8
8
|
require 'pledge'
|
|
9
|
+
require 'unveil'
|
|
9
10
|
|
|
10
|
-
#
|
|
11
|
+
# Load common encodings
|
|
11
12
|
"\255".force_encoding('ISO8859-1').encode('UTF-8')
|
|
12
|
-
|
|
13
|
-
# Load encodings
|
|
14
13
|
''.force_encoding('UTF-16LE')
|
|
15
14
|
''.force_encoding('UTF-16BE')
|
|
16
15
|
|
|
17
16
|
class Unicorn::HttpServer
|
|
18
|
-
# The file name in which to store request information.
|
|
19
|
-
# /var/www/
|
|
20
|
-
# to
|
|
17
|
+
# The file name in which to store request information.
|
|
18
|
+
# The /var/www/request-error-data/$app_name folder is accessable
|
|
19
|
+
# only to the user of the application.
|
|
21
20
|
def request_filename(pid)
|
|
22
|
-
"/var/www/
|
|
21
|
+
"/var/www/request-error-data/#{Unicorn.app_name}/#{pid}.txt"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
unless ENV['UNICORN_WORKER']
|
|
25
|
+
alias _original_spawn_missing_workers spawn_missing_workers
|
|
26
|
+
|
|
27
|
+
# This is the master process, set the master pledge before spawning
|
|
28
|
+
# workers, because spawning workers will also need to be done at runtime.
|
|
29
|
+
def spawn_missing_workers
|
|
30
|
+
if pledge = Unicorn.master_pledge
|
|
31
|
+
Unicorn.master_pledge = nil
|
|
32
|
+
Pledge.pledge(pledge, Unicorn.master_execpledge)
|
|
33
|
+
end
|
|
34
|
+
_original_spawn_missing_workers
|
|
35
|
+
end
|
|
23
36
|
end
|
|
24
37
|
|
|
25
38
|
# Override the process name for the unicorn processes, both master and
|
|
@@ -49,19 +62,28 @@ class << Unicorn
|
|
|
49
62
|
# to enable programmers to debug and fix the issue.
|
|
50
63
|
attr_accessor :request_logger
|
|
51
64
|
|
|
52
|
-
# The
|
|
53
|
-
attr_accessor :
|
|
65
|
+
# The pledge string to use for the master process's spawned processes by default.
|
|
66
|
+
attr_accessor :master_execpledge
|
|
54
67
|
|
|
55
|
-
# The pledge string to use.
|
|
68
|
+
# The pledge string to use for the master process.
|
|
69
|
+
attr_accessor :master_pledge
|
|
70
|
+
|
|
71
|
+
# The pledge string to use for worker processes.
|
|
56
72
|
attr_accessor :pledge
|
|
57
73
|
|
|
58
|
-
# The
|
|
74
|
+
# The hash of unveil paths to use.
|
|
75
|
+
attr_accessor :unveil
|
|
76
|
+
|
|
77
|
+
# The hash of additional unveil paths to use if in the development environment.
|
|
78
|
+
attr_accessor :dev_unveil
|
|
79
|
+
|
|
80
|
+
# The address to email for crash and unhandled exception notifications.
|
|
59
81
|
attr_accessor :email
|
|
60
82
|
|
|
61
83
|
# Helper method to write request information to the request logger.
|
|
62
84
|
# +email_message+ should be an email message including headers and body.
|
|
63
85
|
# This should be called at the top of the Roda route block for the
|
|
64
|
-
# application.
|
|
86
|
+
# application (or at some early point before processing in other web frameworks).
|
|
65
87
|
def write_request(email_message)
|
|
66
88
|
request_logger.seek(0, IO::SEEK_SET)
|
|
67
89
|
request_logger.truncate(0)
|
|
@@ -69,21 +91,30 @@ class << Unicorn
|
|
|
69
91
|
request_logger.fsync
|
|
70
92
|
end
|
|
71
93
|
|
|
72
|
-
# Helper method that sets up all necessary code for
|
|
94
|
+
# Helper method that sets up all necessary code for unveil/pledge support.
|
|
73
95
|
# This should be called inside the appropriate unicorn.conf file.
|
|
74
96
|
# The configurator should be self in the top level scope of the
|
|
75
97
|
# unicorn.conf file, and this takes options:
|
|
76
98
|
#
|
|
77
99
|
# Options:
|
|
78
|
-
# :app :: The name of the application
|
|
100
|
+
# :app (required) :: The name of the application
|
|
79
101
|
# :email : The email to notify for worker crashes
|
|
80
|
-
# :
|
|
81
|
-
# :
|
|
102
|
+
# :pledge :: The string to use when pledging worker processes after loading the app
|
|
103
|
+
# :master_pledge :: The string to use when pledging the master process before
|
|
104
|
+
# spawning worker processes
|
|
105
|
+
# :master_execpledge :: The pledge string for processes spawned by the master
|
|
106
|
+
# process (i.e. worker processes before loading the app)
|
|
107
|
+
# :unveil :: A hash of unveil paths, passed to Pledge.unveil.
|
|
108
|
+
# :dev_unveil :: A hash of unveil paths to use in development, in addition
|
|
109
|
+
# to the ones in :unveil.
|
|
82
110
|
def lockdown(configurator, opts)
|
|
83
111
|
Unicorn.app_name = opts.fetch(:app)
|
|
84
|
-
Unicorn.user_name = opts.fetch(:user)
|
|
85
112
|
Unicorn.email = opts[:email]
|
|
113
|
+
Unicorn.master_pledge = opts[:master_pledge]
|
|
114
|
+
Unicorn.master_execpledge = opts[:master_execpledge]
|
|
86
115
|
Unicorn.pledge = opts[:pledge]
|
|
116
|
+
Unicorn.unveil = opts[:unveil]
|
|
117
|
+
Unicorn.dev_unveil = opts[:dev_unveil]
|
|
87
118
|
|
|
88
119
|
configurator.instance_exec do
|
|
89
120
|
listen "/var/www/sockets/#{Unicorn.app_name}.sock"
|
|
@@ -107,13 +138,12 @@ class << Unicorn
|
|
|
107
138
|
after_fork do |server, worker|
|
|
108
139
|
server.logger.info("worker=#{worker.nr} spawned pid=#{$$}")
|
|
109
140
|
|
|
110
|
-
# Set the request logger for the worker process after forking.
|
|
111
|
-
# process is still root here, so it can open the file in write mode.
|
|
141
|
+
# Set the request logger for the worker process after forking.
|
|
112
142
|
Unicorn.request_logger = File.open(server.request_filename($$), "wb")
|
|
113
143
|
Unicorn.request_logger.sync = true
|
|
114
144
|
end
|
|
115
145
|
|
|
116
|
-
if wrap_app = Unicorn.email
|
|
146
|
+
if wrap_app = Unicorn.email
|
|
117
147
|
require 'rack/email_exceptions'
|
|
118
148
|
end
|
|
119
149
|
|
|
@@ -129,54 +159,31 @@ class << Unicorn
|
|
|
129
159
|
end
|
|
130
160
|
end
|
|
131
161
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
162
|
+
unveil = if Unicorn.dev_unveil && ENV['RACK_ENV'] == 'development'
|
|
163
|
+
Unicorn.unveil.merge(Unicorn.dev_unveil)
|
|
164
|
+
else
|
|
165
|
+
Hash[Unicorn.unveil]
|
|
164
166
|
end
|
|
165
167
|
|
|
166
|
-
#
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
# chrooting requires root privileges.
|
|
173
|
-
worker.user(Unicorn.user_name, Unicorn.user_name, pwd)
|
|
168
|
+
# Don't allow loading files in rack and mail gems if not using rubygems
|
|
169
|
+
if defined?(Gem) && Gem.respond_to?(:loaded_specs)
|
|
170
|
+
# Allow read access to the rack gem directory, as rack autoloads constants.
|
|
171
|
+
if defined?(Rack) && Gem.loaded_specs['rack']
|
|
172
|
+
unveil['rack'] = :gem
|
|
173
|
+
end
|
|
174
174
|
|
|
175
|
-
|
|
176
|
-
#
|
|
177
|
-
|
|
178
|
-
|
|
175
|
+
# If using the mail library, allow read access to the mail gem directory,
|
|
176
|
+
# as mail autoloads constants.
|
|
177
|
+
if defined?(Mail) && Gem.loaded_specs['mail']
|
|
178
|
+
unveil['mail'] = :gem
|
|
179
|
+
end
|
|
179
180
|
end
|
|
181
|
+
|
|
182
|
+
# Restrict access to the file system based on the specified unveil.
|
|
183
|
+
Pledge.unveil(unveil)
|
|
184
|
+
|
|
185
|
+
# Pledge after unveiling, because unveiling requires a separate pledge.
|
|
186
|
+
Pledge.pledge(Unicorn.pledge)
|
|
180
187
|
end
|
|
181
188
|
|
|
182
189
|
# the last time there was a worker crash and the request information
|
|
@@ -219,28 +226,13 @@ class << Unicorn
|
|
|
219
226
|
# If the request filename exists and the worker process crashed,
|
|
220
227
|
# send a notification email.
|
|
221
228
|
Process.waitpid(fork do
|
|
222
|
-
# Load net/smtp early
|
|
229
|
+
# Load net/smtp early
|
|
223
230
|
require 'net/smtp'
|
|
224
231
|
|
|
225
232
|
# When setting the email, first get the contents of the email
|
|
226
233
|
# from the request file.
|
|
227
234
|
body = File.read(file)
|
|
228
235
|
|
|
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
236
|
# Then use a restrictive pledge
|
|
245
237
|
Pledge.pledge('inet prot_exec')
|
|
246
238
|
|
|
@@ -263,4 +255,3 @@ class << Unicorn
|
|
|
263
255
|
end
|
|
264
256
|
end
|
|
265
257
|
end
|
|
266
|
-
|
data/lib/unveiler.rb
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
require 'pledge'
|
|
2
|
+
require 'unveil'
|
|
3
|
+
|
|
4
|
+
# Load encodings
|
|
5
|
+
"\255".force_encoding('ISO8859-1').encode('UTF-8')
|
|
6
|
+
''.force_encoding('UTF-16LE')
|
|
7
|
+
''.force_encoding('UTF-16BE')
|
|
8
|
+
|
|
9
|
+
# Don't run external diff program for failures
|
|
10
|
+
Minitest::Assertions.diff = false if defined?(Minitest::Assertions)
|
|
11
|
+
|
|
12
|
+
# Unveiler allows for testing programs using pledge and unveil.
|
|
13
|
+
module Unveiler
|
|
14
|
+
# Use Pledge.unveil to limit access to the file system based on the
|
|
15
|
+
# +unveil+ argument. Then pledge the process with the given +pledge+
|
|
16
|
+
# permissions. This will automatically unveil the rack and mail gems
|
|
17
|
+
# if they are loaded.
|
|
18
|
+
def self.pledge_and_unveil(pledge, unveil)
|
|
19
|
+
unveil = Hash[unveil]
|
|
20
|
+
|
|
21
|
+
if defined?(Gem) && Gem.respond_to?(:loaded_specs)
|
|
22
|
+
if defined?(Rack) && Gem.loaded_specs['rack']
|
|
23
|
+
unveil['rack'] = :gem
|
|
24
|
+
end
|
|
25
|
+
if defined?(Mail) && Gem.loaded_specs['mail']
|
|
26
|
+
unveil['mail'] = :gem
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
Pledge.unveil(unveil)
|
|
31
|
+
|
|
32
|
+
unless defined?(SimpleCov)
|
|
33
|
+
# If running coverage tests, don't run pledged as coverage
|
|
34
|
+
# testing can require many additional permissions.
|
|
35
|
+
Pledge.pledge(pledge)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: unicorn-lockdown
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jeremy Evans
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2020-11-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: pledge
|
|
@@ -40,17 +40,22 @@ dependencies:
|
|
|
40
40
|
version: '0'
|
|
41
41
|
description:
|
|
42
42
|
email: code@jeremyevans.net
|
|
43
|
-
executables:
|
|
43
|
+
executables:
|
|
44
|
+
- unicorn-lockdown-add
|
|
45
|
+
- unicorn-lockdown-setup
|
|
44
46
|
extensions: []
|
|
45
47
|
extra_rdoc_files: []
|
|
46
48
|
files:
|
|
49
|
+
- CHANGELOG
|
|
47
50
|
- MIT-LICENSE
|
|
48
51
|
- README.rdoc
|
|
52
|
+
- bin/unicorn-lockdown-add
|
|
53
|
+
- bin/unicorn-lockdown-setup
|
|
49
54
|
- files/rc.unicorn
|
|
50
|
-
- lib/chrooter.rb
|
|
51
55
|
- lib/rack/email_exceptions.rb
|
|
52
56
|
- lib/roda/plugins/pg_disconnect.rb
|
|
53
57
|
- lib/unicorn-lockdown.rb
|
|
58
|
+
- lib/unveiler.rb
|
|
54
59
|
homepage: https://github.com/jeremyevans/unicorn-lockdown
|
|
55
60
|
licenses:
|
|
56
61
|
- MIT
|
|
@@ -70,10 +75,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
70
75
|
- !ruby/object:Gem::Version
|
|
71
76
|
version: '0'
|
|
72
77
|
requirements: []
|
|
73
|
-
|
|
74
|
-
rubygems_version: 2.7.6
|
|
78
|
+
rubygems_version: 3.1.4
|
|
75
79
|
signing_key:
|
|
76
80
|
specification_version: 4
|
|
77
|
-
summary: Helper library for running Unicorn with
|
|
78
|
-
on OpenBSD
|
|
81
|
+
summary: Helper library for running Unicorn with fork+exec/unveil/pledge on OpenBSD
|
|
79
82
|
test_files: []
|
data/lib/chrooter.rb
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
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
|