Skip to content

Commit

Permalink
api: add optional TLS/SSL
Browse files Browse the repository at this point in the history
  • Loading branch information
yaauie committed Oct 12, 2021
1 parent e70b7dc commit acb337b
Show file tree
Hide file tree
Showing 6 changed files with 83 additions and 3 deletions.
7 changes: 7 additions & 0 deletions config/logstash.yml
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,13 @@
#
# api.environment: "production"
#
# The HTTP API can be secured with SSL (TLS). To do so, you will need to provide
# the path to a password-protected keystore in jks format, along with credentials.
#
# api.ssl.enabled: false
# api.ssl.keystore.path: /path/to/keystore.jks
# api.ssl.keystore.password: "y0uRp4$$w0rD"
#
# ------------ Module Settings ---------------
# Define modules here. Modules definitions must be defined as an array.
# The simple way to see this is to prepend each `name` with a `-`, and keep
Expand Down
12 changes: 12 additions & 0 deletions logstash-core/lib/logstash/environment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ module Environment
Setting::String.new("api.http.host", "127.0.0.1").with_deprecated_alias("http.host"),
Setting::PortRange.new("api.http.port", 9600..9700).with_deprecated_alias("http.port"),
Setting::String.new("api.environment", "production").with_deprecated_alias("http.environment"),
Setting::Boolean.new("api.ssl.enabled", false),
Setting::ExistingFilePath.new("api.ssl.keystore.path", nil, false).nullable,
Setting::Password.new("api.ssl.keystore.password", nil, false).nullable,
Setting::String.new("queue.type", "memory", true, ["persisted", "memory"]),
Setting::Boolean.new("queue.drain", false),
Setting::Bytes.new("queue.page_capacity", "64mb"),
Expand Down Expand Up @@ -119,6 +122,15 @@ module Environment
end
end

SETTINGS.on_post_process do |settings|
if settings.get('api.ssl.enabled') && !settings.set?('api.ssl.keystore.path')
raise ArgumentError.new('Setting `api.ssl.enabled` is true, but required `api.ssl.keystore.path` is not provided. Please provide a valid keystore in `logstash.yml`')
end
if settings.set?('api.ssl.keystore.path') && !settings.set?('api.ssl.keystore.password')
raise ArgumentError.new("Setting `api.ssl.keystore.path` provided without required `api.ssl.keystore.password`. Please provided credentials in `logstash.yml`")
end
end

module Environment
extend self

Expand Down
8 changes: 8 additions & 0 deletions logstash-core/lib/logstash/patches/puma.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ def puts(str)
alias_method :<<, :puts
end

# ::Puma::Events#error(str) sends Kernel#exit
# let's raise something sensible instead.
UnrecoverablePumaError = Class.new(RuntimeError)
class NonCrashingPumaEvents < ::Puma::Events
def error(str)
raise UnrecoverablePumaError.new(str)
end
end
end

# Reopen the puma class to create a scoped STDERR and STDOUT
Expand Down
37 changes: 37 additions & 0 deletions logstash-core/lib/logstash/settings.rb
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,12 @@ def with_deprecated_alias(deprecated_alias_name)
SettingWithDeprecatedAlias.wrap(self, deprecated_alias_name)
end

##
# Returns a Nullable-wrapped self, effectively making the Setting optional.
def nullable
Nullable.new(self)
end

def format(output)
effective_value = self.value
default_value = self.default
Expand Down Expand Up @@ -510,6 +516,22 @@ def validate(value)
end
end

class Password < Coercible
def initialize(name, default=nil, strict=true)
super(name, LogStash::Util::Password, default, strict)
end

def coerce(value)
return value if value.kind_of?(LogStash::Util::Password)

LogStash::Util::Password.new(value)
end

def validate(value)
super(value)
end
end

# The CoercibleString allows user to enter any value which coerces to a String.
# For example for true/false booleans; if the possible_strings are ["foo", "true", "false"]
# then these options in the config file or command line will be all valid: "foo", true, false, "true", "false"
Expand Down Expand Up @@ -706,6 +728,21 @@ def validate(value)
end
end

# @see Setting#nullable
# @api internal
class Nullable < SimpleDelegator
def validate(value)
return true if value.nil?

__getobj__.send(:validate, value)
end

# prevent delegate from intercepting
def validate_value
validate(value)
end
end

##
# @api private
#
Expand Down
20 changes: 18 additions & 2 deletions logstash-core/lib/logstash/webserver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ def self.from_settings(logger, agent, settings)
options[:http_port] = settings.get('api.http.port')
options[:http_environment] = settings.get('api.environment')

if settings.get('api.ssl.enabled')
ssl_params = {}
ssl_params['keystore'] = settings.get('api.ssl.keystore.path') || fail('Setting `api.ssl.keystore.path` required when `api.ssl.enabled` is true.')
keystore_pass_wrapper = settings.get('api.ssl.keystore.password') || fail('Setting `api.ssl.keystore.password` required when `api.ssl.enabled` is true.')
ssl_params['keystore-pass'] = keystore_pass_wrapper.value

options[:ssl_params] = ssl_params.freeze
end

new(logger, agent, options)
end

Expand All @@ -46,6 +55,7 @@ def initialize(logger, agent, options={})
@http_host = options[:http_host] || DEFAULT_HOST
@http_ports = options[:http_ports] || DEFAULT_PORTS
@http_environment = options[:http_environment] || DEFAULT_ENVIRONMENT
@ssl_params = options[:ssl_params] if options.include?(:ssl_params)
@running = Concurrent::AtomicBoolean.new(false)

# wrap any output that puma could generate into a wrapped logger
Expand Down Expand Up @@ -89,7 +99,7 @@ def stop(options={})

def _init_server
io_wrapped_logger = LogStash::IOWrappedLogger.new(logger)
events = ::Puma::Events.new(io_wrapped_logger, io_wrapped_logger)
events = LogStash::NonCrashingPumaEvents.new(io_wrapped_logger, io_wrapped_logger)

::Puma::Server.new(@app, events)
end
Expand All @@ -102,12 +112,18 @@ def bind_to_available_port
@server = _init_server

logger.debug("Trying to start WebServer", :port => candidate_port)
@server.add_tcp_listener(http_host, candidate_port)
if @ssl_params
ssl_context = Puma::MiniSSL::ContextBuilder.new(@ssl_params, @server.events).context
@server.add_ssl_listener(http_host, candidate_port, ssl_context)
else
@server.add_tcp_listener(http_host, candidate_port)
end

@port = candidate_port
logger.info("Successfully started Logstash API endpoint", :port => candidate_port)
set_http_address_metric("#{http_host}:#{candidate_port}")

@server.run.join
break
rescue Errno::EADDRINUSE
if http_ports.count == 1
Expand Down
2 changes: 1 addition & 1 deletion logstash-core/logstash-core.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Gem::Specification.new do |gem|
gem.add_runtime_dependency "rack", '~> 2'
gem.add_runtime_dependency "mustermann", '~> 1.0.3'
gem.add_runtime_dependency "sinatra", '~> 2'
gem.add_runtime_dependency 'puma', '~> 4'
gem.add_runtime_dependency 'puma', '~> 5'
gem.add_runtime_dependency "jruby-openssl", "= 0.10.5" # >= 0.9.13 Required to support TLSv1.2

gem.add_runtime_dependency "treetop", "~> 1" #(MIT license)
Expand Down

0 comments on commit acb337b

Please sign in to comment.