# # Author:: Daniel DeLeo () # 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_relative "../config" require "pp" unless defined?(PP) require "socket" unless defined?(Socket) require "uri" unless defined?(URI) require_relative "../http/ssl_policies" require "openssl" unless defined?(OpenSSL) require_relative "../mixin/proxified_socket" 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