From d4691e1411b07d077ae7fc57112fe59894865afd Mon Sep 17 00:00:00 2001 From: Denis Knauf Date: Tue, 29 Mar 2022 15:59:38 +0200 Subject: [PATCH] List supported Algorithms and SignatureAlgorithms. No Hostkey verification (/dev/null, no strict). --- collector.rb | 52 ++++---- probe.rb | 332 ++++++++++++++++++++++++++------------------------- 2 files changed, 200 insertions(+), 184 deletions(-) diff --git a/collector.rb b/collector.rb index 658ffa9..5797e7f 100755 --- a/collector.rb +++ b/collector.rb @@ -11,40 +11,48 @@ end class BlackboxSshd::Collector attr_reader :registry, :prober - def initialize registry: nil, prober: nil - @registry = registry || Prometheus::Client::Registry.new - @registry.gauge :sshd_up, docstring: 'Server is up and connection from clients are generally possible', labels: %i[] - @registry.gauge :sshd_host_certificate_serial, docstring: 'Host Certificates serial', labels: %i[] - @registry.gauge :sshd_probe_duration, docstring: 'Time elapsed to probe the SSH-Server', labels: %i[] + def initialize registry: nil, prober: nil + @registry = registry || Prometheus::Client::Registry.new + @registry.gauge :sshd_up, docstring: 'Server is up and connection from clients are generally possible', labels: %i[] + @registry.gauge :sshd_host_certificate_serial, docstring: 'Host Certificates serial', labels: %i[] + @registry.gauge :sshd_probe_duration, docstring: 'Time elapsed to probe the SSH-Server', labels: %i[] @registry.gauge :sshd_host, docstring: 'Provides informations about the remote host.', labels: %i[protocol software] @registry.gauge :sshd_host_key, docstring: 'Provides informations about the host key in labels. 1=key used, 0=no key (possible a certificate)', labels: %i[key] - @registry.gauge :sshd_host_certificate, docstring: 'Provides informations about the host_certificate in labels. 1=certificate used, 0=no certificate used (possible simple key)', labels: %i[key ca id] - @registry.gauge :sshd_host_certificate_valid_to, docstring: 'Certificate will usable till this time, then it will expire.' - @registry.gauge :sshd_host_certificate_valid_from, docstring: 'Certificate is usable from this time.' - @metrics = OpenStruct.new @registry.instance_variable_get( :@metrics) - @prober = prober || BlackboxSshd::Prober.new - end + @registry.gauge :sshd_host_certificate, docstring: 'Provides informations about the host_certificate in labels. 1=certificate used, 0=no certificate used (possible simple key)', labels: %i[key ca id] + @registry.gauge :sshd_host_certificate_valid_to, docstring: 'Certificate will usable till this time, then it will expire.' + @registry.gauge :sshd_host_certificate_valid_from, docstring: 'Certificate is usable from this time.' + @registry.gauge :sshd_host_supported_signature_algorithm, docstring: 'Server supported signature algorithm. Only 1. If not supported, it will not listed.', labels: %i[supported] + @registry.gauge :sshd_host_supported_authentications, docstring: 'Server supported authentication methods. Only 1. If not supported, it will not listed.', labels: %i[supported] + @metrics = OpenStruct.new @registry.instance_variable_get( :@metrics) + @prober = prober || BlackboxSshd::Prober.new + end - def collect hostident - r = @prober.probe hostident + def collect hostident + r = @prober.probe hostident @metrics.sshd_host.set 1, labels: {protocol: r[:protocol], software: r[:remote_software]} - if hc = r[:host_cert] + if hc = r[:host_cert] @metrics.sshd_host_certificate_serial.set hc.delete(:serial) @metrics.sshd_host_certificate_valid_from.set hc.delete(:valid_from).to_f @metrics.sshd_host_certificate_valid_to.set hc.delete(:valid_to).to_f - @metrics.sshd_host_certificate.set 1, labels: hc + @metrics.sshd_host_certificate.set 1, labels: hc else - @metrics.sshd_host_certificate.set 0, labels: {key: '', ca: '', id: ''} - end + @metrics.sshd_host_certificate.set 0, labels: {key: '', ca: '', id: ''} + end if hk = r[:host_key] @metrics.sshd_host_key.set 1, labels: {key: hk} else @metrics.sshd_host_key.set 0, labels: {key: ""} end - @metrics.sshd_up.set 0 == r[:status].exitstatus ? 1 : 0 - @metrics.sshd_probe_duration.set r[:duration] + r[:server_sig_algs].reverse.each_with_index do |alg, i| + @metrics.sshd_host_supported_signature_algorithm.set i+1, labels: {supported: alg} + end + r[:authentications].reverse.each_with_index do |alg, i| + @metrics.sshd_host_supported_authentications.set i+1, labels: {supported: alg} + end + @metrics.sshd_up.set 0 == r[:status].exitstatus ? 1 : 0 + @metrics.sshd_probe_duration.set r[:duration] self - end + end def self.collect hostident, **opts new(**opts).collect hostident @@ -52,6 +60,6 @@ class BlackboxSshd::Collector end if __FILE__ == $0 - collector = BlackboxSshd::Collector.collect ARGV[0] - puts Prometheus::Client::Formats::Text.marshal( collector.registry) + collector = BlackboxSshd::Collector.collect ARGV[0] + puts Prometheus::Client::Formats::Text.marshal( collector.registry) end diff --git a/probe.rb b/probe.rb index bc620f9..94c3ae6 100755 --- a/probe.rb +++ b/probe.rb @@ -1,4 +1,5 @@ #!/usr/bin/env ruby +# vim: set noet sw=2 ts=2 sts=2: require 'time' @@ -6,189 +7,196 @@ module BlackboxSshd end class Popen3 - attr_reader :pid, :in, :out, :err + attr_reader :pid, :in, :out, :err - def initialize *cmd - pin, pout, perr = IO.pipe, IO.pipe, IO.pipe - @pid = Process.spawn *cmd, close_others: true, in: pin[0], out: pout[1], err: perr[1] - [pin[0], pout[1], perr[1]].map &:close - @in, @out, @err = pin[1], pout[0], perr[0] - end + def initialize *cmd + pin, pout, perr = IO.pipe, IO.pipe, IO.pipe + @pid = Process.spawn *cmd, close_others: true, in: pin[0], out: pout[1], err: perr[1] + [pin[0], pout[1], perr[1]].map &:close + @in, @out, @err = pin[1], pout[0], perr[0] + end - def wait(flags=0) Process.wait @pid, flags end - alias :waitpid :wait - def wait2(flags=0) Process.wait2 @pid, flags end - alias :waitpid2 :wait2 + def wait(flags=0) Process.wait @pid, flags end + alias :waitpid :wait + def wait2(flags=0) Process.wait2 @pid, flags end + alias :waitpid2 :wait2 - READ_PARTIAL_BYTES = 10*1024 + READ_PARTIAL_BYTES = 10*1024 - def _io_op xs, io, buf, type, &exe - if io.eof? - xs.delete io - xs.delete_at 0 while xs[0]&.eof? - else - buf << io.read( READ_PARTIAL_BYTES) - while line = buf.slice!( /\A.*?\n/) - yield line, type - end - end - end - private :_io_op + def _io_op xs, io, buf, type, &exe + if io.eof? + xs.delete io + xs.delete_at 0 while xs[0]&.eof? + else + buf << io.read( READ_PARTIAL_BYTES) + while line = buf.slice!( /\A.*?\n/) + yield line, type + end + end + end + private :_io_op - def each_line &exe - return LineEnumerator.new( self).each( &exe) + def each_line &exe + return LineEnumerator.new( self).each( &exe) - return to_enum(__method__) unless block_given? - xs = [@out, @err] - outbuf, errbuf = "", "" - while not xs.empty? - rs, ws, es = IO.select( xs, nil, xs) - rs.each do |r| - if @out == r - _io_op xs, @out, outbuf, :out, &exe - elsif @err == r - _io_op xs, @err, errbuf, :err, &exe - else - raise "IO.select returned something else than @out or @err." - end - end - end - yield outbuf, :out unless outbuf.empty? - yield errbuf, :err unless errbuf.empty? - self - end + return to_enum(__method__) unless block_given? + xs = [@out, @err] + outbuf, errbuf = "", "" + while not xs.empty? + rs, ws, es = IO.select( xs, nil, xs) + rs.each do |r| + if @out == r + _io_op xs, @out, outbuf, :out, &exe + elsif @err == r + _io_op xs, @err, errbuf, :err, &exe + else + raise "IO.select returned something else than @out or @err." + end + end + end + yield outbuf, :out unless outbuf.empty? + yield errbuf, :err unless errbuf.empty? + self + end - class LineEnumerator - include Enumerable - attr_reader :obj + class LineEnumerator + include Enumerable + attr_reader :obj - def initialize obj - @out, @err = obj.out, obj.err - @xs = [@out, @err] - @outbuf, @errbuf = "", "" - end + def initialize obj + @out, @err = obj.out, obj.err + @xs = [@out, @err] + @outbuf, @errbuf = "", "" + end - def _io_op io, buf, type, &exe - if io.eof? - @xs.delete io - @xs.delete_at 0 while @xs[0]&.eof? - else - buf << io.read( READ_PARTIAL_BYTES) - while line = buf.slice!( /\A.*?\n/) - yield line, type - end - end - end - private :_io_op + def _io_op io, buf, type, &exe + if io.eof? + @xs.delete io + @xs.delete_at 0 while @xs[0]&.eof? + else + buf << io.read( READ_PARTIAL_BYTES) + while line = buf.slice!( /\A.*?\n/) + yield line, type + end + end + end + private :_io_op - def each &exe - return self unless block_given? - while line = @errbuf.slice!( /\A.*?\n/) - yield line, :err - end - while line = @outbuf.slice!( /\A.*?\n/) - yield line, :out - end - until @xs.empty? - rs, ws, es = IO.select( @xs, nil, @xs) - rs.each do |r| - if @out == r - _io_op @out, @outbuf, :out, &exe - elsif @err == r - _io_op @err, @errbuf, :err, &exe - else - raise "IO.select returned something else than @out or @err." - end - end - end - self - end + def each &exe + return self unless block_given? + while line = @errbuf.slice!( /\A.*?\n/) + yield line, :err + end + while line = @outbuf.slice!( /\A.*?\n/) + yield line, :out + end + until @xs.empty? + rs, ws, es = IO.select( @xs, nil, @xs) + rs.each do |r| + if @out == r + _io_op @out, @outbuf, :out, &exe + elsif @err == r + _io_op @err, @errbuf, :err, &exe + else + raise "IO.select returned something else than @out or @err." + end + end + end + self + end - def next - to_enum( :each).next - end - end + def next + to_enum( :each).next + end + end - def close - @in.close unless @in&.closed? - @out.close unless @out&.closed? - @err.close unless @err&.closed? - end + def close + @in.close unless @in&.closed? + @out.close unless @out&.closed? + @err.close unless @err&.closed? + end end class BlackboxSshd::Prober - DefaultSshOpts = { - HostbasedKeyTypes: 'ssh-ed25519-cert-v01@openssh.com', - PreferredAuthentications: :publickey, - IdentitiesOnly: true, - IdentityFile: '~/.ssh/id_ed25519' - } + DefaultSshOpts = { + HostbasedKeyTypes: 'ssh-ed25519-cert-v01@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256,ssh-rsa', + PreferredAuthentications: :publickey, + IdentitiesOnly: true, + IdentityFile: '~/.ssh/id_ed25519', + CheckHostIP: false, + StrictHostKeyChecking: false, + UpdateHostKeys: 'no', + UserKnownHostsFile: '/dev/null', + } - attr_reader :ssh_opts + attr_reader :ssh_opts - def initialize **ssh_opts - @ssh_opts = DefaultSshOpts.update( ssh_opts).update BatchMode: true - end + def initialize **ssh_opts + @ssh_opts = DefaultSshOpts.update( ssh_opts).update BatchMode: true + end - def ssh_opts_list - @ssh_opts.map do|k,v| - case v - when true then "-o#{k}=yes" - when false then "-o#{k}=no" - else "-o#{k}=#{v}" - end - end - end + def ssh_opts_list + @ssh_opts.map do|k,v| + case v + when true then "-o#{k}=yes" + when false then "-o#{k}=no" + else "-o#{k}=#{v}" + end + end + end - def probe hostident - r = {lines: [], start: Time.now} - ssh = Popen3.new *%w[ssh -v], *ssh_opts_list, hostident, "true" - lines = ssh.each_line.to_a - ssh.close - r[:status] = ssh.wait2[1] - r[:stop] = Time.now - r[:duration] = r[:stop] - r[:start] - lines.each do |line, type| - r[:lines].push line - case line = line.chomp - when /\Adebug1: Remote protocol version (.*?), remote software version (.*?)\z/ - r[:protocol] = $1 - r[:remote_software] = $2 - when /\Adebug1: Server host key: (.*?)\z/ - r[:host_key] = $1 - when /\Adebug1: Server host certificate: (.*?)\z/ - # ssh-ed25519-cert-v01@openssh.com SHA256:P3b20g3rde66C7kDUF+/rV/CC3s5EaoUoZ35oyxs8aA, serial 43 ID \"host: gtw2\" CA ssh-ed25519 SHA256:9gmtFgVB7VfFE8/UYC22xmToHyDQ23arMQBtsir9w9E valid from 2022-03-02T00:00:00 to 2023-02-25T00:00:00 - meta = $1 - c = {} - c[:key] = $1 if %r{\A([^ ]+ [^ ]+),} =~ meta - c[:serial] = $1.to_i if %r{\bserial (\d+) } =~ meta - c[:id] = $1 if %r{\bID "(.*?)" } =~ meta - c[:ca] = $1 if %r{\bCA ([^ ]+ [^ ]+) } =~ meta - if %r{\bvalid from ([^ ]+) to ([^ ]+)\b} =~ meta - c[:valid_from], c[:valid_to] = Time.parse($1), Time.parse($2) - end - r[:host_cert] = c - when /\Adebug1: Host '(.*?)' is known and matches the (.*?) host certificate.\z/ - r[:host_in_cert], r[:host_cert_type] = $1, $2 - when /\Adebug1: kex_input_ext_info: server-sig-algs=<(.*?)>\z/ - r[:server_sig_algs] = $1.split ',' - when /\Adebug1: Authentications that can continue: (.*?)\z/ - r[:authentications] = $1.split ',' - end - end - r - end + def probe hostident + r = {lines: [], start: Time.now} + cmd = %w[ssh -v] + ssh_opts_list + [hostident, "true"] + r[:command] = cmd + ssh = Popen3.new *cmd + lines = ssh.each_line.to_a + ssh.close + r[:status] = ssh.wait2[1] + r[:stop] = Time.now + r[:duration] = r[:stop] - r[:start] + lines.each do |line, type| + r[:lines].push line + case line = line.chomp + when /\Adebug1: Remote protocol version (.*?), remote software version (.*?)\z/ + r[:protocol] = $1 + r[:remote_software] = $2 + when /\Adebug1: Server host key: (.*?)\z/ + r[:host_key] = $1 + when /\Adebug1: Server host certificate: (.*?)\z/ + meta = $1 + c = {} + c[:key] = $1 if %r{\A([^ ]+ [^ ]+),} =~ meta + c[:serial] = $1.to_i if %r{\bserial (\d+) } =~ meta + c[:id] = $1 if %r{\bID "(.*?)" } =~ meta + c[:ca] = $1 if %r{\bCA ([^ ]+ [^ ]+) } =~ meta + if %r{\bvalid from ([^ ]+) to ([^ ]+)\b} =~ meta + c[:valid_from], c[:valid_to] = Time.parse($1), Time.parse($2) + end + r[:host_cert] = c + when /\Adebug1: Host '(.*?)' is known and matches the (.*?) host certificate.\z/ + r[:host_in_cert], r[:host_cert_type] = $1, $2 + when /\Adebug1: kex_input_ext_info: server-sig-algs=<(.*?)>\z/ + r[:server_sig_algs] = $1.split ',' + when /\Adebug1: Authentications that can continue: (.*?)\z/ + r[:authentications] = $1.split ',' + end + end + r + end - def self.probe hostident, **ssh_opts - self.new( **ssh_opts).probe hostident - end + def self.probe hostident, **ssh_opts + self.new( **ssh_opts).probe hostident + end end if __FILE__ == $0 - require 'yaml' - require 'json' - r = BlackboxSshd::Prober.probe( ARGV[0]) - STDERR.puts r.delete( :lines) - STDERR.puts - puts JSON.parse(r.to_json).to_yaml + require 'yaml' + require 'json' + require 'shellwords' + r = BlackboxSshd::Prober.probe( ARGV[0]) + STDERR.puts "# #{r.delete( :command).shelljoin}" + STDERR.puts r.delete( :lines) + STDERR.puts + puts JSON.parse(r.to_json).to_yaml end