— originally on blog.arkency.com

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:

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.