Rack apps mounted in Rails — how to protect access to them?
Rack apps mounted in Rails — how to protect access to them?
Sidekiq, Flipper, RailsEventStore — what do these Rails gems have in common? They all ship web apps with UI to enhance their usefulness in the application. Getting an overview of processed jobs, managing visibility of feature toggles, browsing events, their correlations and streams — nothing you could not do in code or Rails console already. But never really wanted to do there 😉
In Rails apps we add those UI apps with mount
in routing:
# config/routes.rb
Rails.application.routes.draw do
mount Sidekiq::Web, at: "/sidekiq"
end
In production application you'll want to protect access to this Sidekiq dashboard. Let's assume this Rails application is API-only. There's no Devise nor any other authentication library of your choice there. Fair scenario to rely on HTTP Basic Auth, as illustrated with wonderfully commented example from Sidekiq wiki:
# config/routes.rb
Rails.application.routes.draw do
Sidekiq::Web.use Rack::Auth::Basic do |username, password|
# Protect against timing attacks:
# - See https://codahale.com/a-lesson-in-timing-attacks/
# - See https://thisdata.com/blog/timing-attacks-against-string-comparison/
# - Use & (do not use &&) so that it doesn't short circuit.
# - Use digests to stop length information leaking (see also ActiveSupport::SecurityUtils.variable_size_secure_compare)
ActiveSupport::SecurityUtils.secure_compare(::Digest::SHA256.hexdigest(username), ::Digest::SHA256.hexdigest(ENV["SIDEKIQ_USERNAME"])) &
ActiveSupport::SecurityUtils.secure_compare(::Digest::SHA256.hexdigest(password), ::Digest::SHA256.hexdigest(ENV["SIDEKIQ_PASSWORD"]))
end
mount Sidekiq::Web, at: "/sidekiq"
end
Let's transform this example a bit to not rely on Sidekiq::Web.use
. That's very convenient to provide such interface from a library. I want to show you something else here — Sidekiq::Web
is a Rack application and can be treated as such.
# config/routes.rb
Rails.application.routes.draw do
mount Rack::Builder.new do
use Rack::Auth::Basic do |username, password|
ActiveSupport::SecurityUtils.secure_compare(::Digest::SHA256.hexdigest(username), ::Digest::SHA256.hexdigest(ENV.fetch("DEV_UI_USERNAME"))) &
ActiveSupport::SecurityUtils.secure_compare(::Digest::SHA256.hexdigest(password), ::Digest::SHA256.hexdigest(ENV.fetch("DEV_UI_PASSWORD")))
end
run Sidekiq::Web
end, at: "/sidekiq"
end
Little explanation:
- Rack::Builder is a DSL to create new Rack application
- Rack applications can wrap each other, like layers
- when the request comes in, a chain of Rack apps is executed from outermost to innermost
- when the response is to be returned, it goes from innermost Rack app to outermost
Right. Wasn't it supposed to be about protecting access?
Imagine now that aforementioned Rails application includes all those UIs for Sidekiq, Flipper, RailsEventStore at the same time. How can we have common protection for them without boring copying and pasting same wrapper again and again?
Let's extract (bad word detected) a factory!
# config/routes.rb
Rails.application.routes.draw do
with_dev_auth =
lambda do |app|
Rack::Builder.new do
use Rack::Auth::Basic do |username, password|
ActiveSupport::SecurityUtils.secure_compare(::Digest::SHA256.hexdigest(username), ::Digest::SHA256.hexdigest(ENV.fetch("DEV_UI_USERNAME"))) &
ActiveSupport::SecurityUtils.secure_compare(::Digest::SHA256.hexdigest(password), ::Digest::SHA256.hexdigest(ENV.fetch("DEV_UI_PASSWORD")))
end
run app
end
end
mount with_dev_auth.call(Sidekiq::Web), at: "sidekiq"
end
Fun fact is that a Proc#[]
is an equivalent to Proc#call
.
The last line can be as well written as:
mount with_dev_auth[Sidekiq::Web], at: "sidekiq"
And with all those UIs in place we receive:
mount with_dev_auth[Sidekiq::Web], at: "/sidekiq"
mount with_dev_auth[RailsEventStore::Browser], at: "/res"
mount with_dev_auth[Flipper::UI.app(Flipper)], at: "/flipper"
In the future we could swap Basic Auth to one or another authentication mechanism. The with_dev_auth
factory would remain useful and probably survive them all.