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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5246972fac1124b5bad333f7c94163ca00193f6ad621febaef8d833be05bb6b8
4
- data.tar.gz: f015a78584e3fbbeecdfc961f49acdbd9b6f967d6e73e5e47300ccf3b3b15552
3
+ metadata.gz: 931209c4514dbb9f15da327fac6ed81f82fed2a504c500a34aa5f6e2624cde5a
4
+ data.tar.gz: 160c9cf285e6c4f8abeb38bce3d1b7006f236607e6f18e606fdcbd8b53f73baa
5
5
  SHA512:
6
- metadata.gz: 1d10bca6474b0c6da80c244670e0edd1fd128fa7866285dad4efaf7c5634f48889db95499556330462d23415237bddb537c80e2316e4a2b935878cefc0c2da66
7
- data.tar.gz: b325c37291644bc1b06d37ff6184db81c1355668a612a1cf1bb6516aeee199cf664cce13bb4fcaba2c1bfb2252087ced308f2efa6a5c26f2d78a805ab4a271d9
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)
@@ -1,4 +1,4 @@
1
- Copyright (c) 2018 Jeremy Evans
1
+ Copyright (c) 2018-2020 Jeremy Evans
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy
4
4
  of this software and associated documentation files (the "Software"), to
@@ -1,32 +1,24 @@
1
1
  = unicorn-lockdown
2
2
 
3
- unicorn-lockdown is a helper library for running Unicorn on OpenBSD in a way
4
- that supports security features such as chroot (or unveil), privdrop, fork+exec,
5
- and pledge.
6
-
7
- With the configuration unicorn-lockdown uses, the unicorn process executes as root,
8
- and the unicorn master process continues to run as root. The master process
9
- forks worker processes, which re-exec (fork+exec) so that a new memory layout
10
- is used in each worker process. The worker process then loads the application,
11
- after which it chroots to the application's directory (or uses unveil to limit
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.2+ with the nginx and
19
- rubyXY-unicorn packages installed, and that you have a unicorn symlink in
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/requests :: Stores temporary files for each request with request info,
43
- used for crash notifications
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 per-application users will use as their
49
- group, as well as a /etc/rc.d/rc.unicorn file that the per application
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 an excerpt of the usage:
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 rackup_file rackup configuration file
64
- -d dir application directory name
65
- -f unicorn_file unicorn configuration file relative to application directory
66
- -o owner operating system application owner
67
- -u user operating system user to run application
68
- --uid uid user id to use if creating the user when -U is specified
69
-
70
- It is a very good idea to specify -o and -u, the other options can be ignored
71
- if you are OK with the default values. The owner (-o) and the user (-u) should be
72
- different. The user is the user the application runs as, and should have very limited
73
- access to the application directory. The owner is the user that owns the application
74
- directory and can make modifications to the application.
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 chroot (or unveil), privdrop, fork+exec, and pledge.
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
- :user=>"user_name", # Set application user here
90
- :pledge=>'rpath prot_exec inet unix', # More may be needed
91
- :email=>'root' # update this with correct email
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
- :user :: (required) the user to drop privileges to
99
- :group :: (optional) the group to use to run the application. On Unicorn
100
- 5.5.0+, can be an array with two entries, the first used as the
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
- :unveil :: (optional) a hash of paths to limit file system access, passed
106
- to +Pledge.unveil+. Forces use of unveil instead of chroot.
107
- :dev_unveil :: (optional, requires :unveil option) a hash of paths to
108
- limit file system, merged into the :unveil option paths
109
- if in the development environment. Useful if you are
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 in the chrooted file system
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 in most applications need some form
122
- of network access. pf (OpenBSD's firewall) should be used to limit
123
- access for the application's operating system user to the minimum
124
- necessary access needed.
125
-
126
- unicorn-lockdown has specific support for allowing for emails to be sent
127
- for Unicorn worker crashes (e.g. pledge violations). It also has support
128
- for using rack-unreloader to run your application in development mode
129
- under the chroot while allowing for reloading files if they are modified.
130
- Additionally, unicorn-lockdown modifies unicorn's process status line in a
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. The
170
- only way to fix this is to kill the worker process and have the master
171
- process spawn a new worker. The roda-pg_disconnect plugin is a plugin
172
- for the roda web toolkit to kill the worker if it detects the database
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 be the first
184
- middleware loaded into applications. It rescues unhandled exceptions
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 and the RACK_ENV environment variable is set to production,
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
- === chrooter
251
+ === unveiler
197
252
 
198
- chrooter is a library designed to help with testing applications both in
199
- chroot mode and non-chroot mode. If you are running your application
200
- chrooted, you must support testing while chrooted otherwise it is very
201
- difficult to find problems that only occur when chrooted before putting
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
- chrooter assumes you are using minitest for testing. To use chrooter:
258
+ unveiler assumes you are using minitest for testing. To use unveiler:
206
259
 
207
260
  require 'minitest/autorun'
208
- require 'chrooter'
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
- Chrooter.unveil('user_name', 'rpath prot_exec inet unix',
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 chroot,
237
- autoload is the enemy. Both unicorn-lockdown and chrooter have support
238
- for handling common autoloaded constants in the rack and mail gems.
239
- If you use other gems that use autoload, you'll have to add code that
240
- references the autoloaded constants after the application is loaded but
241
- before chrooting.
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
- :app=>"app_name",
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,
@@ -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 rackup_file", "rackup configuration file") do |v|
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 dir", "application directory name") do |v|
34
+ opts.on("-d DIR", "application directory name") do |v|
35
35
  dir = v
36
36
  end
37
37
 
38
- opts.on("-f unicorn_file", "unicorn configuration file relative to application directory") do |v|
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 owner", "operating system application owner") do |v|
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 user", "operating system user to run application") do |v|
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 uid", "user id to use if creating the user when -U is specified") do |v|
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
- if user
82
- passwd = begin
83
- Etc.getpwnam(user)
84
- rescue ArgumentError
85
- args = ['/usr/sbin/useradd', '-d', '/var/empty', '-g', '=uid', '-G', '_unicorn', '-L', 'daemon', '-s', '/sbin/nologin']
86
- if new_user_uid
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
- app_uid = passwd.uid
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 and chroot directories if they doesn't exist
97
- [dir, "#{dir}/var", "#{dir}/var/www", "#{dir}/etc", "#{dir}/public"].each do |d|
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) if owner
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) if owner
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) if owner
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
- :user=>#{user.inspect}, # Set application user here
154
- :pledge=>'rpath prot_exec inet unix', # More may be needed
155
- :email=>'root' # update this with correct email
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, app_uid, unicorn_log_file) if 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}
@@ -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/requests'
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(0700, request_dir)
35
- File.chown(root_id, daemon_id, request_dir)
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 PostgreSQL database connection sockets
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 using chroot and the database
12
- # connection socket is outside the chroot, or because they are using a firewall and access
13
- # to the database server is not allowed from the application the process is running as after
14
- # privileges are dropped.
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 chrooted.
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 chrooted.
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
@@ -1,25 +1,38 @@
1
- # unicorn-lockdown is designed to be used with Unicorn's chroot support, and
2
- # handles:
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
- # * forcing loading of some common autoloaded constants
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
- # Loading single_byte encoding
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. The
19
- # /var/www/requests folder is currently accessable only
20
- # to root.
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/requests/#{Unicorn.app_name}.#{pid}.txt"
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 user to run as. Also specifies the group to run as if group_name is not set.
53
- attr_accessor :user_name
65
+ # The pledge string to use for the master process's spawned processes by default.
66
+ attr_accessor :master_execpledge
54
67
 
55
- # The group name to run as. Can be an array of two strings, where the first string
56
- # is the primary group, and the second string is the group used for the log files.
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 chroot/pledge support.
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 (required)
100
+ # :app (required) :: The name of the application
89
101
  # :email : The email to notify for worker crashes
90
- # :user :: The user to run as (required)
91
- # :group :: The group to run as (if not set, uses :user as the group).
92
- # Can be an array of two strings, where the first string is the primary
93
- # group, and the second string is the group used for the log files.
94
- # :pledge :: The string to use when pledging
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. The
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 && ENV['RACK_ENV'] == 'production'
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
- if unveil = Unicorn.unveil
151
- require 'unveil'
152
-
153
- unveil = if Unicorn.dev_unveil && ENV['RACK_ENV'] == 'development'
154
- unveil.merge(Unicorn.dev_unveil)
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
- unveil['rack'] = :gem
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, eagerly autoload all constants.
202
- # This costs about 9MB of memory, but the mail gem changes
203
- # their autoloaded constants on a regular basis, so it's
204
- # better to be safe than sorry.
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
- if Unicorn.pledge
220
- # Pledge after dropping privileges, because dropping
221
- # privileges requires a separate pledge.
222
- Pledge.pledge(Unicorn.pledge)
223
- end
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, before chrooting.
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
-
@@ -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.13.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: 2019-07-09 00:00:00.000000000 Z
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.0.3
78
+ rubygems_version: 3.1.4
79
79
  signing_key:
80
80
  specification_version: 4
81
- summary: Helper library for running Unicorn with (chroot|unveil)/privdrop/fork+exec/pledge
82
- on OpenBSD
81
+ summary: Helper library for running Unicorn with fork+exec/unveil/pledge on OpenBSD
83
82
  test_files: []
@@ -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