From 42e0a07059bdafc9214492247351745e76a1ef2e Mon Sep 17 00:00:00 2001 From: UXabre Date: Thu, 4 Mar 2021 14:58:53 +0100 Subject: [PATCH] Support TLS certificate & key pair --- docs/index.asciidoc | 20 +++ .../elasticsearch/http_client_builder.rb | 16 ++- .../elasticsearch/api_configs.rb | 5 + spec/unit/outputs/elasticsearch_ssl_spec.rb | 115 ++++++++++++++++++ 4 files changed, 154 insertions(+), 2 deletions(-) diff --git a/docs/index.asciidoc b/docs/index.asciidoc index 031f2224e..15b46e92d 100644 --- a/docs/index.asciidoc +++ b/docs/index.asciidoc @@ -333,7 +333,9 @@ This plugin supports the following configuration options plus the | <> |<>|No | <> |<>|No | <> |<>|No +| <> |a valid filesystem path|No | <> |a valid filesystem path|No +| <> |a valid filesystem path|No | <> |<>|No | <> |<>|No | <> |<>|No @@ -738,6 +740,24 @@ Logstash uses http://www.joda.org/joda-time/apidocs/org/joda/time/format/DateTimeFormat.html[Joda formats] for the index pattern from event timestamp. +[id="plugins-{type}s-{plugin}-tls_certificate"] +===== `tls_certificate` + + * Value type is <> + * There is no default value for this setting. + +The tls_certificate used to present a client certificate to the server. +It accepts .pem formatted files + +[id="plugins-{type}s-{plugin}-tls_private_key"] +===== `tls_private_key` + + * Value type is <> + * There is no default value for this setting. + +The tls_private_key used to present a client private key to the server, to be used in conjunction with tls_certificate. +It accepts .pkcs8 formatted files + [id="plugins-{type}s-{plugin}-keystore"] ===== `keystore` diff --git a/lib/logstash/outputs/elasticsearch/http_client_builder.rb b/lib/logstash/outputs/elasticsearch/http_client_builder.rb index 457b41234..2a59dcd0f 100644 --- a/lib/logstash/outputs/elasticsearch/http_client_builder.rb +++ b/lib/logstash/outputs/elasticsearch/http_client_builder.rb @@ -112,13 +112,21 @@ def self.setup_ssl(logger, params) return {:ssl => {:enabled => false}} if params["ssl"] == false - cacert, truststore, truststore_password, keystore, keystore_password = - params.values_at('cacert', 'truststore', 'truststore_password', 'keystore', 'keystore_password') + cacert, truststore, truststore_password, keystore, keystore_password, tls_certificate, tls_private_key = + params.values_at('cacert', 'truststore', 'truststore_password', 'keystore', 'keystore_password', 'tls_certificate', 'tls_private_key') if cacert && truststore raise(LogStash::ConfigurationError, "Use either \"cacert\" or \"truststore\" when configuring the CA certificate") if truststore end + if tls_certificate && keystore + raise(LogStash::ConfigurationError, "Use either \"tls_certificate\" or \"keystore\" when configuring the client certificate") + end + + if (tls_private_key && !tls_certificate) || (tls_certificate && !tls_private_key) + raise(LogStash::ConfigurationError, "Both a \"tls_private_key\" and a \"tls_certificate\" need to be present") + end + ssl_options = {:enabled => true} if cacert @@ -131,7 +139,11 @@ def self.setup_ssl(logger, params) if keystore ssl_options[:keystore] = keystore ssl_options[:keystore_password] = keystore_password.value if keystore_password + elsif tls_certificate && tls_private_key + ssl_options[:client_cert] = tls_certificate + ssl_options[:client_key] = tls_private_key end + if !params["ssl_certificate_verification"] logger.warn [ "** WARNING ** Detected UNSAFE options in elasticsearch output configuration!", diff --git a/lib/logstash/plugin_mixins/elasticsearch/api_configs.rb b/lib/logstash/plugin_mixins/elasticsearch/api_configs.rb index 1721a0c00..789da6d73 100644 --- a/lib/logstash/plugin_mixins/elasticsearch/api_configs.rb +++ b/lib/logstash/plugin_mixins/elasticsearch/api_configs.rb @@ -66,6 +66,11 @@ module APIConfigs # Set the keystore password :keystore_password => { :validate => :password }, + # The certificate to present to the server. (only pem format supported) + :tls_certificate => { :validate => :path }, + # The private key to present to the server. (only pkcs8 format supported) + :tls_private_key => { :validate => :path }, + # This setting asks Elasticsearch for the list of all cluster nodes and adds them to the hosts list. # Note: This will return ALL nodes with HTTP enabled (including master nodes!). If you use # this with master nodes, you probably want to disable HTTP on them by setting diff --git a/spec/unit/outputs/elasticsearch_ssl_spec.rb b/spec/unit/outputs/elasticsearch_ssl_spec.rb index 587d0f4e1..aabcd8f2e 100644 --- a/spec/unit/outputs/elasticsearch_ssl_spec.rb +++ b/spec/unit/outputs/elasticsearch_ssl_spec.rb @@ -76,6 +76,121 @@ end.and_call_original subject.register end + end + + context "when using ssl with pem-encoded client certificates" do + let(:tls_certificate) { Stud::Temporary.file.path } + let(:tls_private_key) { Stud::Temporary.file.path } + before do + `openssl req -x509 -batch -nodes -newkey rsa:2048 -keyout #{tls_private_key} -out #{tls_certificate}` + end + + after :each do + File.delete(tls_certificate) + File.delete(tls_private_key) + subject.close + end + + subject do + settings = { + "hosts" => "node01", + "ssl" => true, + "tls_certificate" => tls_certificate, + "tls_private_key" => tls_private_key + } + next LogStash::Outputs::ElasticSearch.new(settings) + end + + it "should pass the pem certificate parameters to the ES client" do + expect(::Manticore::Client) + .to receive(:new) { |args| expect(args[:ssl]).to include(:client_cert => tls_certificate, :client_key => tls_private_key) } + .and_return(manticore_double) + subject.register + end end + + context "when using both pem-encoded and jks-encoded client certificates" do + let(:tls_certificate) { Stud::Temporary.file.path } + let(:tls_private_key) { Stud::Temporary.file.path } + before do + `openssl req -x509 -batch -nodes -newkey rsa:2048 -keyout #{tls_private_key} -out #{tls_certificate}` + end + + after :each do + File.delete(tls_private_key) + File.delete(tls_certificate) + subject.close + end + + subject do + settings = { + "hosts" => "node01", + "ssl" => true, + "tls_certificate" => tls_certificate, + "tls_private_key" => tls_private_key, + # just any file will do for this test + "keystore" => tls_certificate + } + next LogStash::Outputs::ElasticSearch.new(settings) + end + + it "should fail to load the plugin" do + expect { subject.register }.to raise_error(LogStash::ConfigurationError) + end + end + + context "when configuring only tls_certificate but ommitting the private_key" do + let(:tls_certificate) { Stud::Temporary.file.path } + let(:tls_private_key) { Stud::Temporary.file.path } + before do + `openssl req -x509 -batch -nodes -newkey rsa:2048 -keyout #{tls_private_key} -out #{tls_certificate}` + end + + after :each do + File.delete(tls_private_key) + File.delete(tls_certificate) + subject.close + end + + subject do + settings = { + "hosts" => "node01", + "ssl" => true, + "tls_certificate" => tls_certificate, + } + next LogStash::Outputs::ElasticSearch.new(settings) + end + + it "should fail to load the plugin" do + expect { subject.register }.to raise_error(LogStash::ConfigurationError) + end + end + + context "when configuring only private_key but ommitting the tls_certificate" do + let(:tls_certificate) { Stud::Temporary.file.path } + let(:tls_private_key) { Stud::Temporary.file.path } + before do + `openssl req -x509 -batch -nodes -newkey rsa:2048 -keyout #{tls_private_key} -out #{tls_certificate}` + end + + after :each do + File.delete(tls_private_key) + File.delete(tls_certificate) + subject.close + end + + subject do + settings = { + "hosts" => "node01", + "ssl" => true, + "tls_private_key" => tls_private_key, + } + next LogStash::Outputs::ElasticSearch.new(settings) + end + + it "should fail to load the plugin" do + expect { subject.register }.to raise_error(LogStash::ConfigurationError) + end + end end