Skip to content

Commit

Permalink
Refs #25293 - SSL code works with both stacks
Browse files Browse the repository at this point in the history
  • Loading branch information
lzap authored and mmoll committed Apr 28, 2020
1 parent 14b280a commit d4d3c13
Show file tree
Hide file tree
Showing 16 changed files with 189 additions and 137 deletions.
10 changes: 1 addition & 9 deletions bundler.d/puma.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
group :puma do
if RUBY_VERSION < '2.3'
# Important!
# The last actual version that supports v2.0.0 up to v2.2.0 is 3.10.0
# Puma version 3.11.0 changed the usage of socket to a feature that is not
# supported by Ruby 2.2.0 and lower, and it causes a crash on TLS!
gem 'puma', '~>3.10.0', :require => 'puma'
else
gem 'puma', '~>3.12', :require => 'puma'
end
gem 'puma', '~> 4.1', :require => 'puma'
end
20 changes: 11 additions & 9 deletions config/settings.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@
#:ssl_ca_file: ssl/certs/ca.pem
#:ssl_private_key: ssl/private_keys/fqdn.key

# Use this option only if you need to disable certain cipher suites.
# Note: we use the OpenSSL suite name, such as "RC4-MD5".
# The complete list of cipher suite names can be found at:
# https://www.openssl.org/docs/manmaster/man1/ciphers.html#CIPHER-SUITE-NAMES
#:ssl_disabled_ciphers: [CIPHER-SUITE-1, CIPHER-SUITE-2]
# Enabled ciphers for SSL. The complete list of cipher suite
# names and list format can be found at:
# https://www.openssl.org/docs/man1.0.2/man1/ciphers.html
# Accepts both array and colon-separated (OpenSSL) string.
# The default value (when commented out) is defined in:
# /usr/share/foreman-proxy/lib/proxy/settings/global.rb
#:ssl_enabled_ciphers: ['ECDHE-RSA-AES128-GCM-SHA256','ECDHE-RSA-AES256-GCM-SHA384','AES128-GCM-SHA256','AES256-GCM-SHA384','AES128-SHA256','AES256-SHA256','AES128-SHA','AES256-SHA']
#:ssl_enabled_ciphers: "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256"

# Use this option only if you need to strictly specify TLS versions to be
# disabled. SSLv3 and TLS v1.0 are always disabled and cannot be configured.
# Specify versions like: '1.1', or '1.2'
# SSLv3 and TLS 1.0 and 1.1 are disabled in smart-proxy. To disable additional
# versions of TLS, use this setting. Specify versions in an array, e.g. ['1.2'].
#:tls_disabled_versions: []

# Hosts which the proxy accepts connections from
Expand Down Expand Up @@ -60,7 +62,7 @@

# http server type configuration
# A string that indicates the server type (possible values: 'webrick', 'puma').
# Default value is 'webrick'
# Default value is 'puma'
#:http_server_type: 'puma'

# Log configuration
Expand Down
180 changes: 98 additions & 82 deletions lib/launcher.rb
Original file line number Diff line number Diff line change
@@ -1,47 +1,39 @@
require 'proxy/log'
require 'proxy/util'
require 'proxy/sd_notify'
require 'proxy/settings'
require 'proxy/signal_handler'
require 'proxy/log_buffer/trace_decorator'
require 'rack'
require 'webrick'

CIPHERS = ['ECDHE-RSA-AES128-GCM-SHA256', 'ECDHE-RSA-AES256-GCM-SHA384',
'AES128-GCM-SHA256', 'AES256-GCM-SHA384', 'AES128-SHA256',
'AES256-SHA256', 'AES128-SHA', 'AES256-SHA'].freeze

module Proxy
class Launcher
include ::Proxy::Log
include ::Proxy::Util

attr_reader :settings

def initialize(settings = SETTINGS)
def initialize(settings = Proxy::SETTINGS)
@settings = settings
@settings.http_server_type = Proxy::SETTINGS.http_server_type.to_sym
if @settings.http_server_type == :puma
begin
require 'puma'
require 'rack/handler/puma'
require 'puma-patch'
rescue LoadError
logger.warn 'Puma was requested but not installed, falling back to webrick'
@settings.http_server_type = :webrick
end
if @settings.http_server_type == "puma"
require 'puma'
require 'rack/handler/puma'
require 'puma-patch'
end
@servers = []
end

def pid_path
@settings.daemon_pid
settings.daemon_pid
end

def http_enabled?
!@settings.http_port.nil?
!settings.http_port.nil?
end

def https_enabled?
@settings.ssl_private_key && @settings.ssl_certificate && @settings.ssl_ca_file
settings.ssl_private_key && settings.ssl_certificate && settings.ssl_ca_file
end

def plugins
Expand All @@ -64,7 +56,7 @@ def http_app(http_port, plugins = http_plugins)

{
:app => app,
:server => @settings.http_server_type,
:server => settings.http_server_type.to_sym,
:DoNotListen => true,
:Port => http_port, # only being used to correctly log http port being used
:Logger => ::Proxy::LogBuffer::TraceDecorator.instance,
Expand All @@ -84,51 +76,67 @@ def https_app(https_port, plugins = https_plugins)
plugins.each { |p| instance_eval(p.https_rackup) }
end

ssl_options = OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:options]
ssl_options |= OpenSSL::SSL::OP_CIPHER_SERVER_PREFERENCE if defined?(OpenSSL::SSL::OP_CIPHER_SERVER_PREFERENCE)
# This is required to disable SSLv3 on Ruby 1.8.7
ssl_options |= OpenSSL::SSL::OP_NO_SSLv2 if defined?(OpenSSL::SSL::OP_NO_SSLv2)
ssl_options |= OpenSSL::SSL::OP_NO_SSLv3 if defined?(OpenSSL::SSL::OP_NO_SSLv3)
ssl_options |= OpenSSL::SSL::OP_NO_TLSv1 if defined?(OpenSSL::SSL::OP_NO_TLSv1)
ssl_options |= OpenSSL::SSL::OP_NO_TLSv1_1 if defined?(OpenSSL::SSL::OP_NO_TLSv1_1)

if @settings.tls_disabled_versions
@settings.tls_disabled_versions&.each do |version|
constant = OpenSSL::SSL.const_get("OP_NO_TLSv#{version.to_s.tr('.', '_')}") rescue nil

if constant
logger.info "TLSv#{version} will be disabled."
ssl_options |= constant
else
logger.warn "TLSv#{version} was not found."
end
end
ssl_enabled_ciphers = if settings.ssl_enabled_ciphers.is_a?(String)
settings.ssl_enabled_ciphers.split(':')
else
settings.ssl_enabled_ciphers
end

app_details = {
:app => app,
:server => @settings.http_server_type,
:server => settings.http_server_type,
:DoNotListen => true,
:Port => https_port, # only being used to correctly log https port being used
:Logger => ::Proxy::LogBuffer::Decorator.instance,
:ServerSoftware => "foreman-proxy/#{Proxy::VERSION}",
:SSLEnable => true,
:SSLVerifyClient => OpenSSL::SSL::VERIFY_PEER,
:SSLCACertificateFile => @settings.ssl_ca_file,
:SSLOptions => ssl_options,
:SSLCiphers => CIPHERS - Proxy::SETTINGS.ssl_disabled_ciphers,
:SSLCiphers => ssl_enabled_ciphers,
:daemonize => false,
}
case @settings.http_server_type
when :webrick
app_details[:SSLPrivateKey] = load_ssl_private_key(@settings.ssl_private_key)
app_details[:SSLCertificate] = load_ssl_certificate(@settings.ssl_certificate)
when :puma

case settings.http_server_type
when "webrick"
ssl_options = OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:options]
ssl_options |= OpenSSL::SSL::OP_CIPHER_SERVER_PREFERENCE if defined?(OpenSSL::SSL::OP_CIPHER_SERVER_PREFERENCE)
ssl_options |= OpenSSL::SSL::OP_NO_SSLv2 if defined?(OpenSSL::SSL::OP_NO_SSLv2)
ssl_options |= OpenSSL::SSL::OP_NO_SSLv3 if defined?(OpenSSL::SSL::OP_NO_SSLv3)
ssl_options |= OpenSSL::SSL::OP_NO_TLSv1 if defined?(OpenSSL::SSL::OP_NO_TLSv1)
ssl_options |= OpenSSL::SSL::OP_NO_TLSv1_1 if defined?(OpenSSL::SSL::OP_NO_TLSv1_1)

if settings.tls_disabled_versions
settings.tls_disabled_versions&.each do |version|
constant = OpenSSL::SSL.const_get("OP_NO_TLSv#{version.to_s.tr('.', '_')}") rescue nil

if constant
logger.info "TLSv#{version} will be disabled."
ssl_options |= constant
else
logger.warn "TLSv#{version} was not found."
end
end
end

app_details[:SSLEnable] = true
app_details[:SSLVerifyClient] = OpenSSL::SSL::VERIFY_PEER
app_details[:SSLCACertificateFile] = settings.ssl_ca_file
app_details[:SSLPrivateKey] = load_ssl_private_key(settings.ssl_private_key)
app_details[:SSLCertificate] = load_ssl_certificate(settings.ssl_certificate)
app_details[:SSLOptions] = ssl_options
when "puma"
# https://github.com/puma/puma#binding-tcp--sockets
app_details[:SSLArgs] = {
:ca => @settings.ssl_ca_file,
:key => @settings.ssl_private_key,
:cert => @settings.ssl_certificate,
:verify_mode => 'peer'
:ca => settings.ssl_ca_file,
:key => settings.ssl_private_key,
:cert => settings.ssl_certificate,
:verify_mode => 'peer',
}
app_details[:SSLArgs][:no_tlsv1] = "true"
app_details[:SSLArgs][:no_tlsv1_1] = "true"
# no additional TLS versions via tls_disabled_versions can be currently disabled for puma
if settings.ssl_enabled_ciphers
app_details[:SSLArgs][:ssl_cipher_list] = ssl_enabled_ciphers.join(':')
end
else
raise "Unknown http_server_type: #{settings.http_server_type}"
end
app_details
end
Expand Down Expand Up @@ -182,22 +190,25 @@ def add_puma_server_callback(sd_notify)
events = ::Puma::Events.new(::Proxy::LogBuffer::Decorator.instance, ::Proxy::LogBuffer::Decorator.instance)
events.register(:state) do |status|
if status == :running
sd_notify.ready_all { logger.info("Smart proxy has finished launching #{sd_notify.total} puma instances, ready to serve requests.") }
logger.debug "Finished launching a puma instance, #{sd_notify.pending} instances to go..."
sd_notify.ready_all { sd_notify.status_if_active("Finished launching #{sd_notify.total} instances, ready to serve requests", logger) }
sd_notify.status_if_active("Finished launching an instance, #{sd_notify.pending} instances to go...", logger) if sd_notify.pending > 0
end
end
events
end

def format_ip_for_url(address)
addr = IPAddr.new(address)
addr.ipv6? ? "[#{addr}]" : addr.to_s
rescue IPAddr::InvalidAddressError
address
end

def add_puma_server(app, address, port, conn_type, sd_notify)
address = "[#{address}]" if address.include?(':')
address = format_ip_for_url(address)
logger.debug "Launching Puma listener at #{address} port #{port}"
if conn_type == :ssl
require 'cgi'
query_list = app[:SSLArgs].to_a.map do |x|
"#{CGI::escape(x[0].to_s)}=#{CGI::escape(x[1])}"
end
host = "ssl://#{address}:#{port}/?#{query_list.join('&')}"
host = "ssl://#{address}:#{port}/?#{hash_to_query_string(app[:SSLArgs])}"
else
host = address
end
Expand All @@ -214,8 +225,8 @@ def add_puma_server(app, address, port, conn_type, sd_notify)

def add_webrick_server_callback(app, sd_notify)
app[:StartCallback] = lambda do
sd_notify.ready_all { logger.info("Smart proxy has finished launching #{sd_notify.total} webrick instances.\nSending NOTIFY_SOCKET, ready to serve requests.") }
logger.debug "Finished launching a webrick instance, #{sd_notify.pending} instances to go..."
sd_notify.ready_all { sd_notify.status_if_active("Finished launching #{sd_notify.total} instances, ready to serve requests", logger) }
sd_notify.status_if_active("Finished launching an instance, #{sd_notify.pending} instances to go...", logger) if sd_notify.pending > 0
end
end

Expand All @@ -232,18 +243,22 @@ def add_webrick_server(app, addresses, port, sd_notify)
server
end

def ipv6_enabled?
File.exist?('/proc/net/if_inet6') || (RUBY_PLATFORM =~ /cygwin|mswin|mingw|bccwin|wince|emx/)
end

def add_threaded_server(server_name, conn_type, app, addresses, port, sd_notify)
result = []
case server_name
when :webrick
when "webrick"
result << Thread.new do
@servers << add_webrick_server(app, addresses, port, sd_notify).start
end
when :puma
when "puma"
addresses.flatten.each do |address|
# Puma listens both on IPv4 and IPv6 on '::', there is no way to make Puma
# to listen only on IPv6.
address = '::' if address == '*'
address = '::' if address == '*' && ipv6_enabled?
result << Thread.new do
add_puma_server(app, address, port, conn_type, sd_notify)
end
Expand All @@ -255,40 +270,41 @@ def add_threaded_server(server_name, conn_type, app, addresses, port, sd_notify)
def launch
raise Exception.new("Both http and https are disabled, unable to start.") unless http_enabled? || https_enabled?

if @settings.daemon
if settings.daemon
check_pid
Process.daemon
write_pid
end

::Proxy::PluginInitializer.new(::Proxy::Plugins.instance).initialize_plugins

http_app = http_app(@settings.http_port)
https_app = https_app(@settings.https_port)
expected = [http_app, https_app].compact.size
http_app = http_app(settings.http_port)
https_app = https_app(settings.https_port)
hosts = settings.bind_host.is_a?(Array) ? settings.bind_host.size : 1
expected = [http_app, https_app].compact.size * hosts
logger.debug "Expected number of instances to launch: #{expected}"
sd_notify = Proxy::SdNotify.new
sd_notify.ready_when(expected)

http_server_name = @settings.http_server_type
https_server_name = @settings.http_server_type
http_server_name = settings.http_server_type
https_server_name = settings.http_server_type
threads = []
if https_app
threads += add_threaded_server(https_server_name,
:ssl,
https_app,
@settings.bind_host,
@settings.https_port,
sd_notify)
:ssl,
https_app,
settings.bind_host,
settings.https_port,
sd_notify)
end

if http_app
threads += add_threaded_server(http_server_name,
:tcp,
http_app,
@settings.bind_host,
@settings.http_port,
sd_notify)
:tcp,
http_app,
settings.bind_host,
settings.http_port,
sd_notify)
end

Proxy::SignalHandler.install_traps(@servers)
Expand Down
19 changes: 17 additions & 2 deletions lib/proxy/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ def log_halt(code = nil, exception_or_msg = nil, custom_msg = nil)
end

# read the HTTPS client certificate from the environment and extract its CN
def https_cert_cn
certificate_raw = request.env['SSL_CLIENT_CERT'].to_s
def https_cert_cn(request)
certificate_raw = ssl_client_cert(request)
log_halt 403, 'could not read client cert from environment' if certificate_raw.empty?

begin
Expand Down Expand Up @@ -106,4 +106,19 @@ def remote_fqdn(forward_verify = true)
fqdn
end
end

def ssl_client_cert(request)
if request.env.key?('SSL_CLIENT_CERT')
request.env['SSL_CLIENT_CERT'].to_s
elsif request.env.key?('puma.peercert')
request.env['puma.peercert'].to_s
else
''
end
end

def https?(request)
# test env variable for puma and also webrick
request.env['HTTPS'].to_s == 'https' || request.env['HTTPS'].to_s =~ /yes|on|1/
end
end
Loading

0 comments on commit d4d3c13

Please sign in to comment.