logstash-input-tcp 2.0.5 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5c7064c954a185e8f91e06bd314abb3daacc80d8
4
- data.tar.gz: ca2dd0c7f7fdac14a7ea33027a62bd3dccfcc497
3
+ metadata.gz: bc8a144097d1b6d3ee89c67da0a9598c20816dc0
4
+ data.tar.gz: db3e1d7399ee28bcdbad61d343c589249287ad3d
5
5
  SHA512:
6
- metadata.gz: b7ca6d3d05531210c046bf3bb4e093839679434721c0d6e5d5bacc2f881786322689af56c296dd312daabd51febf615c20b6710f41dee00d5252bcbcaf3e45af
7
- data.tar.gz: 6f37a0e1cf9c291e00fa4d6c0f24eb008375fc961c931ec3bcb345e2256a5f274cf2ce64d0198f28a7befd7c0e51e4cb594635929beefc7f6685c8a501297fa5
6
+ metadata.gz: d8b9f22e646a94b0624d961075589d48899595e9aaedb4111fcc1cf6118a9173366f3caf6f7b0315c8048d0a6d850ee6da0319eea9f413413d5a6d2511d09c12
7
+ data.tar.gz: a5d0c419104ca62126ad38c4e3a0a83a81083df5febe5184a4c7edff44893a9b4141589830a99bbfaff365652cbcb43f8cdeb11e88d3fe6b341e36b32911a175
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## 3.0.0
2
+ - Deprecate ssl_cacert as it's confusing, does it job but when willing to add a chain of certificated the name and behaviour is a bit confusing.
3
+ - Add ssl_extra_chain_certs that allows you to specify a list of certificates path that will be added to the CAStore.
4
+ - Make ssl_verify=true as a default value, if using ssl and performing validation is not reasonable as security might be compromised.
5
+ - Add tests to verify behaviour under different SSL connection circumstances.
6
+ - Fixes #3 and #9.
7
+
8
+ ## 2.1.0
9
+ - Added the receiving port in the event payload, fixes #4
10
+
1
11
  ## 2.0.5
2
12
  - Fixed malformed SSL crashing Logstash, see https://github.com/logstash-plugins/logstash-input-tcp/pull/25
3
13
 
@@ -35,10 +35,10 @@ class LogStash::Inputs::Tcp < LogStash::Inputs::Base
35
35
 
36
36
  # Verify the identity of the other end of the SSL connection against the CA.
37
37
  # For input, sets the field `sslsubject` to that of the client certificate.
38
- config :ssl_verify, :validate => :boolean, :default => false
38
+ config :ssl_verify, :validate => :boolean, :default => true
39
39
 
40
40
  # The SSL CA certificate, chainfile or CA path. The system CA path is automatically included.
41
- config :ssl_cacert, :validate => :path
41
+ config :ssl_cacert, :validate => :path, :deprecated => "This setting is deprecated in favor of extra_chain_cert as it sets a more clear expectation to add more X509 certificates to the store"
42
42
 
43
43
  # SSL certificate path
44
44
  config :ssl_cert, :validate => :path
@@ -49,6 +49,10 @@ class LogStash::Inputs::Tcp < LogStash::Inputs::Base
49
49
  # SSL key passphrase
50
50
  config :ssl_key_passphrase, :validate => :password, :default => nil
51
51
 
52
+ # An Array of extra X509 certificates to be added to the certificate chain.
53
+ # Useful when the CA chain is not necessary in the system store.
54
+ config :ssl_extra_chain_certs, :validate => :array, :default => []
55
+
52
56
  def initialize(*args)
53
57
  super(*args)
54
58
 
@@ -126,7 +130,7 @@ class LogStash::Inputs::Tcp < LogStash::Inputs::Base
126
130
  def run_client(output_queue)
127
131
  while !stop?
128
132
  self.client_socket = new_client_socket
129
- handle_socket(client_socket, client_socket.peeraddr[3], output_queue, @codec.clone)
133
+ handle_socket(client_socket, client_socket.peeraddr[3], client_socket.peeraddr[1], output_queue, @codec.clone)
130
134
  end
131
135
  ensure
132
136
  # catch all rescue nil on close to discard any close errors or invalid socket
@@ -137,17 +141,18 @@ class LogStash::Inputs::Tcp < LogStash::Inputs::Base
137
141
  Thread.new(output_queue, socket) do |q, s|
138
142
  begin
139
143
  @logger.debug? && @logger.debug("Accepted connection", :client => s.peer, :server => "#{@host}:#{@port}")
140
- handle_socket(s, s.peeraddr[3], q, @codec.clone)
144
+ handle_socket(s, s.peeraddr[3], s.peeraddr[1], q, @codec.clone)
141
145
  ensure
142
146
  delete_connection_socket(s)
143
147
  end
144
148
  end
145
149
  end
146
150
 
147
- def handle_socket(socket, client_address, output_queue, codec)
151
+ def handle_socket(socket, client_address, client_port, output_queue, codec)
148
152
  while !stop?
149
153
  codec.decode(read(socket)) do |event|
150
154
  event["host"] ||= client_address
155
+ event["port"] ||= client_port
151
156
  event["sslsubject"] ||= socket.peer_cert.subject if @ssl_enable && @ssl_verify
152
157
  decorate(event)
153
158
  output_queue << event
@@ -170,12 +175,28 @@ class LogStash::Inputs::Tcp < LogStash::Inputs::Base
170
175
 
171
176
  codec.respond_to?(:flush) && codec.flush do |event|
172
177
  event["host"] ||= client_address
178
+ event["port"] ||= client_port
173
179
  event["sslsubject"] ||= socket.peer_cert.subject if @ssl_enable && @ssl_verify
174
180
  decorate(event)
175
181
  output_queue << event
176
182
  end
177
183
  end
178
184
 
185
+ private
186
+ def client_thread(output_queue, socket)
187
+ Thread.new(output_queue, socket) do |q, s|
188
+ begin
189
+ @logger.debug? && @logger.debug("Accepted connection", :client => s.peer, :server => "#{@host}:#{@port}")
190
+ handle_socket(s, s.peeraddr[3], s.peeraddr[1], q, @codec.clone)
191
+ rescue Interrupted
192
+ s.close rescue nil
193
+ ensure
194
+ @client_threads_lock.synchronize{@client_threads.delete(Thread.current)}
195
+ end
196
+ end
197
+ end
198
+
199
+ private
179
200
  def server?
180
201
  @mode == "server"
181
202
  end
@@ -192,15 +213,7 @@ class LogStash::Inputs::Tcp < LogStash::Inputs::Base
192
213
  @ssl_context.cert = OpenSSL::X509::Certificate.new(File.read(@ssl_cert))
193
214
  @ssl_context.key = OpenSSL::PKey::RSA.new(File.read(@ssl_key),@ssl_key_passphrase)
194
215
  if @ssl_verify
195
- @cert_store = OpenSSL::X509::Store.new
196
- # Load the system default certificate path to the store
197
- @cert_store.set_default_paths
198
- if File.directory?(@ssl_cacert)
199
- @cert_store.add_path(@ssl_cacert)
200
- else
201
- @cert_store.add_file(@ssl_cacert)
202
- end
203
- @ssl_context.cert_store = @cert_store
216
+ @ssl_context.cert_store = load_cert_store
204
217
  @ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT
205
218
  end
206
219
  rescue => e
@@ -211,9 +224,22 @@ class LogStash::Inputs::Tcp < LogStash::Inputs::Base
211
224
  @ssl_context
212
225
  end
213
226
 
227
+ def load_cert_store
228
+ cert_store = OpenSSL::X509::Store.new
229
+ cert_store.set_default_paths
230
+ if File.directory?(@ssl_cacert)
231
+ cert_store.add_path(@ssl_cacert)
232
+ else
233
+ cert_store.add_file(@ssl_cacert)
234
+ end if @ssl_cacert
235
+ @ssl_extra_chain_certs.each do |cert|
236
+ cert_store.add_file(cert)
237
+ end
238
+ cert_store
239
+ end
240
+
214
241
  def new_server_socket
215
242
  @logger.info("Starting tcp input listener", :address => "#{@host}:#{@port}")
216
-
217
243
  begin
218
244
  socket = TCPServer.new(@host, @port)
219
245
  rescue Errno::EADDRINUSE
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
 
3
3
  s.name = 'logstash-input-tcp'
4
- s.version = '2.0.5'
4
+ s.version = '3.0.0'
5
5
  s.licenses = ['Apache License (2.0)']
6
6
  s.summary = "Read events over a TCP socket."
7
7
  s.description = "This gem is a logstash plugin required to be installed on top of the Logstash core pipeline using $LS_HOME/bin/plugin install gemname. This gem is not a stand-alone program"
@@ -26,6 +26,9 @@ Gem::Specification.new do |s|
26
26
  s.add_runtime_dependency 'logstash-codec-line'
27
27
  s.add_runtime_dependency 'logstash-codec-json'
28
28
  s.add_runtime_dependency 'logstash-codec-json_lines'
29
+
29
30
  s.add_development_dependency 'logstash-devutils'
31
+ s.add_development_dependency 'flores', '~> 0.0.6'
32
+ s.add_development_dependency 'stud', '~> 0.0.22'
30
33
  end
31
34
 
@@ -4,7 +4,10 @@ require "socket"
4
4
  require "timeout"
5
5
  require "logstash/json"
6
6
  require "logstash/inputs/tcp"
7
- require 'stud/try'
7
+ require "stud/try"
8
+ require "flores/pki"
9
+ require "openssl"
10
+
8
11
  require_relative "../spec_helper"
9
12
 
10
13
  describe LogStash::Inputs::Tcp do
@@ -251,7 +254,6 @@ describe LogStash::Inputs::Tcp do
251
254
  socket.flush
252
255
  end
253
256
  end
254
-
255
257
  socket.close rescue nil
256
258
 
257
259
  result
@@ -269,8 +271,154 @@ describe LogStash::Inputs::Tcp do
269
271
  end
270
272
  end
271
273
 
272
- end
274
+ it "should add the host and port to the generated event" do
275
+ events.each do |event|
276
+ expect(event["host"]).to eq("127.0.0.1")
277
+ expect(event["port"]).to be_an(Fixnum)
278
+ end
279
+ end
280
+
281
+ describe "ssl" do
282
+
283
+ let(:certificate) { helper.certificate }
284
+
285
+ subject(:input) { LogStash::Plugin.lookup("input", "tcp").new(config) }
286
+
287
+ let(:config) do
288
+ { "host" => "0.0.0.0", "port" => port, "ssl_verify" => false,
289
+ "ssl_enable" => true, "ssl_cert" => certificate[0].path, "ssl_key" => certificate[1].path }
290
+ end
291
+
292
+ let(:events) do
293
+
294
+ socket = Stud::try(5.times) do
295
+ ssl_context = OpenSSL::SSL::SSLContext.new
296
+ socket = TCPSocket.new("127.0.0.1", port)
297
+ OpenSSL::SSL::SSLSocket.new(socket, ssl_context)
298
+ end
299
+
300
+ result = helper.pipelineless_input(subject, nevents) do
301
+ socket.connect
302
+ nevents.times do |i|
303
+ socket.puts("msg #{i}")
304
+ socket.flush
305
+ end
306
+ end
307
+ socket.close rescue nil
308
+
309
+ result
310
+ end
311
+
312
+ it "should receive events" do
313
+ expect(events.size).to be(nevents)
314
+ end
315
+
316
+ describe "when ssl_verify is on" do
317
+
318
+ let(:chain_of_certificates) { helper.chain_of_certificates }
319
+
320
+ let(:ssl_context) do
321
+ ssl_context = OpenSSL::SSL::SSLContext.new
322
+ ssl_context.cert = OpenSSL::X509::Certificate.new(client_certificate)
323
+ ssl_context.key = OpenSSL::PKey::RSA.new(client_key)
324
+ ssl_context
325
+ end
326
+
327
+ context "and the verification fails" do
328
+
329
+ let(:config) do
330
+ { "host" => "0.0.0.0", "port" => port,
331
+ "ssl_enable" => true, "ssl_verify" => true,
332
+ "ssl_cert" => chain_of_certificates[:a_cert].path, "ssl_key" => chain_of_certificates[:a_key].path }
333
+ end
334
+
335
+ let(:client_certificate) { File.read(chain_of_certificates[:b_cert].path) }
336
+ let(:client_key) { File.read(chain_of_certificates[:b_key].path) }
337
+
338
+ let(:socket) do
339
+ client = TCPSocket.new("127.0.0.1", port)
340
+ OpenSSL::SSL::SSLSocket.new(client, ssl_context)
341
+ end
273
342
 
343
+ it "should raise an exception when connecting" do
344
+ helper.pipelineless_input(subject, 0) do
345
+ expect { socket.connect }.to raise_error
346
+ socket.close rescue nil
347
+ end
348
+ end
349
+ end
350
+ context "and using the root CA" do
351
+
352
+ let(:config) do
353
+ { "host" => "0.0.0.0", "port" => port,
354
+ "ssl_enable" => true, "ssl_verify" => true,
355
+ "ssl_cert" => chain_of_certificates[:a_cert].path, "ssl_key" => chain_of_certificates[:a_key].path,
356
+ "ssl_cacert" => chain_of_certificates[:root_ca].path }
357
+ end
358
+
359
+ let(:client_certificate) { File.read(chain_of_certificates[:aa_cert].path) }
360
+ let(:client_key) { File.read(chain_of_certificates[:aa_key].path) }
361
+
362
+ let(:events) do
363
+ socket = Stud::try(5.times) do
364
+ socket = TCPSocket.new("127.0.0.1", port)
365
+ OpenSSL::SSL::SSLSocket.new(socket, ssl_context)
366
+ end
367
+ result = helper.pipelineless_input(subject, nevents) do
368
+ socket.connect
369
+ nevents.times do |i|
370
+ socket.puts("msg #{i}")
371
+ socket.flush
372
+ end
373
+ end
374
+ socket.close rescue nil
375
+
376
+ result
377
+ end
378
+
379
+ it "should receive events" do
380
+ expect(events.size).to be(nevents)
381
+ end
382
+
383
+ end
384
+
385
+ context "using an extra chain of certificates" do
386
+
387
+ let(:config) do
388
+ { "host" => "0.0.0.0", "port" => port,
389
+ "ssl_enable" => true, "ssl_verify" => true,
390
+ "ssl_cert" => chain_of_certificates[:b_cert].path, "ssl_key" => chain_of_certificates[:b_key].path,
391
+ "ssl_extra_chain_certs" => [ chain_of_certificates[:root_ca].path, chain_of_certificates[:a_cert].path, chain_of_certificates[:b_cert].path ] }
392
+ end
393
+
394
+ let(:client_certificate) { File.read(chain_of_certificates[:c_cert].path) }
395
+ let(:client_key) { File.read(chain_of_certificates[:c_key].path) }
396
+
397
+ let(:events) do
398
+ socket = Stud::try(5.times) do
399
+ socket = TCPSocket.new("127.0.0.1", port)
400
+ OpenSSL::SSL::SSLSocket.new(socket, ssl_context)
401
+ end
402
+ result = helper.pipelineless_input(subject, nevents) do
403
+ socket.connect
404
+ nevents.times do |i|
405
+ socket.puts("msg #{i}")
406
+ socket.flush
407
+ end
408
+ end
409
+ socket.close rescue nil
410
+
411
+ result
412
+ end
413
+
414
+ it "should receive events" do
415
+ expect(events.size).to be(nevents)
416
+ end
417
+ end
418
+ end
419
+
420
+ end
421
+ end
274
422
  it_behaves_like "an interruptible input plugin" do
275
423
  let(:config) { { "port" => port } }
276
424
  end
data/spec/spec_helper.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # encoding: utf-8
2
2
  require "logstash/devutils/rspec/spec_helper"
3
+ require "tempfile"
4
+ require "stud/temporary"
3
5
 
4
6
  # this has been taken from the udp input, it should be DRYed
5
7
 
@@ -19,4 +21,71 @@ class TcpHelpers
19
21
  input_thread.join
20
22
  result
21
23
  end
24
+
25
+ def certificate
26
+ certificate, key = Flores::PKI.generate("CN=localhost", { :key_size => 2048 })
27
+ [new_temp_file('cert', certificate), new_temp_file('key', key)]
28
+ end
29
+
30
+ def chain_of_certificates
31
+ root_ca, root_key = build_root_ca
32
+ a_cert, a_key = build_certificate(root_ca, root_key, "A_Cert")
33
+ aa_cert, aa_key = build_certificate(root_ca, root_key, "AA_Cert")
34
+ b_cert, b_key = build_certificate(a_cert, a_key, "B_Cert")
35
+ c_cert, c_key = build_certificate(b_cert, b_key, "C_Cert")
36
+ { :root_ca => new_temp_file('', root_ca), :root_key => new_temp_file('', root_key),
37
+ :a_cert => new_temp_file('', a_cert), :a_key => new_temp_file('', a_key),
38
+ :aa_cert => new_temp_file('', aa_cert), :aa_key => new_temp_file('', aa_key),
39
+ :b_cert => new_temp_file('', b_cert), :b_key => new_temp_file('', b_key),
40
+ :c_cert => new_temp_file('', c_cert), :c_key => new_temp_file('', c_key)}
41
+ end
42
+
43
+ private
44
+
45
+ def new_temp_file(name, data)
46
+ file = Stud::Temporary.file
47
+ file.write(data)
48
+ file.rewind
49
+ file
50
+ end
51
+
52
+ def build_certificate(root_ca, root_key=nil, name="")
53
+ key = ( root_key.nil? ? OpenSSL::PKey::RSA.new(2048) : root_key )
54
+ options = { :serial => 2, :subject => "/DC=org/DC=ruby-lang/CN=Ruby#{name}", :key => key, :issuer => root_ca.subject}
55
+ cert = new_certificate(options)
56
+ add_ca_extensions(cert, nil, root_ca)
57
+ [ cert.sign(key, OpenSSL::Digest::SHA256.new), key ]
58
+ end
59
+
60
+ def build_root_ca
61
+ key = OpenSSL::PKey::RSA.new(2048)
62
+ options = { :serial => 1, :subject => "/DC=org/DC=ruby-lang/CN=Ruby CA", :key => key}
63
+ ca = new_certificate(options)
64
+ add_ca_extensions(ca)
65
+ [ ca.sign(key, OpenSSL::Digest::SHA256.new), key ]
66
+ end
67
+
68
+ def new_certificate(options)
69
+ cert = OpenSSL::X509::Certificate.new
70
+ cert.version = 2
71
+ cert.serial = options.fetch(:serial, 1)
72
+ cert.subject = OpenSSL::X509::Name.parse(options.fetch(:subject, "/DC=org/DC=ruby-lang/CN=Ruby CA"))
73
+ cert.issuer = options.fetch(:issuer, cert.subject)
74
+ cert.public_key = options[:key].public_key
75
+ cert.not_before = Time.now
76
+ cert.not_after = cert.not_before + 2 * 365 * 86400
77
+ cert
78
+ end
79
+
80
+ def add_ca_extensions(certificate, subject=nil, issuer=nil)
81
+ factory = OpenSSL::X509::ExtensionFactory.new
82
+ factory.subject_certificate = (subject.nil? ? certificate : subject)
83
+ factory.issuer_certificate = (issuer.nil? ? certificate : issuer)
84
+
85
+ certificate.add_extension(factory.create_extension("basicConstraints","CA:TRUE",true))
86
+ certificate.add_extension(factory.create_extension("keyUsage","keyCertSign, cRLSign, digitalSignature", true))
87
+ certificate.add_extension(factory.create_extension("subjectKeyIdentifier","hash",false))
88
+ certificate.add_extension(factory.create_extension("authorityKeyIdentifier","keyid:always",false))
89
+ end
90
+
22
91
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logstash-input-tcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.5
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Elastic
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-11-25 00:00:00.000000000 Z
11
+ date: 2015-11-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: logstash-core
@@ -100,6 +100,34 @@ dependencies:
100
100
  version: '0'
101
101
  prerelease: false
102
102
  type: :development
103
+ - !ruby/object:Gem::Dependency
104
+ name: flores
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ~>
108
+ - !ruby/object:Gem::Version
109
+ version: 0.0.6
110
+ requirement: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ~>
113
+ - !ruby/object:Gem::Version
114
+ version: 0.0.6
115
+ prerelease: false
116
+ type: :development
117
+ - !ruby/object:Gem::Dependency
118
+ name: stud
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ~>
122
+ - !ruby/object:Gem::Version
123
+ version: 0.0.22
124
+ requirement: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - ~>
127
+ - !ruby/object:Gem::Version
128
+ version: 0.0.22
129
+ prerelease: false
130
+ type: :development
103
131
  description: This gem is a logstash plugin required to be installed on top of the Logstash core pipeline using $LS_HOME/bin/plugin install gemname. This gem is not a stand-alone program
104
132
  email: info@elastic.co
105
133
  executables: []
@@ -138,10 +166,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
138
166
  version: '0'
139
167
  requirements: []
140
168
  rubyforge_project:
141
- rubygems_version: 2.4.8
169
+ rubygems_version: 2.4.6
142
170
  signing_key:
143
171
  specification_version: 4
144
172
  summary: Read events over a TCP socket.
145
173
  test_files:
146
174
  - spec/inputs/tcp_spec.rb
147
175
  - spec/spec_helper.rb
176
+ has_rdoc: