diff options
Diffstat (limited to 'knife/lib/chef/knife/ssl_check.rb')
-rw-r--r-- | knife/lib/chef/knife/ssl_check.rb | 284 |
1 files changed, 284 insertions, 0 deletions
diff --git a/knife/lib/chef/knife/ssl_check.rb b/knife/lib/chef/knife/ssl_check.rb new file mode 100644 index 0000000000..c829e7938b --- /dev/null +++ b/knife/lib/chef/knife/ssl_check.rb @@ -0,0 +1,284 @@ +# +# Author:: Daniel DeLeo (<dan@chef.io>) +# Copyright:: Copyright (c) Chef Software Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require_relative "../knife" +require "chef-utils/dist" unless defined?(ChefUtils::Dist) + +class Chef + class Knife + class SslCheck < Chef::Knife + + deps do + require "chef/config" unless defined?(Chef::Config) + require "pp" unless defined?(PP) + require "socket" unless defined?(Socket) + require "uri" unless defined?(URI) + require "chef/http/ssl_policies" unless defined?(Chef::HTTP::DefaultSSLPolicy) + require "openssl" unless defined?(OpenSSL) + require "chef/mixin/proxified_socket" unless defined?(Chef::Mixin::ProxifiedSocket) + include Chef::Mixin::ProxifiedSocket + end + + banner "knife ssl check [URL] (options)" + + def initialize(*args) + @host = nil + @verify_peer_socket = nil + @ssl_policy = HTTP::DefaultSSLPolicy + super + end + + def uri + @uri ||= begin + Chef::Log.trace("Checking SSL cert on #{given_uri}") + URI.parse(given_uri) + end + end + + def given_uri + (name_args[0] || Chef::Config.chef_server_url) + end + + def host + uri.host + end + + def port + uri.port + end + + def validate_uri + unless host && port + invalid_uri! + end + rescue URI::Error + invalid_uri! + end + + def invalid_uri! + ui.error("Given URI: `#{given_uri}' is invalid") + show_usage + exit 1 + end + + def verify_peer_socket + @verify_peer_socket ||= begin + tcp_connection = proxified_socket(host, port) + ssl_client = OpenSSL::SSL::SSLSocket.new(tcp_connection, verify_peer_ssl_context) + ssl_client.hostname = host + ssl_client + end + end + + def verify_peer_ssl_context + @verify_peer_ssl_context ||= begin + verify_peer_context = OpenSSL::SSL::SSLContext.new + @ssl_policy.apply_to(verify_peer_context) + verify_peer_context.verify_mode = OpenSSL::SSL::VERIFY_PEER + verify_peer_context + end + end + + def noverify_socket + @noverify_socket ||= begin + tcp_connection = proxified_socket(host, port) + OpenSSL::SSL::SSLSocket.new(tcp_connection, noverify_peer_ssl_context) + end + end + + def noverify_peer_ssl_context + @noverify_peer_ssl_context ||= begin + noverify_peer_context = OpenSSL::SSL::SSLContext.new + @ssl_policy.apply_to(noverify_peer_context) + noverify_peer_context.verify_mode = OpenSSL::SSL::VERIFY_NONE + noverify_peer_context + end + end + + def verify_X509 + cert_debug_msg = "" + trusted_certificates.each do |cert_name| + message = check_X509_certificate(cert_name) + unless message.nil? + cert_debug_msg << File.expand_path(cert_name) + ": " + message + "\n" + end + end + + unless cert_debug_msg.empty? + debug_invalid_X509(cert_debug_msg) + end + + true # Maybe the bad certs won't hurt... + end + + def verify_cert + ui.msg("Connecting to host #{host}:#{port}") + verify_peer_socket.connect + true + rescue OpenSSL::SSL::SSLError => e + ui.error "The SSL certificate of #{host} could not be verified" + Chef::Log.trace e.message + debug_invalid_cert + false + end + + def verify_cert_host + verify_peer_socket.post_connection_check(host) + true + rescue OpenSSL::SSL::SSLError => e + ui.error "The SSL cert is signed by a trusted authority but is not valid for the given hostname" + Chef::Log.trace(e) + debug_invalid_host + false + end + + def debug_invalid_X509(cert_debug_msg) + ui.msg("\n#{ui.color("Configuration Info:", :bold)}\n\n") + debug_ssl_settings + debug_chef_ssl_config + + ui.warn(<<~BAD_CERTS) + There are invalid certificates in your trusted_certs_dir. + OpenSSL will not use the following certificates when verifying SSL connections: + + #{cert_debug_msg} + + #{ui.color("TO FIX THESE WARNINGS:", :bold)} + + We are working on documentation for resolving common issues uncovered here. + + * If the certificate is generated by the server, you may try redownloading the + server's certificate. By default, the certificate is stored in the following + location on the host where your chef-server runs: + + /var/opt/opscode/nginx/ca/SERVER_HOSTNAME.crt + + Copy that file to your trusted_certs_dir (currently: #{configuration.trusted_certs_dir}) + using SSH/SCP or some other secure method, then re-run this command to confirm + that the server's certificate is now trusted. + + BAD_CERTS + # @TODO: ^ needs URL once documentation is posted. + end + + def debug_invalid_cert + noverify_socket.connect + issuer_info = noverify_socket.peer_cert.issuer + ui.msg("Certificate issuer data: #{issuer_info}") + + ui.msg("\n#{ui.color("Configuration Info:", :bold)}\n\n") + debug_ssl_settings + debug_chef_ssl_config + + ui.err(<<~ADVICE) + + #{ui.color("TO FIX THIS ERROR:", :bold)} + + If the server you are connecting to uses a self-signed certificate, you must + configure #{ChefUtils::Dist::Infra::PRODUCT} to trust that server's certificate. + + By default, the certificate is stored in the following location on the host + where your chef-server runs: + + /var/opt/opscode/nginx/ca/SERVER_HOSTNAME.crt + + Copy that file to your trusted_certs_dir (currently: #{configuration.trusted_certs_dir}) + using SSH/SCP or some other secure method, then re-run this command to confirm + that the server's certificate is now trusted. + + ADVICE + end + + def debug_invalid_host + noverify_socket.connect + subject = noverify_socket.peer_cert.subject + cn_field_tuple = subject.to_a.find { |field| field[0] == "CN" } + cn = cn_field_tuple[1] + + ui.error("You are attempting to connect to: '#{host}'") + ui.error("The server's certificate belongs to '#{cn}'") + ui.err(<<~ADVICE) + + #{ui.color("TO FIX THIS ERROR:", :bold)} + + The solution for this issue depends on your networking configuration. If you + are able to connect to this server using the hostname #{cn} + instead of #{host}, then you can resolve this issue by updating chef_server_url + in your configuration file. + + If you are not able to connect to the server using the hostname #{cn} + you will have to update the certificate on the server to use the correct hostname. + ADVICE + end + + def debug_ssl_settings + ui.err "OpenSSL Configuration:" + ui.err "* Version: #{OpenSSL::OPENSSL_VERSION}" + ui.err "* Certificate file: #{OpenSSL::X509::DEFAULT_CERT_FILE}" + ui.err "* Certificate directory: #{OpenSSL::X509::DEFAULT_CERT_DIR}" + end + + def debug_chef_ssl_config + ui.err "#{ChefUtils::Dist::Infra::PRODUCT} SSL Configuration:" + ui.err "* ssl_ca_path: #{configuration.ssl_ca_path.inspect}" + ui.err "* ssl_ca_file: #{configuration.ssl_ca_file.inspect}" + ui.err "* trusted_certs_dir: #{configuration.trusted_certs_dir.inspect}" + end + + def configuration + Chef::Config + end + + def run + validate_uri + + if verify_X509 && verify_cert && verify_cert_host + ui.msg "Successfully verified certificates from `#{host}'" + else + exit 1 + end + end + + private + + def trusted_certificates + if configuration.trusted_certs_dir && Dir.exist?(configuration.trusted_certs_dir) + glob_dir = ChefConfig::PathHelper.escape_glob_dir(configuration.trusted_certs_dir) + Dir.glob(File.join(glob_dir, "*.{crt,pem}")) + else + [] + end + end + + def check_X509_certificate(cert_file) + store = OpenSSL::X509::Store.new + cert = OpenSSL::X509::Certificate.new(IO.read(File.expand_path(cert_file))) + begin + store.add_cert(cert) + # test if the store can verify the cert we just added + unless store.verify(cert) # true if verified, false if not + return store.error_string + end + rescue OpenSSL::X509::StoreError => e + return e.message + end + nil + end + end + end +end |