I want to send emails from my Rails web application, and I do not want to disable TLS certificate verification. However for some reason, it always fails with "SSLv3 read server certificate B: certificate verify failed", even though the server certificate is valid.
I doubled checked with openssl s_client
(using /etc/ssl/certs/ca-certificates.crt
), and running the following in the rails console also works, delivering successfully.
smtp = Net::SMTP.new(host, port)
smtp.enable_tls
smtp.start("localhost", username, password, :login) do |smtp|
smtp.send_message msgstr, from, to
end
The server has Rails 4.2.6 and Ruby 2.3.0
config.action_mailer.smtp_setting = {
address:
port: 465,
user_name:
password:
authentication: :login,
openssl_verify_mode: OpenSSL::SSL::VERIFY_PEER,
enable_starttls_auto: false,
ssl: true
}
From the described behavior I am quite sure that peer verification has not been done in the console and that you need to explicitly set the certificate store for verifying peer certificates in your Rails configuration.
Why it "works" in the console and how to actually verify peers there:
The observation that it works from the console but does not from Rails code is caused by the fact that smtp.enable_tls
in your console code does not force peer verification whereas your Rails configuration apparently does. Indeed, when you write the command to the console, you get the SSLContext
printed out:
smtp.enable_tls
# => #<OpenSSL::SSL::SSLContext:0x000000064043d0 @cert=nil, @key=nil,
@client_ca=nil, @ca_file=nil, @ca_path=nil, @timeout=nil,
@verify_mode=nil, @verify_depth=nil, @renegotiation_cb=nil,
@verify_callback=nil, @cert_store=nil, @extra_chain_cert=nil,
@client_cert_cb=nil, @session_id_context=nil, @tmp_dh_callback=nil,
@session_get_cb=nil, @session_new_cb=nil, @session_remove_cb=nil,
@tmp_ecdh_callback=nil, @servername_cb=nil, @npn_protocols=nil,
@alpn_protocols=nil, @alpn_select_cb=nil, @npn_select_cb=nil>
Note that @verify_mode
is nil
so there is no peer verification enabled by default on the SSLContext
.
To force peer verification in console, so that you can play with the SSL settings manually, you need to use a custom SSLContext
and pass it to enable_tls
:
ssl_context = Net::SMTP.default_ssl_context
ssl_context.set_params
smtp.enable_tls(ssl_context)
# => #<OpenSSL::SSL::SSLContext:0x000000063c27c8 @cert=nil, @key=nil,
@client_ca=nil, @ca_file=nil, @ca_path=nil, @timeout=nil,
@verify_mode=1, @verify_depth=nil, @renegotiation_cb=nil,
@verify_callback=nil, @cert_store=#<OpenSSL::X509::Store:0x00000002894408 @verify_callback=nil, @error=nil, @error_string=nil, @chain=nil, @time=nil>, @extra_chain_cert=nil,
@client_cert_cb=nil, @session_id_context=nil, @tmp_dh_callback=nil,
@session_get_cb=nil, @session_new_cb=nil, @session_remove_cb=nil,
@tmp_ecdh_callback=nil, @servername_cb=nil, @npn_protocols=nil,
@alpn_protocols=nil, @alpn_select_cb=nil, @npn_select_cb=nil>
Watch closely the differences: the SSLContext
now has verify_mode
set to 1 and has a certificate store for the verifications defined. This is (among other things) what the set_params
method in SSLContext
does.
How to configure the certificate store in Rails
Now, Rails does not call the set_params
methods when constructing the SSLContext
for SMTP connection. Instead, it sets the individual attributes on it according to the options (see here and here in the source code). You have properly configured Rails that you want to verify peer certificates but you have not configured a certificate store to verify peers against.
This can be done using the ca_file
or ca_path
options, so the following Rails configuration should work for you:
config.action_mailer.smtp_setting = {
...
ssl: true
enable_starttls_auto: false,
openssl_verify_mode: OpenSSL::SSL::VERIFY_PEER,
ca_file: "/etc/ssl/certs/ca-certificates.crt",
...
}
I have no idea why this is not properly documented in the Rails Guides...
This Rails configuration works for me (using Ruby 2.2.2 and Rails 5):
ActionMailer::Base.smtp_setting = {
...
enable_starttls_auto: true,
openssl_verify_mode: OpenSSL::SSL::VERIFY_PEER,
openssl_verify_depth: 3, # if your CA is a sub signer
ca_file: "/etc/ssl/certs/ca-certificates.crt",
...
}