unicorn-lockdown 0.13.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG +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
|