How to harden rails+webrick+https with insecure ciphers removed on Ruby 2.2

↘锁芯ラ 提交于 2019-12-08 06:30:03

问题


Updated: At first, my test code didn't adequately show ruby 2.4 sees the :SSLCiphers option whereas ruby 2.2 does not. I have edited the example code below to make that clear.

Updated: Since my question failed to elicit any help from the community, I forged on ahead and two days later found the solution, which I have included below.

I have a small Rails 3 application on Ruby 2.2 and Webrick that handles small loads and therefore does not need the complexity of a "real" web server. It has been patched to support https connections for secure logins, but by default, it accepts many old weak Ciphers, which I want to prohibit. While upgrading ruby to 2.4 offers new hardening options to achieve this, I can't immediately make the switch to the newer ruby. Therefore, I have attempted to monkey-patch ruby 2.2's webrick/ssl.rb to add a couple of these options, :SSLVersion and :SSLCiphers. I was only partially successful: while :SSLVersion appears to work, :SSLCiphers does not.

First, the following example shows successful hardening on Ruby 2.4, based on the answer here https://stackoverflow.com/a/23283909/6588873 but also incorporating some of the advice found here: https://gist.github.com/tam7t/86eb4793e8ecf3f55037 except initially we'll be over-aggressive and exclude AES128 just to show the :SSLCiphers option works (as the version of openssl in Ruby 2.4 disables all SHA1 ciphers by default anyway).

#!/usr/bin/env ruby.exe
# script/rails:
require 'rails/commands/server'
require 'rack'
require 'webrick'
require 'webrick/https'

module Rails
     class Server < ::Rack::Server
         SSL_ENABLED=true
         def default_options
             # Don't use SSLv3
             no_ssl_3 = OpenSSL::SSL::OP_NO_SSLv3
             # Don't use SSLv2
             no_ssl_2 = OpenSSL::SSL::OP_NO_SSLv2
             # Don't use compression (CRIME CVE-2012-4929)
             no_ssl_compression = OpenSSL::SSL::OP_NO_COMPRESSION
             ssl_options = no_ssl_2 + no_ssl_3 + no_ssl_compression

             super.merge({
                 :Port => 3002,
                 :environment => (ENV['RAILS_ENV'] || "development").dup,
                 :daemonize => false,
                 :debugger => false,
                 :config => File.expand_path("config.ru"),
                 :SSLEnable => true,
                 :SSLVerifyClient => OpenSSL::SSL::VERIFY_NONE,
                 :SSLPrivateKey => OpenSSL::PKey::RSA.new(
                        File.open(File.expand_path("../cert/test.cert.key",__FILE__)).read),
                 :SSLCertificate => OpenSSL::X509::Certificate.new(
                        File.open(File.expand_path("../cert/test.cert.crt",__FILE__)).read),
                 :SSLCertName => [["CN", WEBrick::Utils::getservername]],
                 :SSLOptions => ssl_options,
                 :SSLCiphers => 'TLSv1.2:!aNULL:!eNULL:!AES128',
                 :SSLVersion => :TLSv1_2,
             })
         end
     end
end

APP_PATH = File.expand_path('../../config/application',  __FILE__)
require File.expand_path('../../config/boot',  __FILE__)
require 'rails/commands'

And here's an excerpt from sslscan output with the above patch applied, showing the short list of supported ciphers with insecure ciphers and obsolete TLS versions dropped (though they are dropped anyway by default, hence the !AES128 just for the proof of principle showing the option works).

  Supported Server Cipher(s):
Preferred TLSv1.2  256 bits  ECDHE-RSA-AES256-GCM-SHA384   Curve P-256 DHE 256
Accepted  TLSv1.2  256 bits  ECDHE-RSA-AES256-SHA384       Curve P-256 DHE 256
Accepted  TLSv1.2  256 bits  DHE-RSA-AES256-GCM-SHA384     DHE 1024 bits
Accepted  TLSv1.2  256 bits  DHE-RSA-AES256-SHA256         DHE 1024 bits
Accepted  TLSv1.2  256 bits  AES256-GCM-SHA384
Accepted  TLSv1.2  256 bits  AES256-SHA256

So next, I changed :SSLCiphers to the actual string I want, since I'm actually fine with AES128. I just want to exclude all the old SHA1 Ciphers that the older openssl doesn't exclude by default:

             :SSLCiphers => 'TLSv1.2:!aNULL:!eNULL!SHA',

And then prepared my backport, inserted just after the Rails::Server patch shown above. The following is lifted directly from Ruby 2.2's webrick/ssl.rb, but with the two new options from Ruby 2.4 added and passed through to ruby-openssl via WEBrick::GenericServer#setup_ssl_context():

if RUBY_VERSION < '2.4'
  module WEBrick
    module Config
      svrsoft = General[:ServerSoftware]
      osslv = ::OpenSSL::OPENSSL_VERSION.split[1]
      SSL = {
        :ServerSoftware       => "#{svrsoft} OpenSSL/#{osslv}",
        :SSLEnable            => false,
        :SSLCertificate       => nil,
        :SSLPrivateKey        => nil,
        :SSLClientCA          => nil,
        :SSLExtraChainCert    => nil,
        :SSLCACertificateFile => nil,
        :SSLCACertificatePath => nil,
        :SSLCertificateStore  => nil,
        :SSLTmpDhCallback     => nil,
        :SSLVerifyClient      => ::OpenSSL::SSL::VERIFY_NONE,
        :SSLVerifyDepth       => nil,
        :SSLVerifyCallback    => nil,   # custom verification
        :SSLTimeout           => nil,
        :SSLOptions           => nil,
        :SSLCiphers           => nil,
        :SSLVersion           => nil,
        :SSLStartImmediately  => true,
        # Must specify if you use auto generated certificate.
        :SSLCertName          => nil,
        :SSLCertComment       => "Generated by Ruby/OpenSSL"
      }
      General.update(SSL)
    end

    class GenericServer
      def setup_ssl_context(config) # :nodoc:
        unless config[:SSLCertificate]
          cn = config[:SSLCertName]
          comment = config[:SSLCertComment]
          cert, key = Utils::create_self_signed_cert(1024, cn, comment)
          config[:SSLCertificate] = cert
          config[:SSLPrivateKey] = key
        end
        ctx = OpenSSL::SSL::SSLContext.new
        ctx.key = config[:SSLPrivateKey]
        ctx.cert = config[:SSLCertificate]
        ctx.client_ca = config[:SSLClientCA]
        ctx.extra_chain_cert = config[:SSLExtraChainCert]
        ctx.ca_file = config[:SSLCACertificateFile]
        ctx.ca_path = config[:SSLCACertificatePath]
        ctx.cert_store = config[:SSLCertificateStore]
        ctx.tmp_dh_callback = config[:SSLTmpDhCallback]
        ctx.verify_mode = config[:SSLVerifyClient]
        ctx.verify_depth = config[:SSLVerifyDepth]
        ctx.verify_callback = config[:SSLVerifyCallback]
        ctx.timeout = config[:SSLTimeout]
        ctx.options = config[:SSLOptions]
        ctx.ciphers = config[:SSLCiphers]
        ctx.ssl_version = config[:SSLVersion]
        ctx
      end
    end
  end
end

When started, I see the following redefinition warnings. The first, I expected. The second I'm not sure of, but I think is OK:

script/rails:47: warning: already initialized constant WEBrick::Config::SSL
C:/Ruby226/lib/ruby/2.2.0/webrick/ssl.rb:62: warning: previous definition of SSL was here

Now, while sslscan now indicates the SSLVersion is being obeyed, (i.e. no TLS 1.0 or 1.1 are accepted,) the SSLCiphers option doesn't appear to be doing anything at all, i.e. SHA ciphers are still accepted:

  Supported Server Cipher(s):
Preferred TLSv1.2  256 bits  DHE-RSA-AES256-GCM-SHA384     DHE 1024 bits
Accepted  TLSv1.2  256 bits  DHE-RSA-AES256-SHA256         DHE 1024 bits
Accepted  TLSv1.2  256 bits  DHE-RSA-AES256-SHA            DHE 1024 bits
Accepted  TLSv1.2  256 bits  DHE-RSA-CAMELLIA256-SHA       DHE 1024 bits
Accepted  TLSv1.2  256 bits  AES256-GCM-SHA384
Accepted  TLSv1.2  256 bits  AES256-SHA256
Accepted  TLSv1.2  256 bits  AES256-SHA
Accepted  TLSv1.2  256 bits  CAMELLIA256-SHA
Accepted  TLSv1.2  128 bits  DHE-RSA-AES128-GCM-SHA256     DHE 1024 bits
Accepted  TLSv1.2  128 bits  DHE-RSA-AES128-SHA256         DHE 1024 bits
Accepted  TLSv1.2  128 bits  DHE-RSA-AES128-SHA            DHE 1024 bits
Accepted  TLSv1.2  128 bits  DHE-RSA-SEED-SHA              DHE 1024 bits
Accepted  TLSv1.2  128 bits  DHE-RSA-CAMELLIA128-SHA       DHE 1024 bits
Accepted  TLSv1.2  128 bits  AES128-GCM-SHA256
Accepted  TLSv1.2  128 bits  AES128-SHA256
Accepted  TLSv1.2  128 bits  AES128-SHA
Accepted  TLSv1.2  128 bits  SEED-SHA
Accepted  TLSv1.2  128 bits  CAMELLIA128-SHA

Whereas I expected:

  Supported Server Cipher(s):
Preferred TLSv1.2  256 bits  DHE-RSA-AES256-GCM-SHA384     DHE 1024 bits
Accepted  TLSv1.2  256 bits  DHE-RSA-AES256-SHA256         DHE 1024 bits
Accepted  TLSv1.2  256 bits  AES256-GCM-SHA384
Accepted  TLSv1.2  256 bits  AES256-SHA256
Accepted  TLSv1.2  128 bits  DHE-RSA-AES128-GCM-SHA256     DHE 1024 bits
Accepted  TLSv1.2  128 bits  DHE-RSA-AES128-SHA256         DHE 1024 bits
Accepted  TLSv1.2  128 bits  AES128-GCM-SHA256
Accepted  TLSv1.2  128 bits  AES128-SHA256

In fact, it doesn't matter what I change :SSLCiphers to, any valid or even invalid value. Clearly, just updating the ruby is not enough.

Where have I gone wrong with this backport? Is there anything else I can try to achieve the same result? I seem so close to a solution here, working within the constraints of our Ruby version and webrick, which would be difficult for me to move away from.

Solution:

I inserted debug print statements to see where the SSLCiphers value gets 'lost'. Although my testing showed everything was fine on the WEBrick side of things, in the OpenSSL classes, it appeared that when an SSL listener was set up, the wrong ciphers list was passed in (i.e. the default instead of the override).

When looking at the OpenSSL API for clues as to how to obtain more info about the failure, I found OpenSSL::SSL::SSLContext#set_params, a method taking a hash as an argument with any parameters to the SSLContext to set to new values. So I experimentally replaced my monkey-patched WEBrick::GenericServer#setup_ssl_context to use this and pass a hash of all values to change, instead replacing ctx.= config:[:] for each setting (lifted directly from the original WEBrick code; even in Ruby 2.4 this has not changed).

My outcome? Success! Using the alternate method of changing the SSLContext object, I was able to make the :SSLCiphers override value 'stick'

if RUBY_VERSION < '2.4'
  require 'webrick/ssl'

  module WEBrick
    module Config
      svrsoft = General[:ServerSoftware]
      osslv = ::OpenSSL::OPENSSL_VERSION.split[1]
      SSL = {
        :ServerSoftware       => "#{svrsoft} OpenSSL/#{osslv}",
        :SSLEnable            => false,
        :SSLCertificate       => nil,
        :SSLPrivateKey        => nil,
        :SSLClientCA          => nil,
        :SSLExtraChainCert    => nil,
        :SSLCACertificateFile => nil,
        :SSLCACertificatePath => nil,
        :SSLCertificateStore  => nil,
        :SSLTmpDhCallback     => nil,
        :SSLVerifyClient      => ::OpenSSL::SSL::VERIFY_NONE,
        :SSLVerifyDepth       => nil,
        :SSLVerifyCallback    => nil,   # custom verification
        :SSLTimeout           => nil,
        :SSLOptions           => nil,
        :SSLCiphers           => nil,
        :SSLVersion           => nil,
        :SSLStartImmediately  => true,
        # Must specify if you use auto generated certificate.
        :SSLCertName          => nil,
        :SSLCertComment       => "Generated by Ruby/OpenSSL"
      }
      General.update(SSL)
    end

    class GenericServer
      def setup_ssl_context(config) # :nodoc:
        unless config[:SSLCertificate]
          cn = config[:SSLCertName]
          comment = config[:SSLCertComment]
          cert, key = Utils::create_self_signed_cert(1024, cn, comment)
          config[:SSLCertificate] = cert
          config[:SSLPrivateKey] = key
        end
        ctx = OpenSSL::SSL::SSLContext.new
        ctx.set_params({
          key: config[:SSLPrivateKey],
          cert: config[:SSLCertificate],
          client_ca: config[:SSLClientCA],
          extra_chain_cert: config[:SSLExtraChainCert],
          ca_file: config[:SSLCACertificateFile],
          ca_path: config[:SSLCACertificatePath],
          cert_store: config[:SSLCertificateStore],
          tmp_dh_callback: config[:SSLTmpDhCallback],
          verify_mode: config[:SSLVerifyClient],
          verify_depth: config[:SSLVerifyDepth],
          verify_callback: config[:SSLVerifyCallback],
          timeout: config[:SSLTimeout],
          options: config[:SSLOptions],
          ciphers: config[:SSLCiphers],
          ssl_version: config[:SSLVersion],
        })
        ctx
      end
    end
  end
end

With this patch in place, sslscan output matches my expected output shown above.

来源:https://stackoverflow.com/questions/45301637/how-to-harden-railswebrickhttps-with-insecure-ciphers-removed-on-ruby-2-2

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!