unicorn-lockdown 0.12.0 → 1.1.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 +30 -0
- data/MIT-LICENSE +1 -1
- data/README.rdoc +158 -88
- data/bin/unicorn-lockdown-add +1 -223
- data/bin/unicorn-lockdown-setup +1 -67
- data/files/unicorn_lockdown_add.rb +227 -0
- data/files/unicorn_lockdown_setup.rb +74 -0
- data/lib/rack/email_exceptions.rb +6 -1
- data/lib/roda/plugins/pg_disconnect.rb +8 -9
- data/lib/unicorn-lockdown.rb +93 -101
- data/lib/unveiler.rb +42 -0
- metadata +69 -8
- data/lib/chrooter.rb +0 -110
data/bin/unicorn-lockdown-setup
CHANGED
@@ -1,68 +1,2 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
require 'etc'
|
4
|
-
|
5
|
-
def sh(*args)
|
6
|
-
puts "Running: #{args.join(' ')}"
|
7
|
-
system(*args) || raise("Error while running: #{args.join(' ')}")
|
8
|
-
end
|
9
|
-
|
10
|
-
request_dir = '/var/www/requests'
|
11
|
-
socket_dir = '/var/www/sockets'
|
12
|
-
unicorn_log_dir = '/var/log/unicorn'
|
13
|
-
nginx_log_dir = "/var/log/nginx"
|
14
|
-
rc_unicorn_file = '/etc/rc.d/rc.unicorn'
|
15
|
-
|
16
|
-
unicorn_group = '_unicorn'
|
17
|
-
root_id = 0
|
18
|
-
daemon_id = 1
|
19
|
-
www_id = 67
|
20
|
-
|
21
|
-
# Add _unicorn group if it doesn't exist
|
22
|
-
group = begin
|
23
|
-
Etc.getgrnam(unicorn_group)
|
24
|
-
rescue ArgumentError
|
25
|
-
sh('groupadd', unicorn_group)
|
26
|
-
Etc.getgrnam(unicorn_group)
|
27
|
-
end
|
28
|
-
unicorn_group_id = group.gid
|
29
|
-
|
30
|
-
# Setup requests directory to hold per-request information for crash notifications
|
31
|
-
unless File.directory?(request_dir)
|
32
|
-
puts "Creating #{request_dir}"
|
33
|
-
Dir.mkdir(request_dir)
|
34
|
-
File.chmod(0700, request_dir)
|
35
|
-
File.chown(root_id, daemon_id, request_dir)
|
36
|
-
end
|
37
|
-
|
38
|
-
# Setup sockets directory to hold PostgreSQL database connection sockets
|
39
|
-
unless File.directory?(socket_dir)
|
40
|
-
puts "Creating #{socket_dir}"
|
41
|
-
Dir.mkdir(socket_dir)
|
42
|
-
File.chmod(0770, socket_dir)
|
43
|
-
File.chown(www_id, unicorn_group_id, socket_dir)
|
44
|
-
end
|
45
|
-
|
46
|
-
# Setup log directory to hold unicorn application logs
|
47
|
-
unless File.directory?(unicorn_log_dir)
|
48
|
-
puts "Creating #{unicorn_log_dir}"
|
49
|
-
Dir.mkdir(unicorn_log_dir)
|
50
|
-
File.chmod(0755, unicorn_log_dir)
|
51
|
-
File.chown(root_id, daemon_id, unicorn_log_dir)
|
52
|
-
end
|
53
|
-
|
54
|
-
# Setup log directory to hold nginx logs
|
55
|
-
unless File.directory?(nginx_log_dir)
|
56
|
-
puts "Creating #{nginx_log_dir}"
|
57
|
-
Dir.mkdir(nginx_log_dir)
|
58
|
-
File.chmod(0775, nginx_log_dir)
|
59
|
-
File.chown(www_id, root_id, nginx_log_dir)
|
60
|
-
end
|
61
|
-
|
62
|
-
# Setup rc.unicorn file
|
63
|
-
unless File.file?(rc_unicorn_file)
|
64
|
-
puts "Creating #{rc_unicorn_file}"
|
65
|
-
File.binwrite(rc_unicorn_file, File.binread(File.join(File.dirname(__dir__), 'files', 'rc.unicorn')))
|
66
|
-
File.chmod(0644, rc_unicorn_file)
|
67
|
-
File.chown(root_id, root_id, rc_unicorn_file)
|
68
|
-
end
|
2
|
+
require_relative '../files/unicorn_lockdown_setup'
|
@@ -0,0 +1,227 @@
|
|
1
|
+
require 'etc'
|
2
|
+
require 'optparse'
|
3
|
+
|
4
|
+
unicorn = ''
|
5
|
+
rackup = ''
|
6
|
+
unicorn_file = 'unicorn.conf'
|
7
|
+
dir = nil
|
8
|
+
user = nil
|
9
|
+
new_user_uid = nil
|
10
|
+
owner = nil
|
11
|
+
owner_uid = nil
|
12
|
+
owner_gid = nil
|
13
|
+
|
14
|
+
options = OptionParser.new do |opts|
|
15
|
+
opts.banner = "Usage: unicorn-lockdown-add -o owner -u user [options] app_name"
|
16
|
+
opts.separator "Options:"
|
17
|
+
|
18
|
+
opts.on_tail("-h", "-?", "--help", "Show this message") do
|
19
|
+
puts opts
|
20
|
+
exit
|
21
|
+
end
|
22
|
+
|
23
|
+
opts.on("-c RACKUP_FILE", "rackup configuration file") do |v|
|
24
|
+
rackup = "rackup_file=#{v}\n"
|
25
|
+
end
|
26
|
+
|
27
|
+
opts.on("-d DIR", "application directory name") do |v|
|
28
|
+
dir = v
|
29
|
+
end
|
30
|
+
|
31
|
+
opts.on("-f UNICORN_FILE", "unicorn configuration file relative to application directory") do |v|
|
32
|
+
unicorn_file = v
|
33
|
+
unicorn = "unicorn_conf=#{v}\n"
|
34
|
+
end
|
35
|
+
|
36
|
+
opts.on("-o OWNER", "operating system application owner") do |v|
|
37
|
+
owner = v
|
38
|
+
ent = Etc.getpwnam(v)
|
39
|
+
owner_uid = ent.uid
|
40
|
+
owner_gid = ent.gid
|
41
|
+
end
|
42
|
+
|
43
|
+
opts.on("-u USER", "operating system user to run application") do |v|
|
44
|
+
user = v
|
45
|
+
end
|
46
|
+
|
47
|
+
opts.on("--uid UID", "user id to use if creating the user when -U is specified") do |v|
|
48
|
+
new_user_uid = Integer(v, 10)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
options.parse!
|
52
|
+
|
53
|
+
unless user && owner
|
54
|
+
$stderr.puts "Must pass -o and -u options when calling unicorn_lockdown_add"
|
55
|
+
puts options
|
56
|
+
exit(1)
|
57
|
+
end
|
58
|
+
|
59
|
+
app = ARGV.shift
|
60
|
+
dir ||= app
|
61
|
+
|
62
|
+
root_id = 0
|
63
|
+
bin_id = 7
|
64
|
+
www_id = 67
|
65
|
+
|
66
|
+
prefix = ENV['UNICORN_LOCKDOWN_BIN_PREFIX']
|
67
|
+
www_root = "#{prefix}/var/www"
|
68
|
+
dir = "#{www_root}/#{dir}"
|
69
|
+
rc_file = "#{prefix}/etc/rc.d/unicorn_#{app.tr('-', '_')}"
|
70
|
+
nginx_file = "#{prefix}/etc/nginx/#{app}.conf"
|
71
|
+
unicorn_conf_file = "#{dir}/#{unicorn_file}"
|
72
|
+
nonroot_dir = "#{prefix}/var/www/request-error-data/#{app}"
|
73
|
+
unicorn_log_file = "#{prefix}/var/log/unicorn/#{app}.log"
|
74
|
+
nginx_access_log_file = "#{prefix}/var/log/nginx/#{app}.access.log"
|
75
|
+
nginx_error_log_file = "#{prefix}/var/log/nginx/#{app}.error.log"
|
76
|
+
|
77
|
+
if Process.uid == 0
|
78
|
+
# :nocov:
|
79
|
+
chown = lambda{|*a| File.chown(*a)}
|
80
|
+
# :nocov:
|
81
|
+
else
|
82
|
+
chown = lambda{|*a| }
|
83
|
+
end
|
84
|
+
|
85
|
+
# Add application user if it doesn't exist
|
86
|
+
passwd = begin
|
87
|
+
Etc.getpwnam(user)
|
88
|
+
rescue ArgumentError
|
89
|
+
# :nocov:
|
90
|
+
args = ['/usr/sbin/useradd', '-d', '/var/empty', '-g', '=uid', '-G', '_unicorn', '-L', 'daemon', '-s', '/sbin/nologin']
|
91
|
+
if new_user_uid
|
92
|
+
args << '-u' << new_user_uid.to_s
|
93
|
+
end
|
94
|
+
args << user
|
95
|
+
puts "Running: #{args.join(' ')}"
|
96
|
+
system(*args) || raise("Error while running: #{args.join(' ')}")
|
97
|
+
Etc.getpwnam(user)
|
98
|
+
# :nocov:
|
99
|
+
end
|
100
|
+
app_uid = passwd.uid
|
101
|
+
|
102
|
+
# Create the subdirectory used for request error info when not running as root
|
103
|
+
unless File.directory?(nonroot_dir)
|
104
|
+
puts "Creating #{nonroot_dir}"
|
105
|
+
Dir.mkdir(nonroot_dir)
|
106
|
+
File.chmod(0700, nonroot_dir)
|
107
|
+
chown.(app_uid, app_uid, nonroot_dir)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Create application public directory if it doesn't exist (needed by nginx)
|
111
|
+
dirs = [dir, "#{dir}/public"]
|
112
|
+
dirs.each do |d|
|
113
|
+
unless File.directory?(d)
|
114
|
+
puts "Creating #{d}"
|
115
|
+
Dir.mkdir(d)
|
116
|
+
File.chmod(0755, d)
|
117
|
+
chown.(owner_uid, owner_gid, d)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# DRY up file ownership code
|
122
|
+
setup_file_owner = lambda do |file|
|
123
|
+
File.chmod(0644, file)
|
124
|
+
chown.(owner_uid, owner_gid, file)
|
125
|
+
end
|
126
|
+
|
127
|
+
# Setup unicorn configuration file
|
128
|
+
unless File.file?(unicorn_conf_file)
|
129
|
+
unicorn_conf_dir = File.dirname(unicorn_conf_file)
|
130
|
+
unless File.directory?(unicorn_conf_dir)
|
131
|
+
puts "Creating #{unicorn_conf_dir}"
|
132
|
+
Dir.mkdir(unicorn_conf_dir)
|
133
|
+
File.chmod(0755, unicorn_conf_dir)
|
134
|
+
chown.(owner_uid, owner_gid, unicorn_conf_dir)
|
135
|
+
end
|
136
|
+
puts "Creating #{unicorn_conf_file}"
|
137
|
+
File.binwrite(unicorn_conf_file, <<END)
|
138
|
+
require 'unicorn-lockdown'
|
139
|
+
|
140
|
+
Unicorn.lockdown(self,
|
141
|
+
:app=>#{app.inspect},
|
142
|
+
|
143
|
+
# Update this with correct email
|
144
|
+
:email=>'root',
|
145
|
+
|
146
|
+
# More pledges may be needed depending on application
|
147
|
+
:pledge=>'rpath prot_exec inet unix flock',
|
148
|
+
:master_pledge=>'rpath prot_exec cpath wpath inet proc exec',
|
149
|
+
:master_execpledge=>'stdio rpath prot_exec inet unix cpath wpath unveil flock',
|
150
|
+
|
151
|
+
# More unveils may be needed depending on application
|
152
|
+
:unveil=>{
|
153
|
+
'views'=>'r'
|
154
|
+
},
|
155
|
+
:dev_unveil=>{
|
156
|
+
'models'=>'r'
|
157
|
+
},
|
158
|
+
)
|
159
|
+
END
|
160
|
+
setup_file_owner.call(unicorn_conf_file)
|
161
|
+
end
|
162
|
+
|
163
|
+
# Setup /etc/nginx/* file for nginx configuration
|
164
|
+
unless File.file?(nginx_file)
|
165
|
+
puts "Creating #{nginx_file}"
|
166
|
+
File.binwrite(nginx_file, <<END)
|
167
|
+
upstream #{app}_unicorn {
|
168
|
+
server unix:/sockets/#{app}.sock fail_timeout=0;
|
169
|
+
}
|
170
|
+
server {
|
171
|
+
server_name #{app};
|
172
|
+
access_log #{nginx_access_log_file} main;
|
173
|
+
error_log #{nginx_error_log_file} warn;
|
174
|
+
root #{dir}/public;
|
175
|
+
error_page 500 503 /500.html;
|
176
|
+
error_page 502 504 /502.html;
|
177
|
+
proxy_set_header X-Real-IP $remote_addr;
|
178
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
179
|
+
proxy_set_header Host $http_host;
|
180
|
+
proxy_redirect off;
|
181
|
+
add_header X-Content-Type-Options nosniff;
|
182
|
+
add_header X-Frame-Options deny;
|
183
|
+
add_header X-XSS-Protection "1; mode=block";
|
184
|
+
try_files $uri @#{app}_unicorn;
|
185
|
+
location @#{app}_unicorn {
|
186
|
+
proxy_pass http://#{app}_unicorn;
|
187
|
+
}
|
188
|
+
}
|
189
|
+
END
|
190
|
+
|
191
|
+
setup_file_owner.call(nginx_file)
|
192
|
+
end
|
193
|
+
|
194
|
+
# Setup nginx log file
|
195
|
+
[nginx_access_log_file, nginx_error_log_file].each do |f|
|
196
|
+
unless File.file?(f)
|
197
|
+
puts "Creating #{f}"
|
198
|
+
File.binwrite(f, '')
|
199
|
+
File.chmod(0644, f)
|
200
|
+
chown.(www_id, root_id, f)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
# Setup unicorn log file
|
205
|
+
unless File.file?(unicorn_log_file)
|
206
|
+
puts "Creating #{unicorn_log_file}"
|
207
|
+
File.binwrite(unicorn_log_file, '')
|
208
|
+
File.chmod(0640, unicorn_log_file)
|
209
|
+
chown.(app_uid, Etc.getgrnam('_unicorn').gid, unicorn_log_file)
|
210
|
+
end
|
211
|
+
|
212
|
+
# Setup /etc/rc.d/unicorn_* file for daemon management
|
213
|
+
unless File.file?(rc_file)
|
214
|
+
puts "Creating #{rc_file}"
|
215
|
+
File.binwrite(rc_file, <<END)
|
216
|
+
#!/bin/ksh
|
217
|
+
|
218
|
+
daemon_user=#{user}
|
219
|
+
unicorn_app=#{app}
|
220
|
+
unicorn_dir=#{dir}
|
221
|
+
#{unicorn}#{rackup}
|
222
|
+
. /etc/rc.d/rc.unicorn
|
223
|
+
END
|
224
|
+
|
225
|
+
File.chmod(0755, rc_file)
|
226
|
+
chown.(root_id, bin_id, rc_file)
|
227
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'etc'
|
2
|
+
|
3
|
+
prefix = ENV['UNICORN_LOCKDOWN_BIN_PREFIX']
|
4
|
+
request_dir = "#{prefix}/var/www/request-error-data"
|
5
|
+
socket_dir = "#{prefix}/var/www/sockets"
|
6
|
+
unicorn_log_dir = "#{prefix}/var/log/unicorn"
|
7
|
+
nginx_log_dir = "#{prefix}/var/log/nginx"
|
8
|
+
rc_unicorn_file = "#{prefix}/etc/rc.d/rc.unicorn"
|
9
|
+
|
10
|
+
unicorn_group = '_unicorn'
|
11
|
+
root_id = 0
|
12
|
+
daemon_id = 1
|
13
|
+
www_id = 67
|
14
|
+
|
15
|
+
if Process.uid == 0
|
16
|
+
# :nocov:
|
17
|
+
chown = lambda{|*a| File.chown(*a)}
|
18
|
+
# :nocov:
|
19
|
+
else
|
20
|
+
chown = lambda{|*a| }
|
21
|
+
end
|
22
|
+
|
23
|
+
# Add _unicorn group if it doesn't exist
|
24
|
+
group = begin
|
25
|
+
Etc.getgrnam(unicorn_group)
|
26
|
+
rescue ArgumentError
|
27
|
+
# :nocov:
|
28
|
+
args = ['groupadd', unicorn_group]
|
29
|
+
puts "Running: #{args.join(' ')}"
|
30
|
+
system(*args) || raise("Error while running: #{args.join(' ')}")
|
31
|
+
Etc.getgrnam(unicorn_group)
|
32
|
+
# :nocov:
|
33
|
+
end
|
34
|
+
unicorn_group_id = group.gid
|
35
|
+
|
36
|
+
# Setup requests directory to hold per-request information for crash notifications
|
37
|
+
unless File.directory?(request_dir)
|
38
|
+
puts "Creating #{request_dir}"
|
39
|
+
Dir.mkdir(request_dir)
|
40
|
+
File.chmod(0710, request_dir)
|
41
|
+
chown.(root_id, unicorn_group_id, request_dir)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Setup sockets directory to hold nginx connection sockets
|
45
|
+
unless File.directory?(socket_dir)
|
46
|
+
puts "Creating #{socket_dir}"
|
47
|
+
Dir.mkdir(socket_dir)
|
48
|
+
File.chmod(0770, socket_dir)
|
49
|
+
chown.(www_id, unicorn_group_id, socket_dir)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Setup log directory to hold unicorn application logs
|
53
|
+
unless File.directory?(unicorn_log_dir)
|
54
|
+
puts "Creating #{unicorn_log_dir}"
|
55
|
+
Dir.mkdir(unicorn_log_dir)
|
56
|
+
File.chmod(0755, unicorn_log_dir)
|
57
|
+
chown.(root_id, daemon_id, unicorn_log_dir)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Setup log directory to hold nginx logs
|
61
|
+
unless File.directory?(nginx_log_dir)
|
62
|
+
puts "Creating #{nginx_log_dir}"
|
63
|
+
Dir.mkdir(nginx_log_dir)
|
64
|
+
File.chmod(0775, nginx_log_dir)
|
65
|
+
chown.(www_id, root_id, nginx_log_dir)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Setup rc.unicorn file
|
69
|
+
unless File.file?(rc_unicorn_file)
|
70
|
+
puts "Creating #{rc_unicorn_file}"
|
71
|
+
File.binwrite(rc_unicorn_file, File.binread(File.join(File.dirname(__dir__), 'files', 'rc.unicorn')))
|
72
|
+
File.chmod(0644, rc_unicorn_file)
|
73
|
+
chown.(root_id, root_id, rc_unicorn_file)
|
74
|
+
end
|
@@ -36,7 +36,12 @@ ENV:
|
|
36
36
|
#{env.map{|k, v| "#{k.inspect} => #{v.inspect}"}.sort.join("\n")}
|
37
37
|
END
|
38
38
|
|
39
|
-
|
39
|
+
# :nocov:
|
40
|
+
# Don't verify localhost hostname, to avoid SSL errors raised in newer versions of net/smtp
|
41
|
+
smtp_params = Net::SMTP.method(:start).parameters.include?([:key, :tls_verify]) ? {tls_verify: false, tls_hostname: 'localhost'} : {}
|
42
|
+
# :nocov:
|
43
|
+
|
44
|
+
Net::SMTP.start('127.0.0.1', **smtp_params){|s| s.send_message(body, @email, @email)}
|
40
45
|
|
41
46
|
raise
|
42
47
|
end
|
@@ -8,32 +8,31 @@ 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.
|
18
17
|
module PgDisconnect
|
19
18
|
def self.load_dependencies(app)
|
20
|
-
raise RodaError, "error_handler plugin already loaded" if app.method_defined?(:handle_error)
|
19
|
+
raise RodaError, "error_handler plugin already loaded" if app.method_defined?(:handle_error) || app.private_method_defined?(:handle_error)
|
21
20
|
end
|
22
21
|
|
23
22
|
module InstanceMethods
|
24
|
-
#
|
25
|
-
#
|
26
|
-
# once the application is chrooted.
|
23
|
+
# :nocov:
|
24
|
+
# Handle old Roda dispatch API
|
27
25
|
def call
|
28
26
|
super
|
29
27
|
rescue Sequel::DatabaseDisconnectError, Sequel::DatabaseConnectionError, PG::ConnectionBad
|
30
28
|
Process.kill(:QUIT, $$)
|
31
29
|
raise
|
32
30
|
end
|
31
|
+
# :nocov:
|
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
|