diff --git a/lib/logstash/patches.rb b/lib/logstash/patches.rb index b3ec2eeee..f33c91c5c 100644 --- a/lib/logstash/patches.rb +++ b/lib/logstash/patches.rb @@ -1,3 +1,4 @@ require "logstash/patches/bugfix_jruby_2558" require "logstash/patches/cabin" require "logstash/patches/profile_require_calls" +require "logstash/patches/stronger_openssl_defaults" diff --git a/lib/logstash/patches/stronger_openssl_defaults.rb b/lib/logstash/patches/stronger_openssl_defaults.rb new file mode 100644 index 000000000..323b25ba0 --- /dev/null +++ b/lib/logstash/patches/stronger_openssl_defaults.rb @@ -0,0 +1,62 @@ + +require "openssl" + +# :nodoc: +class OpenSSL::SSL::SSLContext + # Wrap SSLContext.new to a stronger default settings. + class << self + alias_method :orig_new, :new + def new(*args) + c = orig_new(*args) + + # MRI nor JRuby seem to actually invoke `SSLContext#set_params` by + # default, which makes the default ciphers (and other settings) not + # actually defaults. Oops! + # To force this, and force our (hopefully more secure) defaults on + # all things using openssl in Ruby, we will invoke set_params + # on all new SSLContext objects. + c.set_params + c + end + end + + # This cipher selection comes from https://wiki.mozilla.org/Security/Server_Side_TLS + MOZILLA_INTERMEDIATE_CIPHERS = "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA" + + # Returns the value that should be used for the default SSLContext options + # + # This is a method instead of a constant because some constants (like + # OpenSSL::SSL::OP_NO_COMPRESSION) may not be available in all Ruby + # versions/platforms. + def self.__default_options + # ruby-core is refusing to patch ruby's default openssl settings to be more + # secure, so let's fix that here. The next few lines setting options and + # ciphers come from jmhodges' proposed patch + ssloptions = OpenSSL::SSL::OP_ALL + + # TODO(sissel): JRuby doesn't have this. Maybe work on a fix? + if defined?(OpenSSL::SSL::OP_DONT_INSERT_EMPTY_FRAGMENTS) + ssloptions &= ~OpenSSL::SSL::OP_DONT_INSERT_EMPTY_FRAGMENTS + end + + # TODO(sissel): JRuby doesn't have this. Maybe work on a fix? + if defined?(OpenSSL::SSL::OP_NO_COMPRESSION) + ssloptions |= OpenSSL::SSL::OP_NO_COMPRESSION + end + + # Disable SSLv2 and SSLv3. They are insecure and highly discouraged. + ssloptions |= OpenSSL::SSL::OP_NO_SSLv2 if defined?(OpenSSL::SSL::OP_NO_SSLv2) + ssloptions |= OpenSSL::SSL::OP_NO_SSLv3 if defined?(OpenSSL::SSL::OP_NO_SSLv3) + ssloptions + end + + # Overwriting the DEFAULT_PARAMS const idea from here: https://www.ruby-lang.org/en/news/2014/10/27/changing-default-settings-of-ext-openssl/ + remove_const(:DEFAULT_PARAMS) if const_defined?(:DEFAULT_PARAMS) + DEFAULT_PARAMS = { + :ssl_version => "SSLv23", + :verify_mode => OpenSSL::SSL::VERIFY_PEER, + :ciphers => MOZILLA_INTERMEDIATE_CIPHERS, + :options => __default_options # Not a constant because it's computed at start-time. + } + +end diff --git a/spec/logstash/patches_spec.rb b/spec/logstash/patches_spec.rb new file mode 100644 index 000000000..89fa987a4 --- /dev/null +++ b/spec/logstash/patches_spec.rb @@ -0,0 +1,25 @@ +require "logstash/patches" + +describe "OpenSSL defaults" do + subject { OpenSSL::SSL::SSLContext.new } + + # OpenSSL::SSL::SSLContext#ciphers returns an array of + # [ [ ciphername, version, bits, alg_bits ], [ ... ], ... ] + + # List of cipher names + let(:ciphers) { subject.ciphers.map(&:first) } + + # List of cipher encryption bit strength. + let(:encryption_bits) { subject.ciphers.map { |_, _, _, a| a } } + + it "should not include any export ciphers" do + # SSLContext#ciphers returns an array of [ciphername, tlsversion, key_bits, alg_bits] + # Let's just check the cipher names + expect(ciphers).not_to be_any { |name| name =~ /EXPORT/ || name =~ /^EXP/ } + end + + it "should not include any weak ciphers (w/ less than 128 bits in encryption algorithm)" do + # SSLContext#ciphers returns an array of [ciphername, tlsversion, key_bits, alg_bits] + expect(encryption_bits).not_to be_any { |bits| bits < 128 } + end +end