unicorn-lockdown 0.9.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|