SMTP STARTTLS certificate verification fails because default CA certificates are not loaded

Environment:

  • Ubuntu 22.04.5 LTS
  • Pritunl 1.34.4681.89-0ubuntu1~jammy
  • Pritunl embedded Python 3.12
  • OpenSSL 3.0.2
  • SMTP server: smtp.gmail.com:587
  • STARTTLS enabled

After updating the server, Pritunl profile emails began failing during
STARTTLS with:

ssl.SSLCertVerificationError:
[SSL: CERTIFICATE_VERIFY_FAILED]
certificate verify failed: unable to get local issuer certificate

The failure occurs in pritunl/utils/mail.py at:

smtp_conn.starttls(context=context)

The SMTP SSL context is currently created with:

context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)

but the default CA certificates are not loaded afterward.

I reproduced the issue using Pritunl’s embedded Python interpreter.

This fails with the same certificate error:

import ssl
import smtplib

ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.minimum_version = ssl.TLSVersion.TLSv1_2

smtp = smtplib.SMTP(“smtp.gmail.com”, 587)
smtp.ehlo()
smtp.starttls(context=ctx)

This succeeds when one line is added:

ctx.load_default_certs()

Complete successful test:

import ssl
import smtplib

ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.load_default_certs()
ctx.minimum_version = ssl.TLSVersion.TLSv1_2

smtp = smtplib.SMTP(“smtp.gmail.com”, 587)
smtp.ehlo()
smtp.starttls(context=ctx)
smtp.ehlo()
smtp.quit()

Additional verification:

  • openssl s_client validates smtp.gmail.com successfully
  • Verify return code is 0
  • /usr/lib/ssl/certs correctly points to /etc/ssl/certs
  • CA certificates and OpenSSL packages are current
  • No SSL_CERT_FILE or SSL_CERT_DIR overrides are set
  • No stale or deleted libraries are mapped into the running process
  • Pritunl is the current package version in the Jammy repository

The following CA store behavior was also observed:

Before load_default_certs():

{‘x509’: 0, ‘crl’: 0, ‘x509_ca’: 0}

After load_default_certs(), before the handshake:

{‘x509’: 0, ‘crl’: 0, ‘x509_ca’: 0}

After a successful SMTP TLS handshake:

{‘x509’: 1, ‘crl’: 0, ‘x509_ca’: 1}

This appears to be normal lazy loading from the hashed CA directory, but it
confirms that calling load_default_certs() configures the trust source needed
for verification.

Temporary workaround:

pritunl set app.email_skip_verify true

That allows email delivery, but disables certificate and hostname
verification, so it is not suitable as a permanent fix.

Would it be appropriate to change mail.py to either:

context = ssl.create_default_context()

or:

context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
context.load_default_certs()

before applying the TLS minimum-version setting?

This will be fixed in the next release.

Thank you so much Zach!