Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Winrm options defaults / precedence #454

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
196 changes: 172 additions & 24 deletions lib/chef/provisioning/aws_driver/driver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,44 @@ def user_data
netsh advfirewall firewall add rule name="WinRM 5985" protocol=TCP dir=in localport=5985 action=allow
netsh advfirewall firewall add rule name="WinRM 5986" protocol=TCP dir=in localport=5986 action=allow

net stop winrm
sc config winrm start=auto
net start winrm
</powershell>
EOD
end

def https_user_data
<<EOD
<powershell>
winrm quickconfig -q
winrm set winrm/config/winrs '@{MaxMemoryPerShellMB="300"}'
winrm set winrm/config '@{MaxTimeoutms="1800000"}'

netsh advfirewall firewall add rule name="WinRM 5986" protocol=TCP dir=in localport=5986 action=allow

$SourceStoreScope = 'LocalMachine'
$SourceStorename = 'Remote Desktop'

$SourceStore = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $SourceStorename, $SourceStoreScope
$SourceStore.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadOnly)

$cert = $SourceStore.Certificates | Where-Object -FilterScript {
$_.subject -like '*'
}

$DestStoreScope = 'LocalMachine'
$DestStoreName = 'My'

$DestStore = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $DestStoreName, $DestStoreScope
$DestStore.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
$DestStore.Add($cert)

$SourceStore.Close()
$DestStore.Close()

winrm create winrm/config/listener?Address=*+Transport=HTTPS `@`{Hostname=`"($certId)`"`;CertificateThumbprint=`"($cert.Thumbprint)`"`}

net stop winrm
sc config winrm start=auto
net start winrm
Expand Down Expand Up @@ -648,11 +686,14 @@ def ready_machine(action_handler, machine_spec, machine_options)
# sending out encrypted password, restarting instance, etc.
if machine_spec.reference['is_windows']
wait_until_machine(action_handler, machine_spec, "receive 'Windows is ready' message from the AWS console", instance) { |instance|
output = instance.console_output.output
if output.nil? || output.empty?
instance.console_output.output
# seems to be a bug as we need to run this twice
# to consistently ensure the output is fully pulled
encoded_output = instance.console_output.output
if encoded_output.nil? || encoded_output.empty?
false
else
output = Base64.decode64(output)
output = Base64.decode64(encoded_output)
output =~ /Message: Windows is Ready to use/
end
}
Expand Down Expand Up @@ -845,10 +886,18 @@ def bootstrap_options_for(action_handler, machine_spec, machine_options)
end

if machine_options[:is_windows]
Chef::Log.debug "Setting Default WinRM userdata for windows..."
bootstrap_options[:user_data] = Base64.encode64(user_data) if bootstrap_options[:user_data].nil?
Chef::Log.debug "Setting Default windows userdata based on WinRM transport"
if bootstrap_options[:user_data].nil?
case machine_options[:winrm_transport]
when 'https'
data = https_user_data
else
data = user_data
end
bootstrap_options[:user_data] = Base64.encode64(data)
end
else
Chef::Log.debug "Non-windows, not setting userdata"
Chef::Log.debug "Non-windows, not setting Default userdata"
end

bootstrap_options = AWSResource.lookup_options(bootstrap_options, managed_entry_store: machine_spec.managed_entry_store, driver: self)
Expand Down Expand Up @@ -885,6 +934,14 @@ def default_ssh_username
'ubuntu'
end

def default_winrm_username
'Administrator'
end

def default_winrm_transport
'http'
end

def keypair_for(bootstrap_options)
if bootstrap_options[:key_name]
keypair_name = bootstrap_options[:key_name]
Expand Down Expand Up @@ -984,31 +1041,122 @@ def default_ami_for_region(region, criteria = {})

def create_winrm_transport(machine_spec, machine_options, instance)
remote_host = determine_remote_host(machine_spec, instance)
username = machine_spec.reference['winrm_username'] ||
machine_options[:winrm_username] ||
default_winrm_username
# default to http for now, should upgrade to https when knife support self-signed
transport_type = machine_spec.reference['winrm_transport'] ||
machine_options[:winrm_transport] ||
default_winrm_transport
type = case transport_type
when 'http'
:plaintext
when 'https'
:ssl
end
port = machine_spec.reference['winrm_port'] ||
machine_options[:winrm_port] ||
case transport_type
when 'http'
'5985'
when 'https'
'5986'
end
endpoint = "#{transport_type}://#{remote_host}:#{port}/wsman"

port = machine_spec.reference['winrm_port'] || 5985
endpoint = "http://#{remote_host}:#{port}/wsman"
type = :plaintext
pem_bytes = get_private_key(instance.key_name)

# TODO plaintext password = bad
password = machine_spec.reference['winrm_password']
if password.nil? || password.empty?
encrypted_admin_password = instance.password_data.password_data
if encrypted_admin_password.nil? || encrypted_admin_password.empty?
raise "You did not specify winrm_password in the machine options and no encrytpted password could be fetched from the instance"
end
decoded = Base64.decode64(encrypted_admin_password)
private_key = OpenSSL::PKey::RSA.new(pem_bytes)
password = private_key.private_decrypt decoded
end
password = machine_spec.reference['winrm_password'] ||
machine_options[:winrm_password] ||
begin
if machine_spec.reference['winrm_encrypted_password']
decoded = Base64.decode64(machine_spec.reference['winrm_encrypted_password'])
else
encrypted_admin_password = instance.password_data.password_data
if encrypted_admin_password.nil? || encrypted_admin_password.empty?
raise "You did not specify winrm_password in the machine options and no encrytpted password could be fetched from the instance"
end
machine_spec.reference['winrm_encrypted_password']||=encrypted_admin_password
# ^^ saves encrypted password to the machine_spec
decoded = Base64.decode64(encrypted_admin_password)
end
# decrypt so we can utilize
private_key = OpenSSL::PKey::RSA.new(get_private_key(instance.key_name))
private_key.private_decrypt decoded
end

disable_sspi = machine_spec.reference['winrm_disable_sspi'] ||
machine_options[:winrm_disable_sspi] ||
false # default to Negotiate
basic_auth_only = machine_spec.reference['winrm_basic_auth_only'] ||
machine_options[:winrm_basic_auth_only] ||
false # disallow Basic auth by default
no_ssl_peer_verification = machine_spec.reference['winrm_no_ssl_peer_verification'] ||
machine_options[:winrm_no_ssl_peer_verification] ||
false #disallow MITM potential by default

winrm_options = {
:user => machine_spec.reference['winrm_username'] || 'Administrator',
:pass => password,
:disable_sspi => true,
:basic_auth_only => true
user: username,
pass: password,
disable_sspi: disable_sspi,
basic_auth_only: basic_auth_only,
no_ssl_peer_verification: no_ssl_peer_verification,
}

if no_ssl_peer_verification or type != :ssl
# => we won't verify certs at all
Chef::Log.info "No SSL or no peer verification"
elsif machine_spec.reference['winrm_ssl_thumbprint']
# we have stored the cert
Chef::Log.info "Using stored fingerprint"
else
# we need to retrieve the cert and verify it by connecting just to
# retrieve the ssl certificate and compare it to what we see in the
# console logs
instance.console_output.data.output
# again this seem to need to be run twice, to ensure
encoded_output = instance.console_output.data.output
console_lines = Base64.decode64(encoded_output).lines
fp_context = OpenSSL::SSL::SSLContext.new
tcp_connection = TCPSocket.new(instance.private_ip_address, port)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @hh - why are you using the private_ip_address here? When I try to connect to an https connection using this PR I get the following error

https://gist.github.com/tyler-ball/73996b85e155ab76cfa2280ecac6c71d

ssl_connection = OpenSSL::SSL::SSLSocket.new(tcp_connection, fp_context)

begin
ssl_connection.connect
rescue OpenSSL::SSL::SSLError => e
raise e unless e.message =~ /bad signature/
ensure
tcp_connection.close
end

winrm_cert = ssl_connection.peer_cert_chain.first

rdp_thumbprint = console_lines.grep(
/RDPCERTIFICATE-THUMBPRINT/)[-1].split(': ').last.chomp
rdp_subject = console_lines.grep(
/RDPCERTIFICATE-SUBJECTNAME/)[-1].split(': ').last.chomp
winrm_subject = winrm_cert.subject.to_s.split('=').last.upcase
winrm_thumbprint=OpenSSL::Digest::SHA1.new(winrm_cert.to_der).to_s.upcase

if rdp_subject != winrm_subject or rdp_thumbprint != winrm_thumbprint
Chef::Log.fatal "Winrm ssl port certificate differs from rdp console logs"
end
# now cache these for later use in the reference
if machine_spec.reference['winrm_ssl_subject'] != winrm_subject
machine_spec.reference['winrm_ssl_subject'] = winrm_subject
end
if machine_spec.reference['winrm_ssl_thumbprint'] != winrm_thumbprint
machine_spec.reference['winrm_ssl_thumbprint'] = winrm_thumbprint
end
if machine_spec.reference['winrm_ssl_cert'] != winrm_cert.to_pem
machine_spec.reference['winrm_ssl_cert'] = winrm_cert.to_pem
end
end

if machine_spec.reference['winrm_ssl_thumbprint']
winrm_options[:ssl_peer_fingerprint] = machine_spec.reference['winrm_ssl_thumbprint']
end

Chef::Provisioning::Transport::WinRM.new("#{endpoint}", type, winrm_options, {})
end

Expand Down