Class: Unicorn
- Inherits:
-
Object
- Object
- Unicorn
- Defined in:
- lib/unicorn-lockdown.rb
Class Attribute Summary collapse
-
.app_name ⇒ Object
The name of the application.
-
.dev_unveil ⇒ Object
The hash of additional unveil paths to use if in the development environment.
-
.email ⇒ Object
The address to email for crash and unhandled exception notifications.
-
.group_name ⇒ Object
The group name to run as.
-
.pledge ⇒ Object
The pledge string to use.
-
.request_logger ⇒ Object
A File instance open for writing.
-
.server ⇒ Object
The Unicorn::HttpServer instance in use.
-
.unveil ⇒ Object
The hash of unveil paths to use.
-
.user_name ⇒ Object
The user to run as.
Class Method Summary collapse
-
.lockdown(configurator, opts) ⇒ Object
Helper method that sets up all necessary code for chroot/pledge support.
-
.write_request(email_message) ⇒ Object
Helper method to write request information to the request logger.
Class Attribute Details
.app_name ⇒ Object
The name of the application. All applications are given unique names. This name is used to construct the log file, listening socket, and process name.
43 44 45 |
# File 'lib/unicorn-lockdown.rb', line 43 def app_name @app_name end |
.dev_unveil ⇒ Object
The hash of additional unveil paths to use if in the development environment.
66 67 68 |
# File 'lib/unicorn-lockdown.rb', line 66 def dev_unveil @dev_unveil end |
.email ⇒ Object
The address to email for crash and unhandled exception notifications
69 70 71 |
# File 'lib/unicorn-lockdown.rb', line 69 def email @email end |
.group_name ⇒ Object
The group name to run as. Can be an array of two strings, where the first string is the primary group, and the second string is the group used for the log files.
57 58 59 |
# File 'lib/unicorn-lockdown.rb', line 57 def group_name @group_name end |
.pledge ⇒ Object
The pledge string to use.
60 61 62 |
# File 'lib/unicorn-lockdown.rb', line 60 def pledge @pledge end |
.request_logger ⇒ Object
A File instance open for writing. This is unique per worker process. Workers should write all new requests to this file before handling the request. If a worker process crashes, the master process will send an notification email with the previously logged request information, to enable programmers to debug and fix the issue.
50 51 52 |
# File 'lib/unicorn-lockdown.rb', line 50 def request_logger @request_logger end |
.server ⇒ Object
The Unicorn::HttpServer instance in use. This is only set once when the unicorn server is started, before forking the first worker.
38 39 40 |
# File 'lib/unicorn-lockdown.rb', line 38 def server @server end |
.unveil ⇒ Object
The hash of unveil paths to use.
63 64 65 |
# File 'lib/unicorn-lockdown.rb', line 63 def unveil @unveil end |
.user_name ⇒ Object
The user to run as. Also specifies the group to run as if group_name is not set.
53 54 55 |
# File 'lib/unicorn-lockdown.rb', line 53 def user_name @user_name end |
Class Method Details
.lockdown(configurator, opts) ⇒ Object
Helper method that sets up all necessary code for chroot/pledge support. This should be called inside the appropriate unicorn.conf file. The configurator should be self in the top level scope of the unicorn.conf file, and this takes options:
Options:
- :app
-
The name of the application (required)
:email : The email to notify for worker crashes
- :user
-
The user to run as (required)
- :group
-
The group to run as (if not set, uses :user as the group). Can be an array of two strings, where the first string is the primary group, and the second string is the group used for the log files.
- :pledge
-
The string to use when pledging
- :unveil
-
A hash of unveil paths
- :dev_unveil
-
A hash of unveil paths to use in development
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 |
# File 'lib/unicorn-lockdown.rb', line 97 def lockdown(configurator, opts) Unicorn.app_name = opts.fetch(:app) Unicorn.user_name = opts.fetch(:user) Unicorn.group_name = opts[:group] || opts[:user] Unicorn.email = opts[:email] Unicorn.pledge = opts[:pledge] Unicorn.unveil = opts[:unveil] Unicorn.dev_unveil = opts[:dev_unveil] configurator.instance_exec do listen "/var/www/sockets/#{Unicorn.app_name}.sock" # Buffer all client bodies in memory. This assumes an Nginx limit of 10MB, # by using 11MB this ensures that client bodies are always buffered in # memory, preventing file uploading causing a program crash if the # pledge does not allow wpath and cpath. client_body_buffer_size(11*1024*1024) # Run all worker processes with unique memory layouts worker_exec true # Only change the log path if daemonizing. # Otherwise, continue to log to stdout/stderr. if Unicorn::Configurator::RACKUP[:daemonize] stdout_path "/var/log/unicorn/#{Unicorn.app_name}.log" stderr_path "/var/log/unicorn/#{Unicorn.app_name}.log" end after_fork do |server, worker| server.logger.info("worker=#{worker.nr} spawned pid=#{$$}") # Set the request logger for the worker process after forking. The # process is still root here, so it can open the file in write mode. Unicorn.request_logger = File.open(server.request_filename($$), "wb") Unicorn.request_logger.sync = true end if wrap_app = Unicorn.email && ENV['RACK_ENV'] == 'production' require 'rack/email_exceptions' end after_worker_ready do |server, worker| server.logger.info("worker=#{worker.nr} ready") # If an notification email address is setup, wrap the entire app in # a middleware that will notify about any exceptions raised when # processing that aren't caught by other middleware. if wrap_app server.instance_exec do @app = Rack::EmailExceptions.new(@app, Unicorn.app_name, Unicorn.email) end end if unveil = Unicorn.unveil require 'unveil' unveil = if Unicorn.dev_unveil && ENV['RACK_ENV'] == 'development' unveil.merge(Unicorn.dev_unveil) else Hash[unveil] end # Allow read access to the rack gem directory, as rack autoloads constants. unveil['rack'] = :gem if defined?(Mail) # If using the mail library, allow read access to the mail gem directory, # as mail autoloads constants. unveil['mail'] = :gem end # Drop privileges worker.user(Unicorn.user_name, Unicorn.group_name) # Restrict access to the file system based on the specified unveil. Pledge.unveil(unveil) else # Before chrooting, reference all constants that use autoload # that are probably needed at runtime. This must be done # before chrooting as attempting to load the constants after # chrooting will break things. # Start with rack, which uses autoload for all constants. # Most of rack's constants are not used at runtime, this # lists the ones most commonly needed. Rack::Multipart Rack::Multipart::Parser Rack::Multipart::Generator Rack::Multipart::UploadedFile Rack::Mime Rack::Auth::Digest::Params # In the development environment, reference all middleware # the unicorn will load by default, unless unicorn is # set to not load middleware by default. if ENV['RACK_ENV'] == 'development' && (!respond_to?(:set) || set[:default_middleware] != false) Rack::ContentLength Rack::CommonLogger Rack::Chunked Rack::Lint Rack::ShowExceptions Rack::TempfileReaper end # If using the mail library, eagerly autoload all constants. # This costs about 9MB of memory, but the mail gem changes # their autoloaded constants on a regular basis, so it's # better to be safe than sorry. if defined?(Mail) Mail.eager_autoload! end # Strip path prefixes from the reloader. This is only # really need in development mode for code reloading to work. pwd = Dir.pwd Unreloader.strip_path_prefix(pwd) if defined?(Unreloader) # Drop privileges. This must be done after chrooting as # chrooting requires root privileges. worker.user(Unicorn.user_name, Unicorn.group_name, pwd) end if Unicorn.pledge # Pledge after dropping privileges, because dropping # privileges requires a separate pledge. Pledge.pledge(Unicorn.pledge) end end # the last time there was a worker crash and the request information # file was empty. Set by default to 10 minutes ago, so the first # crash will always receive an email. last_empty_crash = Time.now - 600 after_worker_exit do |server, worker, status| m = "reaped #{status.inspect} worker=#{worker.nr rescue 'unknown'}" if status.success? server.logger.info(m) else server.logger.error(m) end # Email about worker process crashes. This is necessary so that # programmers are notified about any pledge violations. Pledge # violations immediately abort the process, and are bugs in the # application that should be fixed. This can also catch other # crashes such as SIGSEGV or SIGBUS. file = server.request_filename(status.pid) if File.exist?(file) if !status.success? && Unicorn.email if File.size(file).zero? # If a crash happens and the request information file is empty, # it is generally because the crash happened during initialization, # in which case it will generally continue to crash in a loop until the # problem is fixed. In that case, only send an email if there hasn't # been a similar crash in the last 5 minutes. This rate-limits the # crash notification emails to 1 every 5 minutes instead of potentially # multiple times per second. if Time.now - last_empty_crash > 300 last_empty_crash = Time.now else skip_email = true end end unless skip_email # If the request filename exists and the worker process crashed, # send a notification email. Process.waitpid(fork do # Load net/smtp early, before chrooting. require 'net/smtp' # When setting the email, first get the contents of the email # from the request file. body = File.read(file) # Then get information from /etc and drop group privileges uid = Etc.getpwnam(Unicorn.user_name).uid group = Unicorn.group_name group = group.first if group.is_a?(Array) gid = Etc.getgrnam(group).gid if gid && Process.egid != gid Process.initgroups(Unicorn.user_name, gid) Process::GID.change_privilege(gid) end # Then chroot Dir.chroot(Dir.pwd) Dir.chdir('/') # Then drop user privileges Process.euid != uid and Process::UID.change_privilege(uid) # Then use a restrictive pledge Pledge.pledge('inet prot_exec') # If body empty, crash happened before a request was received, # try to at least provide the application name in this case. if body.empty? body = "Subject: [#{Unicorn.app_name}] Unicorn Worker Process Crash\r\n\r\nNo email content provided for app: #{Unicorn.app_name}" end # Finally send an email to localhost via SMTP. Net::SMTP.start('127.0.0.1'){|s| s.(body, Unicorn.email, Unicorn.email)} end) end end # Remove any request logger file if it exists. File.delete(file) end end end end |
.write_request(email_message) ⇒ Object
Helper method to write request information to the request logger. email_message
should be an email message including headers and body. This should be called at the top of the Roda route block for the application.
75 76 77 78 79 80 |
# File 'lib/unicorn-lockdown.rb', line 75 def write_request() request_logger.seek(0, IO::SEEK_SET) request_logger.truncate(0) request_logger.syswrite() request_logger.fsync end |