How to Connect to Mail Server with Self-Signed Certificate on Windows

This post is to both help resolve my issue and to document the solutions I’ve found for anyone searching in the future.

Until recently, all of my Duplicati clients (2 Ubuntu or derivative, 1 Windows 10) would successfully send mail reports to me using a private, local mail server. A new service I deployed to my environment required this mail server to be configured with SSL, and since I already had a functioning certificate authority (CA), I generated and installed a self-signed certificate on the mail server. This allowed me to continue with the new service, but it unintentionally (though logically) caused MailKit.Security.SslHandshakeException errors on all Duplicati clients.

Resolving the problem was simple on the Ubuntu clients. On each client, I added the CA certificate to the machine trust store (specifically /usr/local/share/ca-certificates/ to keep them separate from distro-provided certs in /etc/ssl/certs/), then ran the following to update the combined list of trusted certs for the client OS and Mono, respectively.

sudo update-ca-certificates
sudo cert-sync /etc/ssl/certs/ca-certificates.crt

I have not been able to identify a solution for the Windows 10 client. I have verified that the client trusts the same CA certificate, as I’m able to access a private HTTPS web service signed with the same CA using Microsoft Edge. The certificate was previously added to a Windows trust store on the client. I am also confident the problem is due to the certificate, as adding ?starttls=never to the --send-mail-url option results in successful receipt of a mail report.

This StackOverflow post seems to be the best explanation of the problem I’ve found so far. MailKit, which I understand Duplicati uses for mail processing, uses a hard-coded list of trusted CAs, which doesn’t include my local CA. The only way I’ve found to update this list on Windows is to modify MailKit code somewhere, but I am both unsure where the changes need to be made and also reluctant to make such drastic changes that would most certainly be overwritten during updates.

I am no expert with SSL. I understand there are multiple certificate stores on Windows, and I may have simply added my CA certificate to the wrong one. I may also be missing an obvious solution.

What is the recommended way to get a Windows 10 Duplicati client to connect to a mail server configured with a self-signed certificate yet still remain as secure as possible?

Hello

I don’t think any Windows software uses a hard coded list of trusted CAs while there is a maintained list in the operating system. I have taken a look at the MailKit source code and I see no such list.
Either you did not add the CA certificate in the right store, or possibly your self-signed certificate misses some attribute to be accepted.

which presumably is before 2.3.1.6 which StackOverflow post says was before change, maybe:

MailKit 2.6.0 (2020-04-03)

Improved default SSL certificate validation logic to be more secure and to recognize the most commonly used mail servers even if their Root CA Certificates are not available on the system.

which I would interpret as an enhancement, but not preventing one from also adding certificates.

Got rough steps or a link? Don’t add it to Edge (and don’t test using Edge) unless sure of its store.
Changes to Microsoft Edge browser TLS server certificate verification shows changes in progress.

I suggest testing with PowerShell, as it uses .NET Framework (like Duplicati), so should be similar:

PS C:\> Invoke-WebRequest -Uri "https://self-signed.badssl.com/"
Invoke-WebRequest : The underlying connection was closed: Could not establish trust relationship for the SSL/TLS secure channel.
At line:1 char:1
+ Invoke-WebRequest -Uri "https://self-signed.badssl.com/"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest], WebException
    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand

This is reminiscent of UNIX tool curl, which is also on many Windows versions these days, e.g. as:

PS C:\> curl "https://self-signed.badssl.com/"
curl : The underlying connection was closed: Could not establish trust relationship for the SSL/TLS secure channel.
At line:1 char:1
+ curl "https://self-signed.badssl.com/"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest], WebException
    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand

openssl s_client may also be available, is more capable, and is also more complicated to operate…
I’m not certain which certificate store these ported tools use, but browsers are famous for going DIY.

from the FAQ (/docs/faq.html) Openssl does not ship certificates, it uses only system provided certificates. Source code shows it is including wincrypt.h, for example. AFAIK Openssl is not included by any Ms tool, but it’s easy to install.

1 Like

I think mine was installed unintentionally, although I don’t mind – except maybe for right now. It’s in:
C:\Strawberry\c\bin\openssl.exe so probably came along with Strawberry Perl that I installed.

Tar and Curl Come to Windows! makes me believe that Windows actually does include curl now…
PowerShell is central to Windows, AFAIK, so those two methods are probably ready out-of-the-box.

Thanks for the quick turnaround. Please allow me a day or two to review the posts and try the suggestions, then I’ll get back to you all.

From my notes, these are the steps I followed to add the CA certificate to the Windows 10 client’s “Trusted Root Certificate Authorities” store. I have confirmed that even though this procedure begins with Google Chrome (which is no longer installed on this client), the CA certificate appears in the same certificate store when accessed from the MMC (Control Panel > Administrative Tools > Manage Computer Certificates), which seems to me to be system-wide.

  1. Download the attached file that ends with .pem.
  2. Open Google Chrome.
  3. Click the dot menu at the top-right, then click Settings.
  4. In the vertical menu on the left, click Privacy & Security.
  5. Find and click Security.
  6. Find and click Manage Certificates.
  7. In the window that appears, click the Trusted Root Certification Authorities tab.
  8. Click Import.
  9. In the window that appears, click Next.
  10. Click Browse, navigate to and select the downloaded certificate, then click Next.
  11. Ensure that Trusted Root Certification Authorities is in the Certificate Store text field, then click Next.
  12. Click Finish.

Good suggestion to use PowerShell. I used Edge specifically because I knew my usual browser, Firefox, has its own certificate store, but I had no idea Edge is planning on doing something similar. The below output appears to confirm that the certificate is trusted.

PS C:\Users\OBFUSCATED> Invoke-WebRequest -Uri "https://OBFUSCATED"


StatusCode        : 200
StatusDescription : OK

The mail server is running on port 25 of this same FQDN. It doesn’t appear that I can test it with Invoke-WebRequest, though, or I haven’t figured out how to yet.

PS C:\Users\OBFUSCATED> Invoke-WebRequest -Uri "OBFUSCATED:25"
Invoke-WebRequest : The URI prefix is not recognized.
At line:1 char:1
+ Invoke-WebRequest -Uri "OBFUSCATED:25"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotImplemented: (:) [Invoke-WebRequest], NotSupportedException
    + FullyQualifiedErrorId : WebCmdletIEDomNotSupportedException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand

PS C:\Users\OBFUSCATED> Invoke-WebRequest -Uri "smtp://OBFUSCATED:25"
Invoke-WebRequest : The URI prefix is not recognized.
At line:1 char:1
+ Invoke-WebRequest -Uri "smtp://OBFUSCATED:25"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotImplemented: (:) [Invoke-WebRequest], NotSupportedException
    + FullyQualifiedErrorId : WebCmdletIEDomNotSupportedException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand

PS C:\Users\OBFUSCATED> Invoke-WebRequest -Uri "smtp://OBFUSCATED"
Invoke-WebRequest : The URI prefix is not recognized.
At line:1 char:1
+ Invoke-WebRequest -Uri "smtp://OBFUSCATED"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotImplemented: (:) [Invoke-WebRequest], NotSupportedException
    + FullyQualifiedErrorId : WebCmdletIEDomNotSupportedException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand

I only found it just before I mentioned it, but Google did lead me to its docs:

Invoke-WebRequest where -Uri section says it can accept http and https.
I would suggest trying https with :25 added at end to tell it the port number.
I just tested http://localhost:8200 and presumably contacted Duplicati.

Let me try an https test:

PS C:\> curl "https://self-signed.badssl.com:25"
curl : Unable to connect to the remote server
At line:1 char:1
+ curl "https://self-signed.badssl.com:25"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest], WebException
    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand

This shows it took 25 and (not surprisingly) found nothing there. BTW STARTTLS is typically on 587.

SMTPS: Securing SMTP and the Differences Between SSL, TLS, and the Ports They Use

Well, a mail server don’t use the HTTP protocol, it may use TLS, but TLS is not HTTPS.
FWIW it’s quite possible to use a Let’s Encript or ZeroSSL certificate for a mail server, either with a wildcard (DNS based) certificate, or by running a web server on the mail server for the only purpose of obtaining the certificate. Of course if a web server is already running, just use the already existing web certificate for the mail server. A certificate don’t care about the port number.

If a mail server is on the Internet, there is no good reason to use a self-signed certificate. And if it’s not on the Internet, unencrypted port 25 would do for me. After all, I don’t see what’s so confidential in a Duplicati backup report.

I wondered about that, then thought that near-clear-text credentials might be sensitive even on LAN, however I don’t even know if server requires authentication, and didn’t feel like questioning too hard.
Authentication types vary from server to server. Some are more protected, others just a bit obscured.

The goal here is to do a certificate test, nothing more. Although STARTTLS switches from plain TCP connection right in the middle, I would think that from a certificate view, it’s all the same. Seem right?

If one thinks that STARTTLS is a weird special case, one can often go TLS from the start at port 465.

Whether or not port 25 supports STARTTLS at all appears to depend on the server (or so some say).
When wondering why something is failing, it would seem best to try it in the commonly used manner.

If one suspects MailKit messaging is generic and might not be a true TLS level issue, one could test
The SEND-MAIL command to see full transcript. Don’t accidentally post the server authentication…

EDIT:

Actually one would probably want the test to go to the implicit TLS port 465 to be TLS right off the bat
unless port 25 is somehow set up that way too. IETF appears to favor implicit TLS according to below:

Cleartext Considered Obsolete: Use of Transport Layer Security (TLS) for Email Submission and Access

openssl allows for that. There is a starttls parameter - but it depends on the protocol, there is a starttls smtp, starttls imap, etc… but there is no starttls for http, AFAIK. You can’t do TLS on port 80, at least not in a standard way. So neither curl nor invoke-webrequest can be used. Powershell maybe, I don’t know if it supports starttls (of course it supports immediate Tls)

I edited my reply while you were writing (correctly). Cert test can’t use port 587, but 465 might work.

The man page doesn’t distinguish between implicit versus explicit TLS, but this works on Windows:

C:>curl --ssl-reqd smtp://<myisp>:587
214 No help here

Wireshark shows it starting as a Duplicati test would show, except on the network it goes encrypted.
This isn’t visible in the Duplicati log, as it’s pre-encryption, but at the network the TLS is unreadable:

250-AUTH LOGIN PLAIN
250-STARTTLS
250-8BITMIME
250-PIPELINING
250-HELP
250-CHUNKING
250-BINARYMIME
250 SIZE 52428800
STARTTLS
220 Ready to start TLS
...........c..F\..A...|...n....em!.

It’s not clear to me which port on user SMTP server is configured which way. Any info may be helpful.
Although Invoke-WebRequest would need an implicit TLS port 465 (if it’s actually there), curl doesn’t.
While fumbling with it, I saw it say schannel: with its error message, suggesting it’s Windows crypto.
That would be what we’d want, and perhaps it’s not necessary to go PowerShell to get to Schannel…

Yes I forgot that curl can do almost anything.

1 Like

Figuring out how to run curl is a little challenging. I started with Google search then began experiment.

Continuing the curl experiment, if I leave off --ssl-reqd the terminal output looks the same, but there
wasn’t a STARTTLS request and everything is in clear text in the packets that Wireshark reassembled.
If I use --ssl it does STARTTLS, but I suspect it would not if the server had not said that it supports it.

There might also be an error recovery case because STARTTLS could presumably fail to achieve TLS, which is a classic security question – if it can’t get to ideal way, would you rather fail or use worse way?

A possible preference on that was given:

We can take a lesson from the above that a STARTTLS is susceptible to a lot of things compared to a
server port that will only use TLS, or fail. If you want secure as possible, use implicit TLS not explicit…

While my ISP doesn’t seem to have anything on port 465, gmail seems to, so I’ll continue and test that.

I can't get anything but error below with smtp: prefix whether adding --ssl, or --ssl-reqd or nothing:

curl: (56) response reading failed

which makes sense. An SMTP server ordinarily send a response out, but here it’s waiting for client TLS.

Using smtps: prefix for curl seems to do what implicit TLS wants:

C:\>curl smtps://smtp.gmail.com:465
214 2.0.0  https://www.google.com/search?btnI&q=RFC+5321 z3-20020a05622a124300b003b62bc6cd1csm10041721qtx.82 - gsmtp

and there was possibly more that curl isn’t showing and Wireshark can’t show because it’s encrypted.
The Client Hello to start TLS goes out pretty much immediately after the TCP connection is made.

Let’s see if Invoke-WebRequest can do anything useful on port 465, such as getting TLS established:

PS C:\> Invoke-WebRequest -Uri "https://smtp.gmail.com:465"
Invoke-WebRequest : The server committed a protocol violation. Section=ResponseStatusLine
At line:1 char:1
+ Invoke-WebRequest -Uri "https://smtp.gmail.com:465"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest], WebException
    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand

however here Wireshark shows TLS established, so the protocol violation is probably at the HTTP level.
Regardless, if user can set up a more-predictable always-used implicit TLS on port 465, use above test.

This has been moving (and changing) fast, so I might give the user a chance to set up to try a few tests.

To clarify: I am referring to one hardware server that hosts multiple services, including the mail server, for a private LAN only (ie “local”). There is no inbound access from the Internet, and the server has unrestricted outbound access.

As stated in the first post, this all came about because a new service that was deployed to the hardware server required any mail server it communicates with to be secured with TLS/SSL. After I added certificates signed by the local CA to the local mail server, the new service worked correctly, but all Duplicati clients were unable to send backup reports via email. All clients, including the Windows 10 one, were successfully sending backup reports over unsecured SMTP (port 25) before then. The top priorities are keeping this new service running and continuing to receive backup reports; cleanup (using the correct ports, etc) will happen when necessary or when time is available.

With that said, I believe I’ve found a satisfactory workaround. The new service actually does not require secured SMTP, it’s just the default. Further, the local mail server is configured to forward mail to a well-known email service on the internet for final delivery, and this connection is secured using TLS. Therefore, I am fine with removing the certificates from the local mail server and allowing it to only apply TLS when mail is sent outside the LAN.

However, aside from this one Windows 10 client, configuring the CA certificates on the other Duplicati clients was simple. This remaining client is the only thing at this time preventing me from leaving the certificates on the local mail server. If you would like me to continue testing for the sake of completeness I can do so. As you’ve probably already noticed, I will only be able to respond outside of business hours. I appreciate the help given.

Below is the result of one of the requested tests. I don’t fully understand where the discussion ended, though. Should I try to configure the mail server to use port 465, then try some curl commands?

PS C:\Users\OBFUSCATED> Invoke-WebRequest -Uri "https://OBFUSCATED:25"
Invoke-WebRequest : The underlying connection was closed: An unexpected error occurred on a send.
At line:1 char:1
+ Invoke-WebRequest -Uri "https://OBFUSCATED:25"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest], WebExc
   eption
    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand

That last error message was probably because port 25 is not configured for TLS-at-connection.
If you’re using starttls successfully from another system, it starts unencrypted then switches.
If a port does implicit TLS upon connect (port 465 is this way), a test with https might connect.

If we assume port 25 is either unencrypted or encrypted depending on STARTLS, TLS forecast:

STARTTLS ports such as 587 or seemingly your port 25 will try a TLS connection in these ways:

curl --ssl-reqd smtp://OBFUSCATED:587 (if you don’t have 587 set up, you can try port 25)

Non-STARTTLS implicit TLS ports such as 465 require no SMTP-specific switch, just a connect:

Invoke-WebRequest -Uri "https://OBFUSCATED:465"

curl smtps://OBFUSCATED:465

Examples given earlier, including how things can get confused after TLS starts due to differences.

send-mail-url

To enable SMTP over SSL, use the format smtps://example.com. To enable SMTP STARTTLS, use the format smtp://example.com:25/?starttls=when-available or smtp://example.com:25/?starttls=always. If no port is specified, port 25 is used for non-ssl, and 465 for SSL connections. To force not to use STARTTLS use smtp://example.com:25/?starttls=never.

So MailKit seems to use the curl terminology of smtps: for always-TLS, but a starttls modifier on smtp: connections that use explicit STARTTLS to (maybe) switch over to TLS in the middle of things.

I apologize, but I will not be able to continue testing this for the foreseeable future. Unless someone suggests otherwise, I will not select a post as a solution since we did not identify how to correctly use a self-signed certificate on Windows.

If anyone comes across this thread in the future and would like to continue testing to identify a solution, please feel free to.