unicorn-lockdown 0.13.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 +14 -0
- data/MIT-LICENSE +1 -1
- data/README.rdoc +147 -123
- data/bin/unicorn-lockdown-add +56 -58
- data/bin/unicorn-lockdown-setup +4 -4
- data/lib/roda/plugins/pg_disconnect.rb +5 -6
- data/lib/unicorn-lockdown.rb +65 -120
- data/lib/unveiler.rb +38 -0
- metadata +5 -6
- data/lib/chrooter.rb +0 -153
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
CHANGED
@@ -1,3 +1,17 @@
|
|
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
|
+
|
1
15
|
= 0.13.0 (2019-07-09)
|
2
16
|
|
3
17
|
* Add Chrooter.unveil for using unveil in tests (jeremyevans)
|
data/MIT-LICENSE
CHANGED
data/README.rdoc
CHANGED
@@ -1,32 +1,24 @@
|
|
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
|
-
file system permissions), drops root privileges and then runs as the application
|
13
|
-
user (privdrop), then runs pledge to limit the allowed system calls to the minimum
|
14
|
-
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.
|
15
12
|
|
16
13
|
== Assumptions
|
17
14
|
|
18
|
-
unicorn-lockdown assumes you are using OpenBSD 6.
|
19
|
-
rubyXY-unicorn packages installed, and that
|
20
|
-
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.
|
21
18
|
|
22
19
|
It also assumes you have a SMTP server listening on localhost port 25 to
|
23
20
|
receive notification emails of worker crashes, if you are notifying for those.
|
24
21
|
|
25
|
-
If using unveil instead of chroot to limit file system access, you must be
|
26
|
-
using OpenBSD 6.6+ (or 6.5-current after July 2019), one of the OpenBSD Ruby
|
27
|
-
ports with backported support for unveil to work correctly with +File.realpath+,
|
28
|
-
and version 1.2.0+ of the pledge gem.
|
29
|
-
|
30
22
|
== Usage
|
31
23
|
|
32
24
|
=== unicorn-lockdown-setup
|
@@ -39,14 +31,14 @@ the following as root after reading the file and understanding what it does.
|
|
39
31
|
Briefly, the configuration this uses the following directories:
|
40
32
|
|
41
33
|
/var/www/sockets :: Stores unix sockets that Unicorn listens on and Nginx uses.
|
42
|
-
/var/www/
|
43
|
-
|
34
|
+
/var/www/request-error-info :: Stores temporary files for each request with request info,
|
35
|
+
used for crash notifications
|
44
36
|
/var/log/unicorn :: Stores unicorn log files, one per application
|
45
37
|
/var/log/nginx :: Stores nginx log files, two per application, one for access
|
46
38
|
and one for errors
|
47
39
|
|
48
|
-
This adds a _unicorn group that all
|
49
|
-
|
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
|
50
42
|
/etc/rc.d/unicorn_* files will use.
|
51
43
|
|
52
44
|
=== unicorn-lockdown-add
|
@@ -54,30 +46,38 @@ group, as well as a /etc/rc.d/rc.unicorn file that the per application
|
|
54
46
|
For each application you want to run with unicorn lockdown, run the following
|
55
47
|
as root, again after reading the file and understanding what it does:
|
56
48
|
|
57
|
-
unicorn-lockdown-add
|
49
|
+
unicorn-lockdown-add -o $owner -u $user $app_name
|
58
50
|
|
59
|
-
Here's
|
51
|
+
Here's the usage:
|
60
52
|
|
61
|
-
Usage: unicorn-lockdown-add [options] app_name
|
53
|
+
Usage: unicorn-lockdown-add -o owner -u user [options] app_name
|
62
54
|
Options:
|
63
|
-
-c
|
64
|
-
-d
|
65
|
-
-f
|
66
|
-
-o
|
67
|
-
-u
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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.
|
75
75
|
|
76
76
|
=== unicorn-lockdown
|
77
77
|
|
78
78
|
unicorn-lockdown is the library required in your unicorn configuration
|
79
79
|
file for the application, to handle configuring unicorn to run the app
|
80
|
-
with
|
80
|
+
with fork+exec, unveil, and pledge.
|
81
81
|
|
82
82
|
When you run unicorn-lockdown-add, it will create the unicorn configuration
|
83
83
|
file for the app if one does not already exist, looking similar to:
|
@@ -86,48 +86,75 @@ file for the app if one does not already exist, looking similar to:
|
|
86
86
|
|
87
87
|
Unicorn.lockdown(self,
|
88
88
|
:app=>"app_name",
|
89
|
-
|
90
|
-
|
91
|
-
: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
|
+
},
|
92
105
|
)
|
93
106
|
|
94
107
|
Unicorn.lockdown options:
|
95
108
|
|
96
109
|
:app :: (required) a short string for the name of the application, used
|
97
110
|
for socket/log file names and in notifications
|
98
|
-
:
|
99
|
-
|
100
|
-
|
101
|
-
process primary group, the second as the owner of the unicorn
|
102
|
-
log files.
|
111
|
+
:email :: (optional) an email address to use for notifications when the
|
112
|
+
worker process crashes or an unhandled exception is raised by
|
113
|
+
the application or middleware.
|
103
114
|
:pledge :: (optional) a pledge string to limit the allowed system calls
|
104
115
|
after privileges have been dropped
|
105
|
-
:
|
106
|
-
|
107
|
-
:
|
108
|
-
|
109
|
-
|
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
|
110
124
|
allowing more access in development, such as access needed
|
111
125
|
for file reloading.
|
112
|
-
:email :: (optional) an email address to use for notifications when the
|
113
|
-
worker process crashes or an unhandled exception is raised by
|
114
|
-
the application or middleware.
|
115
126
|
|
116
127
|
With this example pledge:
|
117
128
|
|
118
|
-
* rpath is needed to read files
|
129
|
+
* rpath is needed to read files
|
119
130
|
* prot_exec is needed in most cases
|
120
131
|
* unix is needed for the unix socket to nginx
|
121
|
-
* inet is not needed in all cases, but
|
122
|
-
of network access
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
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
|
131
158
|
way that allows it to be controllable via OpenBSD's rcctl program for
|
132
159
|
stopping/starting/reloading/restarting daemons.
|
133
160
|
|
@@ -136,7 +163,12 @@ with the expectation of an Nginx limit of 10MB, such that all client
|
|
136
163
|
requests will be buffered in memory and unicorn will not need to write
|
137
164
|
temporary files to disk. If this limit is not correct for your
|
138
165
|
application, please call client_body_buffer_size after calling
|
139
|
-
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.
|
140
172
|
|
141
173
|
When Unicorn.lockdown is used with the :email option, if the worker
|
142
174
|
process crashes, it will email the address using the contents specified
|
@@ -155,6 +187,8 @@ and then at the top of the route block, do:
|
|
155
187
|
|
156
188
|
if defined?(Unicorn) && Unicorn.respond_to?(:write_request)
|
157
189
|
Unicorn.write_request(error_email_content("Unicorn Worker Process Crash"))
|
190
|
+
# or
|
191
|
+
Unicorn.write_request(error_mail_content("Unicorn Worker Process Crash"))
|
158
192
|
end
|
159
193
|
|
160
194
|
If you don't have useful information in the request file, an email will
|
@@ -166,10 +200,20 @@ underlying problem.
|
|
166
200
|
|
167
201
|
If you are using PostgreSQL as the database for the application, and using
|
168
202
|
unix sockets to connect the application to the database, if the database
|
169
|
-
is restarted, the application will no longer be able to connect to it
|
170
|
-
|
171
|
-
|
172
|
-
|
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
|
173
217
|
connection has been lost. This plugin assumes the use of the Sequel database
|
174
218
|
library and postgres adapter with the pg driver.
|
175
219
|
|
@@ -178,78 +222,58 @@ In your Roda application:
|
|
178
222
|
# Sometime before loading the error_handler plugin
|
179
223
|
plugin :pg_disconnect
|
180
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
|
+
|
181
238
|
=== rack-email_exceptions
|
182
239
|
|
183
|
-
rack-email_exceptions is a rack middleware designed to
|
184
|
-
middleware
|
240
|
+
rack-email_exceptions is a rack middleware designed to wrap all other
|
241
|
+
middleware and the application. It rescues unhandled exceptions
|
185
242
|
raised by subsequent middleware or the application itself.
|
186
|
-
|
187
243
|
Unicorn.lockdown will automatically setup this middleware if the :email
|
188
|
-
option is used
|
189
|
-
such that it wraps the application and all other middleware.
|
244
|
+
option is used.
|
190
245
|
|
191
246
|
It is possible to use this middleware manually:
|
192
247
|
|
193
248
|
require 'rack/email_exceptions'
|
194
249
|
use Rack::EmailExceptions, "app_name", 'foo@example.com'
|
195
250
|
|
196
|
-
===
|
251
|
+
=== unveiler
|
197
252
|
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
the application into production, such as a file being read from outside
|
203
|
-
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.
|
204
257
|
|
205
|
-
|
258
|
+
unveiler assumes you are using minitest for testing. To use unveiler:
|
206
259
|
|
207
260
|
require 'minitest/autorun'
|
208
|
-
require '
|
209
|
-
at_exit{Chrooter.chroot('user_name', 'rpath prot_exec inet unix')}
|
210
|
-
|
211
|
-
If you run your specs as a regular user, it will execute them without
|
212
|
-
chrooting, but in a way that can still catch some problems that occur
|
213
|
-
when chrooted. If you run your specs as root, it will chroot to
|
214
|
-
the current directory after loading the specs, then drop
|
215
|
-
privileges to the user given (and optionally pledging using the given
|
216
|
-
pledge string), then run the specs.
|
217
|
-
|
218
|
-
If you want to use unveil instead of chroot, you can use
|
219
|
-
+Chrooter.unveil+:
|
220
|
-
|
221
|
-
require 'minitest/autorun'
|
222
|
-
require 'chrooter'
|
261
|
+
require 'unveiler'
|
223
262
|
at_exit do
|
224
|
-
|
225
|
-
'views' => 'r',
|
226
|
-
'rack' => :gem,
|
227
|
-
'mail' => :gem,
|
228
|
-
)
|
263
|
+
Unveiler.pledge_and_unveil('rpath prot_exec inet unix', 'views' => 'r')
|
229
264
|
end
|
230
265
|
|
231
|
-
One advantage of using unveil instead of chroot is that the unveils
|
232
|
-
will take effect even when not starting the tests as root.
|
233
|
-
|
234
266
|
== autoload
|
235
267
|
|
236
|
-
As you'll find out if you try to run your applications with
|
237
|
-
autoload
|
238
|
-
|
239
|
-
If you use other gems that use
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
If you have to use libraries that use autoload, it is recommended that
|
244
|
-
you use the support for unveil instead of using chroot, and allow
|
245
|
-
access to the given gems. unicorn-lockdown will automatically unveil
|
246
|
-
the +rack+ gem, and will unveil the +mail+ gem if the +Mail+ constant
|
247
|
-
is defined.
|
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:
|
248
274
|
|
249
275
|
Unicorn.lockdown(self,
|
250
|
-
|
251
|
-
:user=>"user_name", # Set application user here
|
252
|
-
:pledge=>'rpath prot_exec inet unix', # More may be needed
|
276
|
+
# ...
|
253
277
|
:unveil=>{
|
254
278
|
'views' => 'r',
|
255
279
|
'gem-name' => :gem,
|
data/bin/unicorn-lockdown-add
CHANGED
@@ -19,7 +19,7 @@ owner_uid = nil
|
|
19
19
|
owner_gid = nil
|
20
20
|
|
21
21
|
options = OptionParser.new do |opts|
|
22
|
-
opts.banner = "Usage: unicorn-lockdown-add [options] app_name"
|
22
|
+
opts.banner = "Usage: unicorn-lockdown-add -o owner -u user [options] app_name"
|
23
23
|
opts.separator "Options:"
|
24
24
|
|
25
25
|
opts.on_tail("-h", "-?", "--help", "Show this message") do
|
@@ -27,39 +27,44 @@ options = OptionParser.new do |opts|
|
|
27
27
|
exit
|
28
28
|
end
|
29
29
|
|
30
|
-
opts.on("-c
|
30
|
+
opts.on("-c RACKUP_FILE", "rackup configuration file") do |v|
|
31
31
|
rackup = "rackup_file=#{v}\n"
|
32
32
|
end
|
33
33
|
|
34
|
-
opts.on("-d
|
34
|
+
opts.on("-d DIR", "application directory name") do |v|
|
35
35
|
dir = v
|
36
36
|
end
|
37
37
|
|
38
|
-
opts.on("-f
|
38
|
+
opts.on("-f UNICORN_FILE", "unicorn configuration file relative to application directory") do |v|
|
39
39
|
unicorn_file = v
|
40
40
|
unicorn = "unicorn_conf=#{v}\n"
|
41
41
|
end
|
42
42
|
|
43
|
-
opts.on("-o
|
43
|
+
opts.on("-o OWNER", "operating system application owner") do |v|
|
44
44
|
owner = v
|
45
45
|
ent = Etc.getpwnam(v)
|
46
46
|
owner_uid = ent.uid
|
47
47
|
owner_gid = ent.gid
|
48
48
|
end
|
49
49
|
|
50
|
-
opts.on("-u
|
50
|
+
opts.on("-u USER", "operating system user to run application") do |v|
|
51
51
|
user = v
|
52
52
|
end
|
53
53
|
|
54
|
-
opts.on("--uid
|
54
|
+
opts.on("--uid UID", "user id to use if creating the user when -U is specified") do |v|
|
55
55
|
new_user_uid = Integer(v, 10)
|
56
56
|
end
|
57
57
|
end
|
58
58
|
options.parse!
|
59
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
|
+
|
60
66
|
app = ARGV.shift
|
61
67
|
dir ||= app
|
62
|
-
base_dir = dir
|
63
68
|
|
64
69
|
root_id = 0
|
65
70
|
bin_id = 7
|
@@ -67,72 +72,51 @@ www_id = 67
|
|
67
72
|
|
68
73
|
www_root = '/var/www'
|
69
74
|
dir = "#{www_root}/#{dir}"
|
70
|
-
etc_dir = "#{dir}/etc"
|
71
|
-
hosts_file = "#{etc_dir}/hosts"
|
72
|
-
resolv_file = "#{etc_dir}/resolv.conf"
|
73
75
|
rc_file = "/etc/rc.d/unicorn_#{app.tr('-', '_')}"
|
74
76
|
nginx_file = "/etc/nginx/#{app}.conf"
|
75
77
|
unicorn_conf_file = "#{dir}/#{unicorn_file}"
|
78
|
+
nonroot_dir = "/var/www/request-error-data/#{app}"
|
76
79
|
unicorn_log_file = "/var/log/unicorn/#{app}.log"
|
77
80
|
nginx_access_log_file = "/var/log/nginx/#{app}.access.log"
|
78
81
|
nginx_error_log_file = "/var/log/nginx/#{app}.error.log"
|
79
82
|
|
80
83
|
# Add application user if it doesn't exist
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
args << '-u' << new_user_uid.to_s
|
88
|
-
end
|
89
|
-
args << user
|
90
|
-
sh(*args)
|
91
|
-
Etc.getpwnam(user)
|
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
|
92
90
|
end
|
93
|
-
|
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)
|
94
103
|
end
|
95
104
|
|
96
|
-
# Create application directory
|
97
|
-
|
105
|
+
# Create application public directory if it doesn't exist (needed by nginx)
|
106
|
+
dirs = [dir, "#{dir}/public"]
|
107
|
+
dirs.each do |d|
|
98
108
|
unless File.directory?(d)
|
99
109
|
puts "Creating #{d}"
|
100
110
|
Dir.mkdir(d)
|
101
111
|
File.chmod(0755, d)
|
102
|
-
File.chown(owner_uid, owner_gid, d)
|
112
|
+
File.chown(owner_uid, owner_gid, d)
|
103
113
|
end
|
104
114
|
end
|
105
115
|
|
106
116
|
# DRY up file ownership code
|
107
117
|
setup_file_owner = lambda do |file|
|
108
118
|
File.chmod(0644, file)
|
109
|
-
File.chown(owner_uid, owner_gid, file)
|
110
|
-
end
|
111
|
-
|
112
|
-
# Setup symlink to root so that the same paths work both when
|
113
|
-
# chrooted and when not chrooted.
|
114
|
-
chroot_link = "#{dir}#{dir}"
|
115
|
-
unless File.symlink?(chroot_link)
|
116
|
-
puts "Creating #{chroot_link}"
|
117
|
-
File.symlink('/', chroot_link)
|
118
|
-
end
|
119
|
-
|
120
|
-
# Add /etc/hosts files to chroot
|
121
|
-
unless File.file?(hosts_file)
|
122
|
-
puts "Creating #{hosts_file}"
|
123
|
-
File.binwrite(hosts_file, <<END)
|
124
|
-
127.0.0.1 localhost
|
125
|
-
END
|
126
|
-
setup_file_owner.call(hosts_file)
|
127
|
-
end
|
128
|
-
|
129
|
-
# Add /etc/resolv.conf files to chroot
|
130
|
-
unless File.file?(resolv_file)
|
131
|
-
puts "Creating #{resolv_file}"
|
132
|
-
File.binwrite(resolv_file, <<END)
|
133
|
-
lookup file
|
134
|
-
END
|
135
|
-
setup_file_owner.call(resolv_file)
|
119
|
+
File.chown(owner_uid, owner_gid, file)
|
136
120
|
end
|
137
121
|
|
138
122
|
# Setup unicorn configuration file
|
@@ -142,7 +126,7 @@ unless File.file?(unicorn_conf_file)
|
|
142
126
|
puts "Creating #{unicorn_conf_dir}"
|
143
127
|
Dir.mkdir(unicorn_conf_dir)
|
144
128
|
File.chmod(0755, unicorn_conf_dir)
|
145
|
-
File.chown(owner_uid, owner_gid, unicorn_conf_dir)
|
129
|
+
File.chown(owner_uid, owner_gid, unicorn_conf_dir)
|
146
130
|
end
|
147
131
|
puts "Creating #{unicorn_conf_file}"
|
148
132
|
File.binwrite(unicorn_conf_file, <<END)
|
@@ -150,9 +134,22 @@ require 'unicorn-lockdown'
|
|
150
134
|
|
151
135
|
Unicorn.lockdown(self,
|
152
136
|
:app=>#{app.inspect},
|
153
|
-
|
154
|
-
|
155
|
-
:email=>'root'
|
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
|
+
},
|
156
153
|
)
|
157
154
|
END
|
158
155
|
setup_file_owner.call(unicorn_conf_file)
|
@@ -204,7 +201,7 @@ unless File.file?(unicorn_log_file)
|
|
204
201
|
puts "Creating #{unicorn_log_file}"
|
205
202
|
File.binwrite(unicorn_log_file, '')
|
206
203
|
File.chmod(0640, unicorn_log_file)
|
207
|
-
File.chown(app_uid,
|
204
|
+
File.chown(app_uid, Etc.getgrnam('_unicorn').gid, unicorn_log_file) if app_uid
|
208
205
|
end
|
209
206
|
|
210
207
|
# Setup /etc/rc.d/unicorn_* file for daemon management
|
@@ -213,6 +210,7 @@ unless File.file?(rc_file)
|
|
213
210
|
File.binwrite(rc_file, <<END)
|
214
211
|
#!/bin/ksh
|
215
212
|
|
213
|
+
daemon_user=#{user}
|
216
214
|
unicorn_app=#{app}
|
217
215
|
unicorn_dir=#{dir}
|
218
216
|
#{unicorn}#{rackup}
|
data/bin/unicorn-lockdown-setup
CHANGED
@@ -7,7 +7,7 @@ def sh(*args)
|
|
7
7
|
system(*args) || raise("Error while running: #{args.join(' ')}")
|
8
8
|
end
|
9
9
|
|
10
|
-
request_dir = '/var/www/
|
10
|
+
request_dir = '/var/www/request-error-data'
|
11
11
|
socket_dir = '/var/www/sockets'
|
12
12
|
unicorn_log_dir = '/var/log/unicorn'
|
13
13
|
nginx_log_dir = "/var/log/nginx"
|
@@ -31,11 +31,11 @@ unicorn_group_id = group.gid
|
|
31
31
|
unless File.directory?(request_dir)
|
32
32
|
puts "Creating #{request_dir}"
|
33
33
|
Dir.mkdir(request_dir)
|
34
|
-
File.chmod(
|
35
|
-
File.chown(root_id,
|
34
|
+
File.chmod(0710, request_dir)
|
35
|
+
File.chown(root_id, unicorn_group_id, request_dir)
|
36
36
|
end
|
37
37
|
|
38
|
-
# Setup sockets directory to hold
|
38
|
+
# Setup sockets directory to hold nginx connection sockets
|
39
39
|
unless File.directory?(socket_dir)
|
40
40
|
puts "Creating #{socket_dir}"
|
41
41
|
Dir.mkdir(socket_dir)
|
@@ -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,7 +22,7 @@ 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
|
@@ -33,7 +32,7 @@ class Roda
|
|
33
32
|
|
34
33
|
# When database connection is lost, kill the worker process, so a new one will be generated.
|
35
34
|
# This is necessary because the unix socket used by the database connection is no longer available
|
36
|
-
# once the application is
|
35
|
+
# once the application is unveiled or pledged.
|
37
36
|
def _roda_handle_main_route
|
38
37
|
super
|
39
38
|
rescue Sequel::DatabaseDisconnectError, Sequel::DatabaseConnectionError, PG::ConnectionBad
|
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,14 +62,13 @@ 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
|
56
|
-
|
57
|
-
attr_accessor :group_name
|
68
|
+
# The pledge string to use for the master process.
|
69
|
+
attr_accessor :master_pledge
|
58
70
|
|
59
|
-
# The pledge string to use.
|
71
|
+
# The pledge string to use for worker processes.
|
60
72
|
attr_accessor :pledge
|
61
73
|
|
62
74
|
# The hash of unveil paths to use.
|
@@ -65,13 +77,13 @@ class << Unicorn
|
|
65
77
|
# The hash of additional unveil paths to use if in the development environment.
|
66
78
|
attr_accessor :dev_unveil
|
67
79
|
|
68
|
-
# The address to email for crash and unhandled exception notifications
|
80
|
+
# The address to email for crash and unhandled exception notifications.
|
69
81
|
attr_accessor :email
|
70
82
|
|
71
83
|
# Helper method to write request information to the request logger.
|
72
84
|
# +email_message+ should be an email message including headers and body.
|
73
85
|
# This should be called at the top of the Roda route block for the
|
74
|
-
# application.
|
86
|
+
# application (or at some early point before processing in other web frameworks).
|
75
87
|
def write_request(email_message)
|
76
88
|
request_logger.seek(0, IO::SEEK_SET)
|
77
89
|
request_logger.truncate(0)
|
@@ -79,26 +91,27 @@ class << Unicorn
|
|
79
91
|
request_logger.fsync
|
80
92
|
end
|
81
93
|
|
82
|
-
# Helper method that sets up all necessary code for
|
94
|
+
# Helper method that sets up all necessary code for unveil/pledge support.
|
83
95
|
# This should be called inside the appropriate unicorn.conf file.
|
84
96
|
# The configurator should be self in the top level scope of the
|
85
97
|
# unicorn.conf file, and this takes options:
|
86
98
|
#
|
87
99
|
# Options:
|
88
|
-
# :app :: The name of the application
|
100
|
+
# :app (required) :: The name of the application
|
89
101
|
# :email : The email to notify for worker crashes
|
90
|
-
# :
|
91
|
-
# :
|
92
|
-
#
|
93
|
-
#
|
94
|
-
#
|
95
|
-
# :unveil :: A hash of unveil paths
|
96
|
-
# :dev_unveil :: A hash of unveil paths to use in development
|
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.
|
97
110
|
def lockdown(configurator, opts)
|
98
111
|
Unicorn.app_name = opts.fetch(:app)
|
99
|
-
Unicorn.user_name = opts.fetch(:user)
|
100
|
-
Unicorn.group_name = opts[:group] || opts[:user]
|
101
112
|
Unicorn.email = opts[:email]
|
113
|
+
Unicorn.master_pledge = opts[:master_pledge]
|
114
|
+
Unicorn.master_execpledge = opts[:master_execpledge]
|
102
115
|
Unicorn.pledge = opts[:pledge]
|
103
116
|
Unicorn.unveil = opts[:unveil]
|
104
117
|
Unicorn.dev_unveil = opts[:dev_unveil]
|
@@ -125,13 +138,12 @@ class << Unicorn
|
|
125
138
|
after_fork do |server, worker|
|
126
139
|
server.logger.info("worker=#{worker.nr} spawned pid=#{$$}")
|
127
140
|
|
128
|
-
# Set the request logger for the worker process after forking.
|
129
|
-
# 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.
|
130
142
|
Unicorn.request_logger = File.open(server.request_filename($$), "wb")
|
131
143
|
Unicorn.request_logger.sync = true
|
132
144
|
end
|
133
145
|
|
134
|
-
if wrap_app = Unicorn.email
|
146
|
+
if wrap_app = Unicorn.email
|
135
147
|
require 'rack/email_exceptions'
|
136
148
|
end
|
137
149
|
|
@@ -147,80 +159,31 @@ class << Unicorn
|
|
147
159
|
end
|
148
160
|
end
|
149
161
|
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
else
|
156
|
-
Hash[unveil]
|
157
|
-
end
|
162
|
+
unveil = if Unicorn.dev_unveil && ENV['RACK_ENV'] == 'development'
|
163
|
+
Unicorn.unveil.merge(Unicorn.dev_unveil)
|
164
|
+
else
|
165
|
+
Hash[Unicorn.unveil]
|
166
|
+
end
|
158
167
|
|
168
|
+
# Don't allow loading files in rack and mail gems if not using rubygems
|
169
|
+
if defined?(Gem) && Gem.respond_to?(:loaded_specs)
|
159
170
|
# Allow read access to the rack gem directory, as rack autoloads constants.
|
160
|
-
|
161
|
-
|
162
|
-
if defined?(Mail)
|
163
|
-
# If using the mail library, allow read access to the mail gem directory,
|
164
|
-
# as mail autoloads constants.
|
165
|
-
unveil['mail'] = :gem
|
166
|
-
end
|
167
|
-
|
168
|
-
# Drop privileges
|
169
|
-
worker.user(Unicorn.user_name, Unicorn.group_name)
|
170
|
-
|
171
|
-
# Restrict access to the file system based on the specified unveil.
|
172
|
-
Pledge.unveil(unveil)
|
173
|
-
else
|
174
|
-
# Before chrooting, reference all constants that use autoload
|
175
|
-
# that are probably needed at runtime. This must be done
|
176
|
-
# before chrooting as attempting to load the constants after
|
177
|
-
# chrooting will break things.
|
178
|
-
|
179
|
-
# Start with rack, which uses autoload for all constants.
|
180
|
-
# Most of rack's constants are not used at runtime, this
|
181
|
-
# lists the ones most commonly needed.
|
182
|
-
Rack::Multipart
|
183
|
-
Rack::Multipart::Parser
|
184
|
-
Rack::Multipart::Generator
|
185
|
-
Rack::Multipart::UploadedFile
|
186
|
-
Rack::Mime
|
187
|
-
Rack::Auth::Digest::Params
|
188
|
-
|
189
|
-
# In the development environment, reference all middleware
|
190
|
-
# the unicorn will load by default, unless unicorn is
|
191
|
-
# set to not load middleware by default.
|
192
|
-
if ENV['RACK_ENV'] == 'development' && (!respond_to?(:set) || set[:default_middleware] != false)
|
193
|
-
Rack::ContentLength
|
194
|
-
Rack::CommonLogger
|
195
|
-
Rack::Chunked
|
196
|
-
Rack::Lint
|
197
|
-
Rack::ShowExceptions
|
198
|
-
Rack::TempfileReaper
|
171
|
+
if defined?(Rack) && Gem.loaded_specs['rack']
|
172
|
+
unveil['rack'] = :gem
|
199
173
|
end
|
200
174
|
|
201
|
-
# If using the mail library,
|
202
|
-
#
|
203
|
-
|
204
|
-
|
205
|
-
if defined?(Mail)
|
206
|
-
Mail.eager_autoload!
|
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
|
207
179
|
end
|
208
|
-
|
209
|
-
# Strip path prefixes from the reloader. This is only
|
210
|
-
# really need in development mode for code reloading to work.
|
211
|
-
pwd = Dir.pwd
|
212
|
-
Unreloader.strip_path_prefix(pwd) if defined?(Unreloader)
|
213
|
-
|
214
|
-
# Drop privileges. This must be done after chrooting as
|
215
|
-
# chrooting requires root privileges.
|
216
|
-
worker.user(Unicorn.user_name, Unicorn.group_name, pwd)
|
217
180
|
end
|
218
181
|
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
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)
|
224
187
|
end
|
225
188
|
|
226
189
|
# the last time there was a worker crash and the request information
|
@@ -263,30 +226,13 @@ class << Unicorn
|
|
263
226
|
# If the request filename exists and the worker process crashed,
|
264
227
|
# send a notification email.
|
265
228
|
Process.waitpid(fork do
|
266
|
-
# Load net/smtp early
|
229
|
+
# Load net/smtp early
|
267
230
|
require 'net/smtp'
|
268
231
|
|
269
232
|
# When setting the email, first get the contents of the email
|
270
233
|
# from the request file.
|
271
234
|
body = File.read(file)
|
272
235
|
|
273
|
-
# Then get information from /etc and drop group privileges
|
274
|
-
uid = Etc.getpwnam(Unicorn.user_name).uid
|
275
|
-
group = Unicorn.group_name
|
276
|
-
group = group.first if group.is_a?(Array)
|
277
|
-
gid = Etc.getgrnam(group).gid
|
278
|
-
if gid && Process.egid != gid
|
279
|
-
Process.initgroups(Unicorn.user_name, gid)
|
280
|
-
Process::GID.change_privilege(gid)
|
281
|
-
end
|
282
|
-
|
283
|
-
# Then chroot
|
284
|
-
Dir.chroot(Dir.pwd)
|
285
|
-
Dir.chdir('/')
|
286
|
-
|
287
|
-
# Then drop user privileges
|
288
|
-
Process.euid != uid and Process::UID.change_privilege(uid)
|
289
|
-
|
290
236
|
# Then use a restrictive pledge
|
291
237
|
Pledge.pledge('inet prot_exec')
|
292
238
|
|
@@ -309,4 +255,3 @@ class << Unicorn
|
|
309
255
|
end
|
310
256
|
end
|
311
257
|
end
|
312
|
-
|
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
|
@@ -52,10 +52,10 @@ files:
|
|
52
52
|
- bin/unicorn-lockdown-add
|
53
53
|
- bin/unicorn-lockdown-setup
|
54
54
|
- files/rc.unicorn
|
55
|
-
- lib/chrooter.rb
|
56
55
|
- lib/rack/email_exceptions.rb
|
57
56
|
- lib/roda/plugins/pg_disconnect.rb
|
58
57
|
- lib/unicorn-lockdown.rb
|
58
|
+
- lib/unveiler.rb
|
59
59
|
homepage: https://github.com/jeremyevans/unicorn-lockdown
|
60
60
|
licenses:
|
61
61
|
- MIT
|
@@ -75,9 +75,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
75
75
|
- !ruby/object:Gem::Version
|
76
76
|
version: '0'
|
77
77
|
requirements: []
|
78
|
-
rubygems_version: 3.
|
78
|
+
rubygems_version: 3.1.4
|
79
79
|
signing_key:
|
80
80
|
specification_version: 4
|
81
|
-
summary: Helper library for running Unicorn with
|
82
|
-
on OpenBSD
|
81
|
+
summary: Helper library for running Unicorn with fork+exec/unveil/pledge on OpenBSD
|
83
82
|
test_files: []
|
data/lib/chrooter.rb
DELETED
@@ -1,153 +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/unveil and non
|
24
|
-
# chroot modes.
|
25
|
-
module Chrooter
|
26
|
-
# If the current user is the super user, drop privileges to
|
27
|
-
# the given +user+ first.
|
28
|
-
#
|
29
|
-
# Use Pledge.unveil to limit access to the file system based on the
|
30
|
-
# +unveil+ option. Then pledge the process with the given +pledge+
|
31
|
-
# permissions (if given).
|
32
|
-
def self.unveil(user, pledge=nil, unveil={}, group=user)
|
33
|
-
require 'unveil'
|
34
|
-
|
35
|
-
unveil = Hash[unveil]
|
36
|
-
if defined?(Rack)
|
37
|
-
unveil['rack'] = :gem
|
38
|
-
end
|
39
|
-
if defined?(Mail)
|
40
|
-
unveil['mail'] = :gem
|
41
|
-
end
|
42
|
-
|
43
|
-
if Process.euid == 0
|
44
|
-
_drop_privs(user, group)
|
45
|
-
puts "Unveiled, running as user #{user}"
|
46
|
-
end
|
47
|
-
|
48
|
-
Pledge.unveil(unveil)
|
49
|
-
|
50
|
-
_pledge(pledge)
|
51
|
-
end
|
52
|
-
|
53
|
-
# If the current user is the super user, change to the given
|
54
|
-
# user/group, chroot to the given directory, and pledge
|
55
|
-
# the process with the given permissions (if given).
|
56
|
-
#
|
57
|
-
# If the current user is not the super user, freeze
|
58
|
-
# $LOADED_FEATURES to more easily detect problems with
|
59
|
-
# autoloaded constants, and just pledge the process with the
|
60
|
-
# given permissions (if given).
|
61
|
-
#
|
62
|
-
# This will reference common autoloaded constants in the
|
63
|
-
# rack and mail libraries if they are defined. Other
|
64
|
-
# autoloaded constants should be referenced before calling
|
65
|
-
# this method.
|
66
|
-
#
|
67
|
-
# In general this should be called inside an at_exit block
|
68
|
-
# after loading minitest/autorun, so it will run after all
|
69
|
-
# specs are loaded, but before running specs.
|
70
|
-
def self.chroot(user, pledge=nil, group=user, dir=Dir.pwd)
|
71
|
-
# Work around autoload issues in libraries.
|
72
|
-
# autoload is problematic when chrooting because if the
|
73
|
-
# constant is not referenced before chrooting, an
|
74
|
-
# exception is raised if the constant is raised
|
75
|
-
# after chrooting.
|
76
|
-
#
|
77
|
-
# The constants listed here are the autoloaded constants
|
78
|
-
# known to be used by any applications. This list
|
79
|
-
# may need to be updated when libraries are upgraded
|
80
|
-
# and add new constants, or when applications start
|
81
|
-
# using new features.
|
82
|
-
if defined?(Rack)
|
83
|
-
Rack::MockRequest if defined?(Rack::MockRequest)
|
84
|
-
Rack::Auth::Digest::Params if defined?(Rack::Auth::Digest::Params)
|
85
|
-
if defined?(Rack::Multipart)
|
86
|
-
Rack::Multipart
|
87
|
-
Rack::Multipart::Parser
|
88
|
-
Rack::Multipart::Generator
|
89
|
-
Rack::Multipart::UploadedFile
|
90
|
-
end
|
91
|
-
end
|
92
|
-
if defined?(Mail)
|
93
|
-
Mail::Address
|
94
|
-
Mail::AddressList
|
95
|
-
Mail::Parsers::AddressListsParser
|
96
|
-
Mail::ContentTransferEncodingElement
|
97
|
-
Mail::ContentDispositionElement
|
98
|
-
Mail::MessageIdsElement
|
99
|
-
Mail::MimeVersionElement
|
100
|
-
Mail::OptionalField
|
101
|
-
Mail::ContentTypeElement
|
102
|
-
end
|
103
|
-
|
104
|
-
if Process.euid == 0
|
105
|
-
_drop_privs(user, group) do
|
106
|
-
Dir.chroot(dir)
|
107
|
-
Dir.chdir('/')
|
108
|
-
end
|
109
|
-
puts "Chrooted to #{dir}, running as user #{user}"
|
110
|
-
else
|
111
|
-
# Load minitest plugins before freezing loaded features,
|
112
|
-
# so they don't break.
|
113
|
-
Minitest.load_plugins
|
114
|
-
|
115
|
-
# Emulate chroot not working by freezing $LOADED_FEATURES
|
116
|
-
# This allows to more easily catch bugs that only occur
|
117
|
-
# when chrooted, such as referencing an autoloaded constant
|
118
|
-
# that wasn't loaded before the chroot.
|
119
|
-
$LOADED_FEATURES.freeze
|
120
|
-
end
|
121
|
-
|
122
|
-
_pledge(pledge)
|
123
|
-
end
|
124
|
-
|
125
|
-
def self._drop_privs(user, group)
|
126
|
-
uid = Etc.getpwnam(user).uid
|
127
|
-
gid = Etc.getgrnam(group).gid
|
128
|
-
if Process.egid != gid
|
129
|
-
Process.initgroups(user, gid)
|
130
|
-
Process::GID.change_privilege(gid)
|
131
|
-
end
|
132
|
-
|
133
|
-
yield if block_given?
|
134
|
-
|
135
|
-
Process.euid != uid and Process::UID.change_privilege(uid)
|
136
|
-
|
137
|
-
nil
|
138
|
-
end
|
139
|
-
private_class_method :_drop_privs
|
140
|
-
|
141
|
-
def self._pledge(pledge)
|
142
|
-
unless defined?(SimpleCov)
|
143
|
-
if pledge
|
144
|
-
# If running coverage tests, don't run pledged as coverage
|
145
|
-
# testing can require many additional permissions.
|
146
|
-
Pledge.pledge(pledge)
|
147
|
-
end
|
148
|
-
end
|
149
|
-
|
150
|
-
nil
|
151
|
-
end
|
152
|
-
private_class_method :_pledge
|
153
|
-
end
|