Both virtual and real users in the same domain with Exim and Courier

My personal server only served a few real users to this day, which means that each of us had an account on the server, that owned files and could run commands, schedule tasks, and so on.

I just extended my hosting perimeter, but I only allow email usage for my new guests so far. With this in mind, I did not want to create new Linux accounts, which would have needlessly weakened my server by exposing it to attacks towards these new accounts. Thus I created my first virtual users, for electronic mail.

I found several configuration examples on the Internet, but those mostly addressed situations where all users were virtual, often in several domains, or where a given domain was targeted at real users while others domains were targeted at virtual users. My wish was rather to allow new, virtual, users into my existing domains, the users of which were so far of the real kind only. Still, by taking from all those readings, it was not that hard to get there.

Cet article existe aussi en français.

From here on, let’s assume that my domain name is


Authentication was based so far on PAM only (the standard authentication service in Linux); Courier (IMAP) did so through the courierauthdaemon service, and Exim (SMTP) went through the saslauthd service.

For the sake of Courier, I first modified the configuration in /etc/courier/authdaemonrc; the bold word below is the part that I added:

authmodulelist="authpam authuserdb"
authmodulelistorig="authuserdb authpam authpgsql authldap authmysql authcustom authpipe"

Then I had to create a database of all virtual users; here is an example of the steps for a fictive user named newuser:

userdb newuser set uid=8 gid=8 home=/var/mail/virtual/ mail=/var/mail/virtual/
userdbpw -md5 | userdb newuser set systempw
mkdir -p /var/mail/virtual/{cur,tmp,new}
chown -R mail:mail /var/mail/virtual

The users thus created do not actually exist from the operating system’s point of view, so I chose to put them under the control of the mail user (uid=8, gid=8), which had been automatically created by Debian at some point.

After the courierauthdaemon service had restarted, I was able to test these first steps using the authtest command (the bold stands for what I type):

authtest 'newuser' 'good password'
Authentication succeeded.

Authenticated: newuser (uid 8, gid 8)
Home Directory: /var/mail/virtual/
Maildir: /var/mail/virtual/
Quota: (none)
Encrypted Password: $1$7zFoadsH$/BkKSkhoMrQ2yJ1GCi993.
Cleartext Password: good password
Options: (none)

authtest 'newuser' 'bad password'
Authentication FAILED: Operation not permitted

Now for Exim, the goal was to benefit from the same users database. This was thankfully possible, with two possible configurations: either Exim accessed courierauthdaemon’s database file directly (/etc/courier/userdb.dat), or I made Exim talk to courierauthdaemon itself through its socket file. I chose the latter, first of all because this ensures that I get the same authentication results from IMAP and SMTP even if I change courierauthdaemon’s configuration again, but also because a direct access to courierauthdaemon’s private file seemed dirtier to me, and more fragile too. In /etc/exim4/conf.d/auth/30_exim4-config_examples , I disabled the former authentication method, and I enabled this instead:

driver = plaintext
public_name = PLAIN
server_prompts = :
server_condition = \
${extract {ADDRESS} \
{${readsocket{/var/run/courier/authdaemon/socket} \
{AUTH ${strlen:exim\nlogin\n$auth2\n$auth3\n}\nexim\nlogin\n$auth2\n$auth3\n} }} \
{yes} \
server_set_id = $auth2
server_advertise_condition = ${if eq{$tls_in_cipher}{}{}{*}}

Warning: the line in bold above, regarding server_prompts, is important for most clients to authenticate successfully, even though this line is absent from the default Debian file.

For this new authentication method to work, Exim had to be allowed to access courierauthdaemon’s socket file. I checked the permissions, and it happened that a single command was enough to set these right:

chmod o+x /run/courier/authdaemon

However, in order to make this change persistent accross reboots, I also had to tweak /etc/init.d/courier-authdaemon with this change:

old line: mkdir -m 0750 $rundir
new line: mkdir -m 0751 $rundir

Then the Exim service could be restarted.

In order to test this configuration, I had to call the SMTP server from an unknown machine from the Internet, not a trusted client (as the latter would not be asked to authenticate). Thus it is from an SSH account that I have on the Internet, that I ran the following tests, using the base64 and gnutls-cli tools (the bold stands for what I type):

printf 'newuser\0newuser\0good password\0' | base64

gnutls-cli -s -p 25 --insecure
220 example_host ESMTP Exim 4.84 Fri, 05 Jun 2015 23:44:37 +0200
250-example_host Hello you at []
250-SIZE 52428800
250 HELP
220 TLS go ahead
*** Starting TLS handshake
250-example_host Hello you at []
250-SIZE 52428800
250 HELP
AUTH PLAIN bmV3dXNlcgBuZXd1c2VyAGdvb2QgcGFzc3dvcmQA
235 Authentication succeeded
221 example_host closing connection
- Peer has closed the GnuTLS connection

This test should be run again with a wrong user name, and then again with a wrong password; in both cases, the result must be an authentication failure. These tests should also be run for existing real users, to ensure that no regression happens.

Email management: Exim

Email is processed by Exim in three stages:

  1. ACLs (Access Control Lists) are run, corresponding to the steps of the SMTP protocol (EHLO, AUTH, MAIL FROM, RCPT TO);
  2. if the message has been accepted, it is routed, which means that it is directed at the recipient, reading its initial value in the message’s headers, and then applying transformations as needed (/etc/aliases, .forward, procmail…);
  3. finally, the message is transported to its final destination, which can be an mbox or Maildir mailbox, or a command that will read the message from its standard input, or a named pipe, or even a distant server…

Handling the virtual users in the first stage required no adaptation, other than the changes already applied to authentication; indeed, my current and new users are all part of the same domains and must obey the same rules.

For the second stage, I had to create a new router because virtual users are not recognized the same way as real ones are, and their data is not stored the same way either. Here were the existing routers before I changed anything (the bold stands for what I type):

ls -1 /etc/exim4/conf.d/router

These routers are executed in sequence, until one of the routers explicitly accepts the recipient. Since my new users do not have a home directory, I decided that they will not use tools such as procmail; however, I allow them to have aliases (although I did not test this possibility). As a consequence, I inserted my new file between 400… and 500…, under the name /etc/exim4/conf.d/router/450_exim4-config_vusers:

debug_print = "R: vusers for $local_part@$domain"
driver = accept
domains = dsearch;/var/mail/virtual
local_parts = dsearch;/var/mail/virtual/$domain
transport = maildir_vusers

This tells Exim to accept a recipient if their domain matches an entry in the /var/mail/virtual directory, and their username (after aliases have been taken into account) matches an entry in that sub-directory; then this recipient must be transported by maildir_vusers (third stage of the process), that must be created yet.

Here were the existing transports before I changed anything (the bold stands for what I type):

ls -1 /etc/exim4/conf.d/transport

My new transport is very similar to maildir_home; thus I drew upon the latter to create my new file /etc/exim4/conf.d/transport/30_exim4-config_maildir_vusers:

debug_print = "T: maildir_vusers for $local_part@$domain"
driver = appendfile
directory = /var/mail/virtual/$domain/$local_part
user = mail
group = mail
directory_mode = 0700
mode = 0600
mode_fail_narrower = false
# This transport always chdirs to $home before trying to deliver. If
# $home is not accessible, this chdir fails and prevents delivery.
# If you are in a setup where home directories might not be
# accessible, uncomment the current_directory line below.
current_directory = /var/mail/virtual

After Exim had been restarted, I was able to test this configuration using the exim4 command (the bold stands for what I type):

exim4 -d+route -bt 2>&1 | less
--------> vusers router <--------
checking domains
search_open: dsearch "/var/mail/virtual"
search_find: file="/var/mail/virtual"
key="" partial=-1 affix=NULL starflags=0
lookup yielded: in "dsearch;/var/mail/virtual"? yes (matched "dsearch;/var/mail/virtual")
checking local_parts
search_open: dsearch "/var/mail/virtual/"
search_find: file="/var/mail/virtual/"
key="me" partial=-1 affix=NULL starflags=0
lookup failed
me in "dsearch;/var/mail/virtual/"? no (end of list)
vusers router skipped: local_parts mismatch
--------> local_user router <--------
checking domains
cached yes match for +local_domains
cached lookup data = NULL in "+local_domains"? yes (matched "+local_domains" - cached)
checking local_parts
me in "! root"? yes (end of list)
routed by local_user router
envelope to:
transport: maildir_home
search_tidyup called
>>>>>>>>>>>>>>>> Exim pid=13811 terminating with rc=0 >>>>>>>>>>>>>>>>
router = local_user, transport = maildir_home

exim4 -d+route -bt 2>&1 | less
--------> vusers router <--------
checking domains
search_open: dsearch "/var/mail/virtual"
search_find: file="/var/mail/virtual"
key="" partial=-1 affix=NULL starflags=0
lookup yielded: in "dsearch;/var/mail/virtual"? yes (matched "dsearch;/var/mail/virtual")
checking local_parts
search_open: dsearch "/var/mail/virtual/"
search_find: file="/var/mail/virtual/"
key="newuser" partial=-1 affix=NULL starflags=0
lookup yielded: newuser
newuser in "dsearch;/var/mail/virtual/"? yes (matched "dsearch;/var/mail/virtual/")
routed by vusers router
envelope to:
transport: maildir_vusers
search_tidyup called
>>>>>>>>>>>>>>>> Exim pid=13861 terminating with rc=0 >>>>>>>>>>>>>>>>
router = vusers, transport = maildir_vusers

That is all for Exim, and all that had to be done for Courier had already been taken care of, since it draws the information it needs from courierauthdaemon (in particular the location of the Maildir directory).

Warning: as virtual users of a domain are processed before real users of this domain are, one must take care not to create a directory named /var/mail/virtual/ (corresponding to a real user). If such a directory were created, then the matching user would be seen as virtual even though they do have a system account.

Here are the articles that helped me understand and make it all work:


  • 2015-06-14 — I removed saslauthd authentication after all, because courierauthdaemon is independent from Courier’s IMAP daemons anyway. Besides, the verification of the value returned by the socket is now more secure, thanks to a tip by henk on IRC ( Last but not least, ensuring access to the socket proved to be more complicated than I thought at first.
  • 2015-10-07 — The authentication now has better compatibility thanks to a fix about prompts.

Ajouter un commentaire

Le code HTML est affiché comme du texte et les adresses web sont automatiquement transformées.

La discussion continue ailleurs

URL de rétrolien :

Fil des commentaires de ce billet