How can I password-protect my /sidekiq route (i.e. require authentication for the Sidekiq::Web tool)?
Asked Answered
F

10

64

I am using sidekiq in my rails application. By Default, Sidekiq can be accessed by anybody by appending "/sidekiq" after the url. I want to password protect / authenticate only the sidekiq part. How can i do that?

Filariasis answered 4/9, 2012 at 14:16 Comment(0)
S
122

Put the following into your sidekiq initializer

require 'sidekiq'
require 'sidekiq/web'

Sidekiq::Web.use(Rack::Auth::Basic) do |user, 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
  Rack::Utils.secure_compare(::Digest::SHA256.hexdigest(user), ::Digest::SHA256.hexdigest(ENV["SIDEKIQ_USER"])) &
  Rack::Utils.secure_compare(::Digest::SHA256.hexdigest(password), ::Digest::SHA256.hexdigest(ENV["SIDEKIQ_PASSWORD"]))
end

And in the routes file:

mount Sidekiq::Web => '/sidekiq'
Skidway answered 16/11, 2012 at 2:15 Comment(8)
BTW-I put the following in my Gemfile so I wouldn't have to manually require sidekiq and sidekiq/web in both the initializer and the routes.rb file: "gem 'sidekiq', require: ['sidekiq', 'sidekiq/web']"Deme
At least in my app running Rails 5, it wasn't necessary for me to wrap the mount call with the authenticate block.Ant
As documented on Sidekiq project, you probably want to use ActiveSupport::SecurityUtils.secure_compare(::Digest::SHA256.hexdigest(user), ::Digest::SHA256.hexdigest(ENV["SIDEKIQ_USER"])) & ActiveSupport::SecurityUtils.secure_compare(::Digest::SHA256.hexdigest(password), ::Digest::SHA256.hexdigest(ENV["SIDEKIQ_PASSWORD"])) to compare username and password.Trakas
WARNING! Using == operator exposes the application to timing attacks. Any attacker can eventually find out what the password is by guessing each character one by one based on request processing times. Use ActiveSupport::SecurityUtils.secure_compare, Devise.secure_compare, or alike.Lederer
You dont need authenticate block. If you are not using devise, it gives error on rails 6Karlmarxstadt
You dont need authenticate block for Rails 5 neither.Huntsville
Edited out the authenticate block just now. I can personally vouch it's not needed as I have this solution working in a couple of Rails 6 apps already.Singletree
If you're using ENV['SIDEKIQ_USER'] or Rails.application.credentials.dig(:sidekiq, :username), you run the risk of not having set the environment variable or included the master key in production. This scenario can lead to getting the error TypeError (no implicit conversion of nil into String). Instead, use ENV.fetch('SIDEKIQ_USER') or Rails.application.credentials.fetch(:sidekiq).fetch(:username) to ensure a missing value raises a more helpful error message.Suzette
D
32

Sorry to late to the party, but Sidekiq's wiki recommends the following for Devise:

To allow any authenticated User:

# config/routes.rb
authenticate :user do
  mount Sidekiq::Web => '/sidekiq'
end

To restrict access to User.admin?

# config/routes.rb
authenticate :user, lambda { |u| u.admin? } do
  mount Sidekiq::Web => '/sidekiq'
end

This wiki post also has many other security schemes.

This was tested using Rails 5.1.3, Devise 4.3 and Sidekiq 5.0

Doornail answered 23/10, 2017 at 14:19 Comment(4)
Thanks for the links to the good practices on the Sidekiq wiki, Tom!Occident
Any documentation to show how to RSpec test this?Lucy
@tomAranda did you every figure out how to rspec test this?Lucy
I have written an rspec for this and I don't plan to in the near future.Doornail
T
15

See "Security" under https://github.com/mperham/sidekiq/wiki/Monitoring

Sidekiq::Web uses Rack::Protection to protect your application against typical web attacks (such as CSRF, XSS, etc). Rack::Protection would invalidate your session and raise Forbidden error if it finds that your request doesn't satisfy security requirements. One of the possible situations is having your application working behind a reverse proxy and not passing important headers to it (X-Forwarded-For,X-Forwarded-Proto). Such situation and solution could be found in this article and issue #2560...

Trickery answered 4/9, 2012 at 15:30 Comment(0)
S
13

If you are rolling your own custom authentication, then you can use the below example which is referenced in the docs here.

# lib/admin_constraint.rb
class AdminConstraint
  def matches?(request)
    return false unless request.session[:user_id]
    user = User.find request.session[:user_id]
    user && user.admin?
  end
end

# config/routes.rb
require 'sidekiq/web'
require 'admin_constraint'
mount Sidekiq::Web => '/sidekiq', :constraints => AdminConstraint.new
Sheena answered 14/9, 2016 at 18:8 Comment(2)
This answer was helpful although I'm sure if we can avoid the extra queries each time sidekiq/stats is requested which is quite often at the moment.Pugging
In rails 7 I'm seeing that the constant won't be findable by default unless it's in /app. That is, app/constraints/Blither
S
11

If you're using Devise (or other Warden-based authentication), you can do this, supposing you have an AdminUser model in your app.

# config/routes.rb
# This defines the authentication constraint
constraint = lambda do |request|
               request.env['warden'].authenticate!({ scope: :admin_user })
             end

# This mounts the route using the constraint.
# You could use any other path to make it less obvious
constraints constraint do
  mount Sidekiq::Web => '/sidekiq'
end
Subfamily answered 17/2, 2015 at 21:36 Comment(1)
can I put this constraint at any other place? I mean instead of putting it in routes.rbSludgy
T
7

The accepted answer is good, but I think that it can be implemented more securely, as Sidekiq documentation mentions (it got edited to demonstrate the right solution after I posted).

To protect your app against timing attacks, use ActiveSupport::SecurityUtils.secure_compare.

Also, use & (do not use &&) so that it doesn't short circuit.

And finally, use digests to stop length information leaking (default of secure_compare in Active Support 5).

So, in an initializer file, typically in config/initializers/sidekiq.rb in Rails projects, depending of your version of Active Support/Rails, write the following.

Active Support 5+: Thanks to Rails PR #24510, parameters passed to secure_compare are going through Digest::SHA256.hexdigest by default.

require 'active_support/security_utils'
require 'sidekiq'
require 'sidekiq/web'

Sidekiq::Web.use(Rack::Auth::Basic) do |user, 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
  ActiveSupport::SecurityUtils.secure_compare(user, ENV["SIDEKIQ_ADMIN_USER"]) &
    ActiveSupport::SecurityUtils.secure_compare(password, ENV["SIDEKIQ_ADMIN_PASSWORD"])
end

Active Support 4:

require 'active_support/security_utils'
require 'sidekiq'
require 'sidekiq/web'

Sidekiq::Web.use(Rack::Auth::Basic) do |user, 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
  ActiveSupport::SecurityUtils.secure_compare(
    ::Digest::SHA256.hexdigest(user),
    ::Digest::SHA256.hexdigest(ENV["SIDEKIQ_ADMIN_USER"])
  ) &
    ActiveSupport::SecurityUtils.secure_compare(
      ::Digest::SHA256.hexdigest(password),
      ::Digest::SHA256.hexdigest(ENV["SIDEKIQ_ADMIN_PASSWORD"])
    )
end
Trakas answered 25/1, 2019 at 17:53 Comment(0)
A
1

If you're using Sorcery for authentication, here's how to use Rails routes constraints to protect certain routes.


Copied here from the sorcery wiki for redundancy:

This tutorial shows how to use Rails routes constraints with Sorcery gem. Thanks to @anthonator for writing it!

First, define UserConstraint module that will be used for all constraints:

module RouteConstraints::UserConstraint
  def current_user(request)
    User.find_by_id(request.session[:user_id])
  end
end

Then, having that module defined, you can specify specific constraint classes. In these examples, first route will work only if there's no user logged in, the second will work only for logged user who is an admin:

class RouteConstraints::NoUserRequiredConstraint
  include RouteConstraints::UserConstraint

  def matches?(request)
    !current_user(request).present?
  end
end

class RouteConstraints::AdminRequiredConstraint
  include RouteConstraints::UserConstraint

  def matches?(request)
    user = current_user(request)
    user.present? && user.is_admin?
  end
end

Finally, you can add the constraints to the config/routes.rb:

MyApp::Application.routes.draw do

  # other routes …

  root :to => 'admin#dashboard', :constraints => RouteConstraints::AdminRequiredConstraint.new
  root :to => 'home#welcome', :constraints => RouteConstraints::NoUserRequiredConstraint.new

end
Amarillo answered 7/5, 2015 at 15:28 Comment(0)
P
0

Another option would be to add something like CanCan and special access based on roles.

Portraitist answered 7/4, 2015 at 17:19 Comment(0)
K
0

with ability to use devise username/password

this will also work for the APIs

#config/initializers/sidekiq.rb

require "sidekiq/web"

Sidekiq::Web.use Rack::Auth::Basic do |username, password|
    User.find_by(username: username)&.valid_password?(password)
end

#config/routes.rb

mount Sidekiq::Web => "/sidekiq"
Kemberlykemble answered 12/9, 2023 at 17:0 Comment(0)
S
0

With active admin write this in config/routes.rb file

  authenticate :admin_user do
    mount Sidekiq::Web => '/sidekiq'
  end
Stupefacient answered 5/12, 2023 at 12:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.