For a while now I’ve been interested in using client certificates for authentication of e-mail clients using IMAP and SMTP, while still permitting password authentication. This week I finally decided to actually figure out how I could get this to work. Because it took me quite some effort to discover how to do this but couldn’t find anyone else documenting the same I thought it might help other people to describe my setup.

I’m using Postfix 2.11.3 for SMTP and a patched Dovecot 2.2.27 for IMAP. I had to patch Dovecot because the required functionality only gets introduced with 2.2.28 in cdf00f5:

auth: Support filtering by SASL mechanism: passdb { mechanisms }

So if you want to get the same functionality either use Dovecot 2.2.28 or higher or backport the mentioned commit like I did.

Certificate Authority

To manage my CA I’m using the easy-rsa utility. For brevity I’m not documenting how to use that, except to mention how I’m exporting the combined CA+CRL file which is what I’m using in the rest of this article. Producing that file is done as follows:

$ cd easy-rsa/easyrsa3
$ ./easyrsa gen-crl
$ cat pki/ca.crt pki/crl.pem > pki/ca+crl.pem

SMTP server Postfix

First lets add certificate authentication to Postfix as it’s the easiest. We’re assuming that any valid certificate, signed by our CA, is authorized to use this server for relaying mail. If you actually want more complicated authentication than that I don’t think Postfix can currently help you.

In /etc/postfix/ configure TLS to be required and ask for a client certificate on the submission port. You don’t want to do this globally, in, because some servers, wishing to deliver mail to you, might not deal well with being asked for a client certificate.

submission inet n       -       -       -       -       smtpd
  # mandatory encryption. 'may', opportunistic encryption, works too, but you
  # probably don't want that
  -o smtpd_tls_security_level=encrypt

  # enable regular SASL authentication (with passwords, assuming you currently
  # have this and want to retain it)
  -o smtpd_sasl_auth_enable=yes

  # ask for a client certificate: gives the client the opportunity to provide
  # one, not the obligation to do so
  -o smtpd_tls_ask_ccert=yes

Then in /etc/postfix/ configure certificate verification and give permission for relay access:

smtpd_tls_CAfile = /etc/easy-rsa/easyrsa3/pki/ca+crl.pem
# necessary to prevent any random, public, CA from giving relay access to your server
tls_append_default_CA = no

# add to either smtpd_recipient_restrictions or smtpd_relay_restrictions
# depending on which of those you use to control authenticated relay access
smtpd_relay_restrictions =

IMAP server Dovecot

For setting up Dovecot I’ll assume you already have it running with TLS enabled. I’m only describing the additional configuration options that are needed. In /etc/dovecot/conf.d/10-ssl.conf add:

# Our CA that we use to sign the client certificates
ssl_ca = </etc/easy-rsa/easyrsa3/pki/ca+crl.pem

# only permit non-revoked certificates
ssl_require_crl = yes

# ask for client certificates (but don't require them)
ssl_verify_client_cert = yes

# I'm using a full mail address as username and storing it in the emailAddres
# field of the certificate
ssl_cert_username_field = emailAddres

In /etc/dovecot/conf.d/10-auth.conf add EXTERNAL as an authentication mechanism:

# Use the username taken from the client certificate
auth_ssl_username_from_cert = yes

# Add 'external' to auth_mechanisms. That will use the username extracted from
# the certificate combined with the empty string as password to authenticate.
# This is my list of enabled mechanisms
auth_mechanisms = plain login external

Now comes the “trick” to making the combination of password and certificate authentication work. Because Dovecot’s EXTERNAL authentication mechanism attempts to authenticate with the empty string as password, we need to have a password database that permits that. At the same time though, we want to prevent regular logins, with password, to succeed when specifying the empty string as password. This requires the filtering of password databases by SASL mechanism added to Dovecot 2.2.28 that I mentioned earlier. I’m adding a passwd-file password database containing only the usernames, without passwords, as /etc/dovecot/users-external:

Subsequently I’m adding an EXTERNAL-specific password database to /etc/dovecot/conf.d/auth-passwdfile.conf.ext. Make sure to also include it, near the bottom of /etc/dovecot/10-auth.conf.

passdb {
  driver = passwd-file
  # the PLAIN scheme prevents us from having to hash the empty string
  args = scheme=PLAIN username_format=%u /etc/dovecot/users-external

  # this option requires Dovecot 2.2.28 (or the patch), without it this setup
  # is insecure because it permits logins with the empty string as password
  mechanisms = external

  # explicitly permit empty passwords
  override_fields = nopassword

You can test this on a terminal with the openssl s_client:

$ openssl s_client -CAfile /etc/ssl/certs/ca-certificates.crt -verify 4 \
>   -cert ${PATH_TO_CLIENT_CERT} \
>   -tls1 -starttls imap -connect
<lots of text scrolling by>
. OK Pre-login capabilities listed, post-login capabilities have more.
* CAPABILITY IMAP4rev1 ......
A001 OK Logged in

Thunderbird as Client

When using Thunderbird as a client you can specify the “TLS certificate” “authentication method” in the “security settings” portion of the “server settings” for your account settings. Unfortunately you cannot choose this during the account setup wizard. So during the wizard you’ll still need to use password authentication. For SMTP you can just use “no authentication” as the “authentication method” (it’s a misleading name).


  • Posted on , in reply to.

    I share the goal of client certificate authentication to IMAP, while still allowing passwords by users without certificates.  However, I’m missing something in your description of how to accomplish this.

    I read in 10-auth.conf that auth_ssl_username_from_cert will take the DN_CN from the client certificate, and pass that as the username.  I don’t see ssl_cert_username_field documented there, but it is in the code.

    Presumably, plain and login won’t recognize the username (, so it will get to external - where you pass the username as %u.  That all makes sense - if is in the file, the lookup will succeed.

    On the other hand, telling my e-mail client to login with “foo” and “” will fail, as plain/login will reject it.  And “foo” isn’t in the external file.  This is all good.

    But: what prevents a user from telling their e-mail client to login with “” and “” (the null password)?  A malicious actor certainly knows the e-mail address that he is trying to access…

    I don’t see the access to EXTERNAL conditioned by the username coming from a validated certificate.  It seems to me that this is required. indicates that %k /%{cert} could be passed to an external driver to allow it to respond “unknown user” to lookups where the username doesn’t come from a validated certificate.  But I don’t see any reference to that.

    (I’m looking at the code of 2.2.31, and I don’t see these variables referenced, except by a test.)

    Presumably you avoided this scenario.  What am I missing?

    (I thought it would be necessary to write a custom driver to handle this case, but if you have solved all the problems, I don’t want to re-invent the wheel!)

    • Posted on , in reply to.

      Hi tlhackque,

      I eventually switched away from this setup because of other, unrelated, deployment problems.

      I was using an older version of Dovecot than the 2.2.31 you’re referencing, so perhaps the %k variable you mention didn’t exist yet at that time. I definitely don’t recall seeing it before.

      As for how this should work: The mechanism filter, introduced with cdf00f5, expressed as mechanisms = external in the config file, should prevent the separate passdb from being used for anything else.

      I.e. I had two passdb entries. /etc/dovecot/users-external was one of those and I produced it by taking my regular /ect/dovecot/users and emptying all fields except the username one. The external database only got used for the EXTERNAL mechanism, and the regular database was still permitted for all mechanisms.

      This ensured that the empty password was only permitted (required actually) for the EXTERNAL mechanism. The username itself got taken from the emailAddress field of the certificate due to the ssl_cert_username_field setting. This ensures that the username being used is under control of the party controlling the CA.

      I don’t think I mentioned this very clearly in my blog post, but the EXTERNAL-specific password database config section was in addition to what was already present in the default config.

      This is the complete content of /etc/dovecot/conf.d/auth-passwdfile.conf.ext (taken from a backup):

      # Authentication for passwd-file users. Included from 10-auth.conf.
      # passwd-like file with specified location.
      # <doc/wiki/AuthDatabase.PasswdFile.txt>
      passdb {
        driver = passwd-file
        args = scheme=CRYPT username_format=%u /etc/dovecot/users
      # Password database intended only for use with SASL EXTERNAL
      # Specifically the user is expected to be authenticated via a TLS client certificate instead
      passdb {
        driver = passwd-file
        args = scheme=PLAIN username_format=%u /etc/dovecot/users-external
        # specify this with Dovecot 2.30+ (or maybe 2.28+?): requires 10f6f2224c897fc543973efd2f46b86a3ab1148d
        mechanisms = external
        override_fields = nopassword
      userdb {
        driver = passwd-file
        args = username_format=%u /etc/dovecot/users
        # Default fields that can be overridden by passwd-file
        default_fields = home=/var/mail/vhosts/%d/%n
Add a comment via email

Comment Atom Feed