class Riemann::Tools::TLSCheck

Constants

OPENSSL_ERROR_STRINGS

Ruby OpenSSL does not expose ERR_error_string(3), and depending on the version of OpenSSL the available values change. Build a local list of mappings from include/openssl/x509_vfy.h.in and crypto/x509/x509_txt.c for lookups.

Public Class Methods

new() click to toggle source
Calls superclass method Riemann::Tools::new
# File lib/riemann/tools/tls_check.rb, line 239
def initialize
  super

  opts[:connect_timeout] ||= [10, opts[:interval] / 2].min

  @resolve_queue = Queue.new
  @work_queue = Queue.new

  opts[:resolvers].times do
    Thread.new do
      loop do
        uri = @resolve_queue.pop
        host = uri.host

        addresses = if host == 'localhost'
                      Socket.ip_address_list.select { |address| address.ipv6_loopback? || address.ipv4_loopback? }.map(&:ip_address)
                    else
                      Resolv::DNS.new.getaddresses(host)
                    end
        if addresses.empty?
          host = host[1...-1] if host[0] == '[' && host[-1] == ']'
          begin
            addresses << IPAddr.new(host)
          rescue IPAddr::InvalidAddressError
            # Ignore
          end
        end

        @work_queue.push([uri, addresses])
      end
    end
  end

  opts[:workers].times do
    Thread.new do
      loop do
        uri, addresses = @work_queue.pop
        test_uri_addresses(uri, addresses)
      end
    end
  end
end

Public Instance Methods

imap_tls_socket(uri, address) click to toggle source
# File lib/riemann/tools/tls_check.rb, line 522
def imap_tls_socket(uri, address)
  socket = tcp_socket(address, uri.port)
  read_socket_lines_until_prefix_matched(socket, '* OK')
  socket.send(". CAPABILITY\r\n", 0)
  read_socket_lines_until_prefix_matched(socket, '. OK', also_accept_prefixes: ['* CAPABILITY'])
  socket.send(". STARTTLS\r\n", 0)
  read_socket_lines_until_prefix_matched(socket, '. OK')

  tls_handshake(socket, uri.host)
end
ldap_tls_socket(uri, address) click to toggle source
# File lib/riemann/tools/tls_check.rb, line 533
def ldap_tls_socket(uri, address)
  socket = tcp_socket(address, uri.port)
  socket.write(['301d02010177188016312e332e362e312e342e312e313436362e3230303337'].pack('H*'))
  expected_res = ['300c02010178070a010004000400'].pack('H*')
  res = socket.read(expected_res.length)

  return nil unless res == expected_res

  tls_handshake(socket, uri.host)
end
my_hostname() click to toggle source
# File lib/riemann/tools/tls_check.rb, line 506
def my_hostname
  Addrinfo.tcp(Socket.gethostname, 8023).getnameinfo.first
rescue SocketError
  Socket.gethostname
end
mysql_tls_socket(uri, address) click to toggle source
# File lib/riemann/tools/tls_check.rb, line 471
def mysql_tls_socket(uri, address)
  socket = tcp_socket(address, uri.port)
  length = "#{socket.read(3)}\0".unpack1('L*')
  _sequence = socket.read(1)
  body = socket.read(length)
  initial_handshake_packet = body.unpack('cZ*La8aScSS')

  capabilities = initial_handshake_packet[5] | (initial_handshake_packet[8] << 16)

  ssl_flag = 1 << 11
  raise 'No TLS support' if (capabilities & ssl_flag).zero?

  socket.write(['2000000185ae7f0000000001210000000000000000000000000000000000000000000000'].pack('H*'))
  tls_handshake(socket, uri.host)
end
not_after_state(tls_check_result) click to toggle source
not_before                      not_after
    |<----------------------------->|         validity_duration
                        |<--------->|         renewal_duration
                        | ⅓ | ⅓ | ⅓ |

…oooooooooooooooooooooooooooooooowwwwcccccccccc… not_after_state

time --->>>>
# File lib/riemann/tools/tls_check.rb, line 440
def not_after_state(tls_check_result)
  if tls_check_result.expired_or_expire_soon?
    'critical'
  elsif tls_check_result.expire_soonish?
    'warning'
  else
    'ok'
  end
end
not_before_state(tls_check_result) click to toggle source
not_before                      not_after
    |<----------------------------->|         validity_duration

…ccccccccoooooooooooooooooooooooooooooooooooooo… not_before_state

time --->>>>
# File lib/riemann/tools/tls_check.rb, line 429
def not_before_state(tls_check_result)
  tls_check_result.not_valid_yet? ? 'critical' : 'ok'
end
postgres_tls_socket(uri, address) click to toggle source
# File lib/riemann/tools/tls_check.rb, line 487
def postgres_tls_socket(uri, address)
  socket = tcp_socket(address, uri.port)
  socket.write(['0000000804d2162f'].pack('H*'))
  raise 'Unexpected reply' unless socket.read(1) == 'S'

  tls_handshake(socket, uri.host)
end
raw_tls_socket(uri, address) click to toggle source
# File lib/riemann/tools/tls_check.rb, line 544
def raw_tls_socket(uri, address)
  raise "No default port for #{uri.scheme} scheme" unless uri.port

  socket = tcp_socket(address, uri.port)
  tls_handshake(socket, uri.host)
end
read_socket_lines_until_prefix_matched(socket, prefix, also_accept_prefixes: []) click to toggle source
# File lib/riemann/tools/tls_check.rb, line 512
def read_socket_lines_until_prefix_matched(socket, prefix, also_accept_prefixes: [])
  loop do
    line = socket.gets
    break if line.start_with?(prefix)
    next if also_accept_prefixes.map { |accepted_prefix| line.start_with?(accepted_prefix) }.any?

    raise UnexpectedMessage, line
  end
end
report_availability(tls_check_result) click to toggle source
# File lib/riemann/tools/tls_check.rb, line 334
def report_availability(tls_check_result)
  if tls_check_result.exception
    report(
      service: "#{tls_endpoint_name(tls_check_result)} availability",
      state: 'critical',
      description: tls_check_result.exception.message,
    )
  else
    issues = []

    issues << 'Certificate is not valid yet' if tls_check_result.not_valid_yet?
    issues << 'Certificate has expired' if tls_check_result.expired?
    issues << 'Certificate identity could not be verified' unless tls_check_result.valid_identity?
    issues << 'Certificate is not trusted' unless tls_check_result.trusted?
    issues << 'Certificate OCSP verification failed' if tls_check_result.ocsp? && !tls_check_result.valid_ocsp?

    report(
      service: "#{tls_endpoint_name(tls_check_result)} availability",
      state: issues.empty? ? 'ok' : 'critical',
      description: issues.join("\n"),
    )
  end
end
report_identity(tls_check_result) click to toggle source
# File lib/riemann/tools/tls_check.rb, line 384
def report_identity(tls_check_result)
  report(
    service: "#{tls_endpoint_name(tls_check_result)} identity",
    state: tls_check_result.valid_identity? ? 'ok' : 'critical',
    description: "Valid for:\n#{tls_check_result.acceptable_identities.join("\n")}",
  )
end
report_not_after(tls_check_result) click to toggle source
# File lib/riemann/tools/tls_check.rb, line 366
def report_not_after(tls_check_result)
  report(
    service: "#{tls_endpoint_name(tls_check_result)} not after",
    state: not_after_state(tls_check_result),
    metric: tls_check_result.not_after_ago,
    description: tls_check_result.not_after_ago_in_words,
  )
end
report_not_before(tls_check_result) click to toggle source
# File lib/riemann/tools/tls_check.rb, line 375
def report_not_before(tls_check_result)
  report(
    service: "#{tls_endpoint_name(tls_check_result)} not before",
    state: not_before_state(tls_check_result),
    metric: tls_check_result.not_before_away,
    description: tls_check_result.not_before_away_in_words,
  )
end
report_ocsp(tls_check_result) click to toggle source
# File lib/riemann/tools/tls_check.rb, line 414
def report_ocsp(tls_check_result)
  return unless tls_check_result.ocsp?

  report(
    service: "#{tls_endpoint_name(tls_check_result)} OCSP status",
    state: tls_check_result.valid_ocsp? ? 'ok' : 'critical',
    description: tls_check_result.ocsp_status,
  )
end
report_trust(tls_check_result) click to toggle source
# File lib/riemann/tools/tls_check.rb, line 392
def report_trust(tls_check_result)
  commont_attrs = {
    service: "#{tls_endpoint_name(tls_check_result)} trust",
  }
  extra_attrs = if tls_check_result.exception
                  {
                    state: 'critical',
                    description: tls_check_result.exception.message,
                  }
                else
                  {
                    state: tls_check_result.trusted? ? 'ok' : 'critical',
                    description: if OPENSSL_ERROR_STRINGS[tls_check_result.verify_result]
                                   format('%<code>d - %<msg>s', code: tls_check_result.verify_result, msg: OPENSSL_ERROR_STRINGS[tls_check_result.verify_result])
                                 else
                                   tls_check_result.verify_result.to_s
                                 end,
                  }
                end
  report(commont_attrs.merge(extra_attrs))
end
report_unavailability(uri, address, exception) click to toggle source
# File lib/riemann/tools/tls_check.rb, line 358
def report_unavailability(uri, address, exception)
  report(
    service: "#{tls_endpoint_name2(uri, address)} availability",
    state: 'critical',
    description: exception.message,
  )
end
smtp_tls_socket(uri, address) click to toggle source
# File lib/riemann/tools/tls_check.rb, line 495
def smtp_tls_socket(uri, address)
  socket = tcp_socket(address, uri.port)
  read_socket_lines_until_prefix_matched(socket, '220 ', also_accept_prefixes: ['220-'])
  socket.send("EHLO #{my_hostname}\r\n", 0)
  read_socket_lines_until_prefix_matched(socket, '250 ', also_accept_prefixes: ['250-'])
  socket.send("STARTTLS\r\n", 0)
  socket.gets

  tls_handshake(socket, uri.host)
end
ssl_context() click to toggle source
# File lib/riemann/tools/tls_check.rb, line 570
def ssl_context
  @ssl_context ||= begin
    ctx = OpenSSL::SSL::SSLContext.new
    ctx.cert_store = store
    ctx.verify_hostname = false
    ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
    ctx
  end
end
store() click to toggle source
# File lib/riemann/tools/tls_check.rb, line 580
def store
  @store ||= begin
    store = OpenSSL::X509::Store.new
    store.set_default_paths
    opts[:trust].each do |path|
      if File.directory?(path)
        store.add_path(path)
      else
        store.add_file(path)
      end
    end
    store
  end
end
tcp_socket(host, port) click to toggle source
# File lib/riemann/tools/tls_check.rb, line 450
def tcp_socket(host, port)
  Socket.tcp(host, port, connect_timeout: opts[:connect_timeout])
end
test_uri_address(uri, address) click to toggle source
# File lib/riemann/tools/tls_check.rb, line 319
def test_uri_address(uri, address)
  socket = tls_socket(uri, address)
  tls_check_result = TLSCheckResult.new(uri, address, socket, self)
  report_availability(tls_check_result)
  return unless socket.peer_cert

  report_not_before(tls_check_result) if opts[:checks].include?('not-before')
  report_not_after(tls_check_result) if opts[:checks].include?('not-after')
  report_identity(tls_check_result) if opts[:checks].include?('identity')
  report_trust(tls_check_result) if opts[:checks].include?('trust')
  report_ocsp(tls_check_result) if opts[:checks].include?('ocsp')
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, UnexpectedMessage => e
  report_unavailability(uri, address, e)
end
test_uri_addresses(uri, addresses) click to toggle source
# File lib/riemann/tools/tls_check.rb, line 313
def test_uri_addresses(uri, addresses)
  addresses.each do |address|
    test_uri_address(uri, address.to_s)
  end
end
tick() click to toggle source
# File lib/riemann/tools/tls_check.rb, line 282
def tick
  report(
    service: 'riemann tls-check resolvers utilization',
    metric: (opts[:resolvers].to_f - @resolve_queue.num_waiting) / opts[:resolvers],
    state: @resolve_queue.num_waiting.positive? ? 'ok' : 'critical',
    tags: %w[riemann],
  )
  report(
    service: 'riemann tls-check resolvers saturation',
    metric: @resolve_queue.length,
    state: @resolve_queue.empty? ? 'ok' : 'critical',
    tags: %w[riemann],
  )
  report(
    service: 'riemann tls-check workers utilization',
    metric: (opts[:workers].to_f - @work_queue.num_waiting) / opts[:workers],
    state: @work_queue.num_waiting.positive? ? 'ok' : 'critical',
    tags: %w[riemann],
  )
  report(
    service: 'riemann tls-check workers saturation',
    metric: @work_queue.length,
    state: @work_queue.empty? ? 'ok' : 'critical',
    tags: %w[riemann],
  )

  opts[:uri].each do |uri|
    @resolve_queue.push(URI(uri))
  end
end
tls_endpoint_name(tls_check_result) click to toggle source
# File lib/riemann/tools/tls_check.rb, line 595
def tls_endpoint_name(tls_check_result)
  tls_endpoint_name2(tls_check_result.uri, tls_check_result.address)
end
tls_endpoint_name2(uri, address) click to toggle source
# File lib/riemann/tools/tls_check.rb, line 599
def tls_endpoint_name2(uri, address)
  "TLS certificate #{uri} #{endpoint_name(IPAddr.new(address), uri.port)}"
end
tls_handshake(raw_socket, hostname) click to toggle source
# File lib/riemann/tools/tls_check.rb, line 551
def tls_handshake(raw_socket, hostname)
  tls_socket = OpenSSL::SSL::SSLSocket.new(raw_socket, ssl_context)
  tls_socket.hostname = hostname
  begin
    tls_socket.connect
  rescue OpenSSL::SSL::SSLError => e
    # This may fail for example if a client certificate is required but
    # not provided. In this case, the remote certificate is available and
    # we can ignore this issue. In other cases, the remote certificate is
    # not available, in this case we want to stop and report the issue
    # (e.g. connecting to a host with a SNI for a name not handled by
    # that host).
    tls_socket.define_singleton_method(:exception) do
      e
    end
  end
  tls_socket
end
tls_socket(uri, address) click to toggle source
# File lib/riemann/tools/tls_check.rb, line 454
def tls_socket(uri, address)
  case uri.scheme
  when 'smtp'
    smtp_tls_socket(uri, address)
  when 'imap'
    imap_tls_socket(uri, address)
  when 'ldap'
    ldap_tls_socket(uri, address)
  when 'mysql'
    mysql_tls_socket(uri, address)
  when 'postgres'
    postgres_tls_socket(uri, address)
  else
    raw_tls_socket(uri, address)
  end
end