List supported Algorithms and SignatureAlgorithms. No Hostkey verification (/dev/null, no strict).

This commit is contained in:
Denis Knauf 2022-03-29 15:59:38 +02:00
parent 94d241c6f1
commit d4691e1411
2 changed files with 200 additions and 184 deletions

View file

@ -11,40 +11,48 @@ end
class BlackboxSshd::Collector class BlackboxSshd::Collector
attr_reader :registry, :prober attr_reader :registry, :prober
def initialize registry: nil, prober: nil def initialize registry: nil, prober: nil
@registry = registry || Prometheus::Client::Registry.new @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_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_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_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, 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_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, 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_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_certificate_valid_from, docstring: 'Certificate is usable from this time.'
@metrics = OpenStruct.new @registry.instance_variable_get( :@metrics) @registry.gauge :sshd_host_supported_signature_algorithm, docstring: 'Server supported signature algorithm. Only 1. If not supported, it will not listed.', labels: %i[supported]
@prober = prober || BlackboxSshd::Prober.new @registry.gauge :sshd_host_supported_authentications, docstring: 'Server supported authentication methods. Only 1. If not supported, it will not listed.', labels: %i[supported]
end @metrics = OpenStruct.new @registry.instance_variable_get( :@metrics)
@prober = prober || BlackboxSshd::Prober.new
end
def collect hostident def collect hostident
r = @prober.probe hostident r = @prober.probe hostident
@metrics.sshd_host.set 1, labels: {protocol: r[:protocol], software: r[:remote_software]} @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_serial.set hc.delete(:serial)
@metrics.sshd_host_certificate_valid_from.set hc.delete(:valid_from).to_f @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_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 else
@metrics.sshd_host_certificate.set 0, labels: {key: '', ca: '', id: ''} @metrics.sshd_host_certificate.set 0, labels: {key: '', ca: '', id: ''}
end end
if hk = r[:host_key] if hk = r[:host_key]
@metrics.sshd_host_key.set 1, labels: {key: hk} @metrics.sshd_host_key.set 1, labels: {key: hk}
else else
@metrics.sshd_host_key.set 0, labels: {key: ""} @metrics.sshd_host_key.set 0, labels: {key: ""}
end end
@metrics.sshd_up.set 0 == r[:status].exitstatus ? 1 : 0 r[:server_sig_algs].reverse.each_with_index do |alg, i|
@metrics.sshd_probe_duration.set r[:duration] @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 self
end end
def self.collect hostident, **opts def self.collect hostident, **opts
new(**opts).collect hostident new(**opts).collect hostident
@ -52,6 +60,6 @@ class BlackboxSshd::Collector
end end
if __FILE__ == $0 if __FILE__ == $0
collector = BlackboxSshd::Collector.collect ARGV[0] collector = BlackboxSshd::Collector.collect ARGV[0]
puts Prometheus::Client::Formats::Text.marshal( collector.registry) puts Prometheus::Client::Formats::Text.marshal( collector.registry)
end end

332
probe.rb
View file

@ -1,4 +1,5 @@
#!/usr/bin/env ruby #!/usr/bin/env ruby
# vim: set noet sw=2 ts=2 sts=2:
require 'time' require 'time'
@ -6,189 +7,196 @@ module BlackboxSshd
end end
class Popen3 class Popen3
attr_reader :pid, :in, :out, :err attr_reader :pid, :in, :out, :err
def initialize *cmd def initialize *cmd
pin, pout, perr = IO.pipe, IO.pipe, IO.pipe 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] @pid = Process.spawn *cmd, close_others: true, in: pin[0], out: pout[1], err: perr[1]
[pin[0], pout[1], perr[1]].map &:close [pin[0], pout[1], perr[1]].map &:close
@in, @out, @err = pin[1], pout[0], perr[0] @in, @out, @err = pin[1], pout[0], perr[0]
end end
def wait(flags=0) Process.wait @pid, flags end def wait(flags=0) Process.wait @pid, flags end
alias :waitpid :wait alias :waitpid :wait
def wait2(flags=0) Process.wait2 @pid, flags end def wait2(flags=0) Process.wait2 @pid, flags end
alias :waitpid2 :wait2 alias :waitpid2 :wait2
READ_PARTIAL_BYTES = 10*1024 READ_PARTIAL_BYTES = 10*1024
def _io_op xs, io, buf, type, &exe def _io_op xs, io, buf, type, &exe
if io.eof? if io.eof?
xs.delete io xs.delete io
xs.delete_at 0 while xs[0]&.eof? xs.delete_at 0 while xs[0]&.eof?
else else
buf << io.read( READ_PARTIAL_BYTES) buf << io.read( READ_PARTIAL_BYTES)
while line = buf.slice!( /\A.*?\n/) while line = buf.slice!( /\A.*?\n/)
yield line, type yield line, type
end end
end end
end end
private :_io_op private :_io_op
def each_line &exe def each_line &exe
return LineEnumerator.new( self).each( &exe) return LineEnumerator.new( self).each( &exe)
return to_enum(__method__) unless block_given? return to_enum(__method__) unless block_given?
xs = [@out, @err] xs = [@out, @err]
outbuf, errbuf = "", "" outbuf, errbuf = "", ""
while not xs.empty? while not xs.empty?
rs, ws, es = IO.select( xs, nil, xs) rs, ws, es = IO.select( xs, nil, xs)
rs.each do |r| rs.each do |r|
if @out == r if @out == r
_io_op xs, @out, outbuf, :out, &exe _io_op xs, @out, outbuf, :out, &exe
elsif @err == r elsif @err == r
_io_op xs, @err, errbuf, :err, &exe _io_op xs, @err, errbuf, :err, &exe
else else
raise "IO.select returned something else than @out or @err." raise "IO.select returned something else than @out or @err."
end end
end end
end end
yield outbuf, :out unless outbuf.empty? yield outbuf, :out unless outbuf.empty?
yield errbuf, :err unless errbuf.empty? yield errbuf, :err unless errbuf.empty?
self self
end end
class LineEnumerator class LineEnumerator
include Enumerable include Enumerable
attr_reader :obj attr_reader :obj
def initialize obj def initialize obj
@out, @err = obj.out, obj.err @out, @err = obj.out, obj.err
@xs = [@out, @err] @xs = [@out, @err]
@outbuf, @errbuf = "", "" @outbuf, @errbuf = "", ""
end end
def _io_op io, buf, type, &exe def _io_op io, buf, type, &exe
if io.eof? if io.eof?
@xs.delete io @xs.delete io
@xs.delete_at 0 while @xs[0]&.eof? @xs.delete_at 0 while @xs[0]&.eof?
else else
buf << io.read( READ_PARTIAL_BYTES) buf << io.read( READ_PARTIAL_BYTES)
while line = buf.slice!( /\A.*?\n/) while line = buf.slice!( /\A.*?\n/)
yield line, type yield line, type
end end
end end
end end
private :_io_op private :_io_op
def each &exe def each &exe
return self unless block_given? return self unless block_given?
while line = @errbuf.slice!( /\A.*?\n/) while line = @errbuf.slice!( /\A.*?\n/)
yield line, :err yield line, :err
end end
while line = @outbuf.slice!( /\A.*?\n/) while line = @outbuf.slice!( /\A.*?\n/)
yield line, :out yield line, :out
end end
until @xs.empty? until @xs.empty?
rs, ws, es = IO.select( @xs, nil, @xs) rs, ws, es = IO.select( @xs, nil, @xs)
rs.each do |r| rs.each do |r|
if @out == r if @out == r
_io_op @out, @outbuf, :out, &exe _io_op @out, @outbuf, :out, &exe
elsif @err == r elsif @err == r
_io_op @err, @errbuf, :err, &exe _io_op @err, @errbuf, :err, &exe
else else
raise "IO.select returned something else than @out or @err." raise "IO.select returned something else than @out or @err."
end end
end end
end end
self self
end end
def next def next
to_enum( :each).next to_enum( :each).next
end end
end end
def close def close
@in.close unless @in&.closed? @in.close unless @in&.closed?
@out.close unless @out&.closed? @out.close unless @out&.closed?
@err.close unless @err&.closed? @err.close unless @err&.closed?
end end
end end
class BlackboxSshd::Prober class BlackboxSshd::Prober
DefaultSshOpts = { DefaultSshOpts = {
HostbasedKeyTypes: 'ssh-ed25519-cert-v01@openssh.com', HostbasedKeyTypes: 'ssh-ed25519-cert-v01@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256,ssh-rsa',
PreferredAuthentications: :publickey, PreferredAuthentications: :publickey,
IdentitiesOnly: true, IdentitiesOnly: true,
IdentityFile: '~/.ssh/id_ed25519' 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 def initialize **ssh_opts
@ssh_opts = DefaultSshOpts.update( ssh_opts).update BatchMode: true @ssh_opts = DefaultSshOpts.update( ssh_opts).update BatchMode: true
end end
def ssh_opts_list def ssh_opts_list
@ssh_opts.map do|k,v| @ssh_opts.map do|k,v|
case v case v
when true then "-o#{k}=yes" when true then "-o#{k}=yes"
when false then "-o#{k}=no" when false then "-o#{k}=no"
else "-o#{k}=#{v}" else "-o#{k}=#{v}"
end end
end end
end end
def probe hostident def probe hostident
r = {lines: [], start: Time.now} r = {lines: [], start: Time.now}
ssh = Popen3.new *%w[ssh -v], *ssh_opts_list, hostident, "true" cmd = %w[ssh -v] + ssh_opts_list + [hostident, "true"]
lines = ssh.each_line.to_a r[:command] = cmd
ssh.close ssh = Popen3.new *cmd
r[:status] = ssh.wait2[1] lines = ssh.each_line.to_a
r[:stop] = Time.now ssh.close
r[:duration] = r[:stop] - r[:start] r[:status] = ssh.wait2[1]
lines.each do |line, type| r[:stop] = Time.now
r[:lines].push line r[:duration] = r[:stop] - r[:start]
case line = line.chomp lines.each do |line, type|
when /\Adebug1: Remote protocol version (.*?), remote software version (.*?)\z/ r[:lines].push line
r[:protocol] = $1 case line = line.chomp
r[:remote_software] = $2 when /\Adebug1: Remote protocol version (.*?), remote software version (.*?)\z/
when /\Adebug1: Server host key: (.*?)\z/ r[:protocol] = $1
r[:host_key] = $1 r[:remote_software] = $2
when /\Adebug1: Server host certificate: (.*?)\z/ when /\Adebug1: Server host key: (.*?)\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 r[:host_key] = $1
meta = $1 when /\Adebug1: Server host certificate: (.*?)\z/
c = {} meta = $1
c[:key] = $1 if %r{\A([^ ]+ [^ ]+),} =~ meta c = {}
c[:serial] = $1.to_i if %r{\bserial (\d+) } =~ meta c[:key] = $1 if %r{\A([^ ]+ [^ ]+),} =~ meta
c[:id] = $1 if %r{\bID "(.*?)" } =~ meta c[:serial] = $1.to_i if %r{\bserial (\d+) } =~ meta
c[:ca] = $1 if %r{\bCA ([^ ]+ [^ ]+) } =~ meta c[:id] = $1 if %r{\bID "(.*?)" } =~ meta
if %r{\bvalid from ([^ ]+) to ([^ ]+)\b} =~ meta c[:ca] = $1 if %r{\bCA ([^ ]+ [^ ]+) } =~ meta
c[:valid_from], c[:valid_to] = Time.parse($1), Time.parse($2) if %r{\bvalid from ([^ ]+) to ([^ ]+)\b} =~ meta
end c[:valid_from], c[:valid_to] = Time.parse($1), Time.parse($2)
r[:host_cert] = c end
when /\Adebug1: Host '(.*?)' is known and matches the (.*?) host certificate.\z/ r[:host_cert] = c
r[:host_in_cert], r[:host_cert_type] = $1, $2 when /\Adebug1: Host '(.*?)' is known and matches the (.*?) host certificate.\z/
when /\Adebug1: kex_input_ext_info: server-sig-algs=<(.*?)>\z/ r[:host_in_cert], r[:host_cert_type] = $1, $2
r[:server_sig_algs] = $1.split ',' when /\Adebug1: kex_input_ext_info: server-sig-algs=<(.*?)>\z/
when /\Adebug1: Authentications that can continue: (.*?)\z/ r[:server_sig_algs] = $1.split ','
r[:authentications] = $1.split ',' when /\Adebug1: Authentications that can continue: (.*?)\z/
end r[:authentications] = $1.split ','
end end
r end
end r
end
def self.probe hostident, **ssh_opts def self.probe hostident, **ssh_opts
self.new( **ssh_opts).probe hostident self.new( **ssh_opts).probe hostident
end end
end end
if __FILE__ == $0 if __FILE__ == $0
require 'yaml' require 'yaml'
require 'json' require 'json'
r = BlackboxSshd::Prober.probe( ARGV[0]) require 'shellwords'
STDERR.puts r.delete( :lines) r = BlackboxSshd::Prober.probe( ARGV[0])
STDERR.puts STDERR.puts "# #{r.delete( :command).shelljoin}"
puts JSON.parse(r.to_json).to_yaml STDERR.puts r.delete( :lines)
STDERR.puts
puts JSON.parse(r.to_json).to_yaml
end end