|
| 1 | +#! /usr/bin/env ruby |
| 2 | +# |
| 3 | +# check-ssl-root-issuer |
| 4 | +# |
| 5 | +# DESCRIPTION: |
| 6 | +# Check that a certificate is chained to a specific root certificate issuer |
| 7 | +# |
| 8 | +# OUTPUT: |
| 9 | +# plain text |
| 10 | +# |
| 11 | +# PLATFORMS: |
| 12 | +# Linux |
| 13 | +# |
| 14 | +# DEPENDENCIES: |
| 15 | +# gem: sensu-plugin |
| 16 | +# |
| 17 | +# USAGE: |
| 18 | +# |
| 19 | +# Check that a specific website is chained to a specific root certificate |
| 20 | +# ./check-ssl-root-issuer.rb \ |
| 21 | +# -u https://example.com \ |
| 22 | +# -i "CN=DST Root CA X3,O=Digital Signature Trust Co." |
| 23 | +# |
| 24 | +# LICENSE: |
| 25 | +# Copyright Jef Spaleta (jspaleta@gmail.com) 2020 |
| 26 | +# Released under the same terms as Sensu (the MIT license); see LICENSE |
| 27 | +# for details. |
| 28 | +# |
| 29 | + |
| 30 | +require 'sensu-plugin/check/cli' |
| 31 | +require 'openssl' |
| 32 | +require 'uri' |
| 33 | +require 'net/http' |
| 34 | +require 'net/https' |
| 35 | + |
| 36 | +# |
| 37 | +# Check root certificate has specified issuer name |
| 38 | +# |
| 39 | +class CheckSSLRootIssuer < Sensu::Plugin::Check::CLI |
| 40 | + option :url, |
| 41 | + description: 'Url to check: Ex "https://google.com"', |
| 42 | + short: '-u', |
| 43 | + long: '--url URL', |
| 44 | + required: true |
| 45 | + |
| 46 | + option :issuer, |
| 47 | + description: 'An X509 certificate issuer name, RFC2253 format Ex: "CN=DST Root CA X3,O=Digital Signature Trust Co."', |
| 48 | + short: '-i', |
| 49 | + long: '--issuer ISSUER_NAME', |
| 50 | + required: true |
| 51 | + |
| 52 | + option :regexp, |
| 53 | + description: 'Treat the issuer name as a regexp', |
| 54 | + short: '-r', |
| 55 | + long: '--regexp', |
| 56 | + default: false, |
| 57 | + boolean: true, |
| 58 | + required: false |
| 59 | + |
| 60 | + option :format, |
| 61 | + description: 'optional issuer name format.', |
| 62 | + short: '-f', |
| 63 | + long: '--format FORMAT_VAL', |
| 64 | + default: 'RFC2253', |
| 65 | + in: %w('RFC2253', 'ONELINE', 'COMPAT'), |
| 66 | + required: false |
| 67 | + |
| 68 | + def cert_name_format |
| 69 | + # Note: because format argument is pre-validated by mixin 'in' logic eval is safe to use |
| 70 | + eval "OpenSSL::X509::Name::#{config[:format]}" # rubocop:disable Lint/Eval |
| 71 | + end |
| 72 | + |
| 73 | + def validate_issuer(cert) |
| 74 | + issuer = cert.issuer.to_s(cert_name_format) |
| 75 | + if config[:regexp] |
| 76 | + issuer_regexp = Regexp.new(config[:issuer].to_s) |
| 77 | + issuer =~ issuer_regexp |
| 78 | + else |
| 79 | + issuer == config[:issuer].to_s |
| 80 | + end |
| 81 | + end |
| 82 | + |
| 83 | + def find_root_cert(uri) |
| 84 | + root_cert = nil |
| 85 | + http = Net::HTTP.new(uri.host, uri.port) |
| 86 | + http.open_timeout = 10 |
| 87 | + http.read_timeout = 10 |
| 88 | + http.use_ssl = true |
| 89 | + http.cert_store = OpenSSL::X509::Store.new |
| 90 | + http.cert_store.set_default_paths |
| 91 | + http.verify_mode = OpenSSL::SSL::VERIFY_PEER |
| 92 | + |
| 93 | + http.verify_callback = lambda { |verify_ok, store_context| |
| 94 | + root_cert = store_context.current_cert unless root_cert |
| 95 | + unless verify_ok |
| 96 | + @failed_cert = store_context.current_cert |
| 97 | + @failed_cert_reason = [store_context.error, store_context.error_string] if store_context.error != 0 |
| 98 | + end |
| 99 | + verify_ok |
| 100 | + } |
| 101 | + http.start {} |
| 102 | + root_cert |
| 103 | + end |
| 104 | + |
| 105 | + # Do the actual work and massage some data |
| 106 | + |
| 107 | + def run |
| 108 | + @fail_cert = nil |
| 109 | + @failed_cert_reason = 'Unknown' |
| 110 | + uri = URI.parse(config[:url]) |
| 111 | + critical "url protocol must be https, you specified #{url}" if uri.scheme != 'https' |
| 112 | + root_cert = find_root_cert(uri) |
| 113 | + if @failed_cert |
| 114 | + msg = "Certificate verification failed.\n Reason: #{@failed_cert_reason}" |
| 115 | + critical msg |
| 116 | + end |
| 117 | + |
| 118 | + if validate_issuer(root_cert) |
| 119 | + msg = 'Root certificate in chain has expected issuer name' |
| 120 | + ok msg |
| 121 | + else |
| 122 | + msg = "Root certificate issuer did not match expected name.\nFound: \"#{root_cert.issuer.to_s(config[:issuer_format])}\"" |
| 123 | + critical msg |
| 124 | + end |
| 125 | + end |
| 126 | +end |
0 commit comments