Mildred's Website

Configuring Cyrus IMAP

Configuring Cyrus IMAP

I decided to install Cyrus IMAP on a server and create a terraform recipe for automatic configuration management for this installation. My goal is to have an easily maintainable mail server, and I choose Cyrus IMAP because it is pioneer in many modern features such as JMAP. It’s quite feature complete too.

The problem I got was user management. I don’t plan to use Docker but I wanted to be able to have a web interface for user management, without sharing a db file on the disk (what if the user management and cyrus are on different servers?) or share a database schema (what if I want to change the database schema, should I modify the complete infrastructure because of it. What if I change the database provider?).

Cyrus IMAP relies on Cyrus SASL for user management. It works with two different options:

  • auxprops, built-in with Cyrus SASL or pluggable using a shared library (.so)
  • authdaemon, externalized to a separate process but without much more choices of providers and only supporting logins with plain password (no hashing possible)

And with auxprops, without writing a plugin, the choice is quite limited:

  • LDAP server
  • NTLM, a Windows thing
  • OTP, I suppose one time passwords, not suitable for standard passwords
  • specific database file, not suitable for a networked infrastructure
  • SQL, not suitable if we want to keep the database schema private or if we prefer SQLite (problem in a networked environment)
  • Kerebos, also a very specific authentication method

After quite some thought, I decided to go with LDAP. It’s quite a complex beast but there are alternative to OpenLDAP. In particular, I found glauth that provides a minimal LDAP server written in Go just for authentication. If needed, it will be easily extensible to allow authenticating with extra sources.

Now, the question is how to configure Cyrus to work with glauth….

Step 1: Make Cyrus work and get some logs

To make things worse, I decided to use configuration file in a non standard location, to avoid messing with the distribution packages that I otherwise use (so I can get security updates easily). My configuration files are in /etc/cyrus/cyrus.conf and /etc/cyrus/imapd.conf instead of /etc/cyrus.conf and /etc/imapd.conf.

Cyrus works by having a master process (configured with the cyrus.conf file) that handle listening to sockets and spawning separate daemons for each protocol. The first problem was that the daemons were taking the configuration in /etc/imapd.conf instead of /etc/cyrus/imapd.conf and I was not able to get extra logging.

The solution is to specify the configuration file location in cyrus.conf for each and every daemon using the -C option 9that is otherwise missing from that file):

START {
	# do not delete this entry!
	recover		cmd="/usr/sbin/cyrus ctl_cyrusdb -C /etc/cyrus/imapd.conf -r"
	...
}

SERVICES {
	imap		cmd="imapd -C /etc/cyrus/imapd.conf -U 30" listen="imap" prefork=0 maxchild=1
	...
}

EVENTS {
	# this is required
	checkpoint	cmd="/usr/sbin/cyrus ctl_cyrusdb -C /etc/cyrus/imapd.conf -c" period=30
	...
}

I also modified the maxchild= option from 100 to 1 for imapd for easier process tracing, we’ll see that later on.

I also enable debug logging in /etc/cyrus/imapd.conf:

debug: 1

# SASL_LOG_NONE  0 don't log anything
# SASL_LOG_ERR   1 log unusual errors (default)
# SASL_LOG_FAIL  2 log all authentication failures
# SASL_LOG_WARN  3 log non-fatal warnings
# SASL_LOG_NOTE  4 more verbose than LOG_WARN
# SASL_LOG_DEBUG 5 more verbose than LOG_NOTE
# SASL_LOG_TRACE 6 traces of internal protocols
# SASL_LOG_PASS  7 traces of internal protocols, including passwords
sasl_log_level: 7

Step 2: basic configuration and authentication testing

In imapd.conf I also configured the basic settings for authentication:

allowplaintext: yes
sasl_pwcheck_method: auxprop
sasl_auto_transition: yes
sasl_auxprop_plugin: ldapdb

And started testing with the following command on the server:

cyrus imtest -u jdoe@example.com -w dogood

I enabled the logs on cyrus and glauth and plan to improve on to get something working. For now, I get the following logs from Cyrus:

cyrus/master[12535]: about to exec /usr/lib/cyrus/bin/imapd
cyrus/imap[12535]: executed
cyrus/imap[12535]: ldapdb
cyrus/imap[12535]: _sasl_plugin_load failed on sasl_canonuser_init
cyrus/imap[12535]: auxpropfunc error invalid parameter supplied
cyrus/imap[12535]: _sasl_plugin_load failed on sasl_auxprop_plug_init
cyrus/imap[12535]: ldapdb
cyrus/imap[12535]: _sasl_plugin_load failed on sasl_canonuser_init
cyrus/imap[12535]: accepted connection
cyrus/imap[12535]: SASL DIGEST-MD5 server step 1
cyrus/imap[12535]: SASL DIGEST-MD5 server step 2
cyrus/imap[12535]: SASL could not find auxprop plugin, was searching for 'ldapdb
cyrus/imap[12535]: SASL unable to canonify user and get auxprops"+y
cyrus/imap[12535]: SASL DIGEST-MD5 common mech dispose
cyrus/imap[12535]: badlogin: localhost [::1] DIGEST-MD5 [SASL(-4): no mechanism available: unable to canonify user and get auxprops]

I don’t get what does not work, the error messages are not very specific. Let’s debug this…

Step 3: Debug the whole thing and understand what’s missing

ltrace or strace did not help very much, so I resolved to debug with gdb. I installed the debug packages libsasl2-2-dbgsym, libsasl2-modules-dbgsym, libsasl2-modules-db-dbgsym and libsasl2-modules-ldap-dbgsym and started gdb:

gdb -p $(pidof master)
...
(gdb) set follow-fork-mode child

I found that the message _sasl_plugin_load failed on sasl_auxprop_plug_init came from the function sasl_canonuser_add_plugin, so I set a breakpoint on it:

(gdb) break sasl_canonuser_add_plugin
Function "sasl_canonuser_add_plugin" not defined.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 1 (sasl_canonuser_add_plugin) pending.
(gdb) c

Now, gdb is ready to catch the issue, I started cyrus imtest as above and got to debug it:

Thread 2.1 "imapd" hit Breakpoint 1, sasl_canonuser_add_plugin (plugname=plugname@entry=0x7f6bc4a231cd "INTERNAL", 
    canonuserfunc=0x7f6bc4a139e0 <internal_canonuser_init>) at ../../lib/canonusr.c:308
308	../../lib/canonusr.c: No such file or directory.
(gdb) c

This first breakpoint was for a plugin named "INTERNAL", not my problem so I continued…

Thread 2.1 "imapd" hit Breakpoint 1, sasl_canonuser_add_plugin (plugname=plugname@entry=0x7ffd87fed980 "ldapdb", canonuserfunc=0x7f6bc16b7450 <sasl_canonuser_init>)
    at ../../lib/canonusr.c:308
308	in ../../lib/canonusr.c

There, my plugin… The disas command will get you the disassembled code for that function. I didn’t know I knew how to read assembler code but that’s not so hard to understand:

=> 0x00007f6bc4a13870 <+0>:	push   %r12
   0x00007f6bc4a13872 <+2>:	push   %rbp
   0x00007f6bc4a13873 <+3>:	push   %rbx
   0x00007f6bc4a13874 <+4>:	sub    $0x20,%rsp
   0x00007f6bc4a13878 <+8>:	mov    %fs:0x28,%rax
   0x00007f6bc4a13881 <+17>:	mov    %rax,0x18(%rsp)
   0x00007f6bc4a13886 <+22>:	xor    %eax,%eax
   0x00007f6bc4a13888 <+24>:	test   %rdi,%rdi
   0x00007f6bc4a1388b <+27>:	je     0x7f6bc4a139a8 <sasl_canonuser_add_plugin+312>
   0x00007f6bc4a13891 <+33>:	mov    %rdi,%rbx
   0x00007f6bc4a13894 <+36>:	mov    %rsi,%rbp
   0x00007f6bc4a13897 <+39>:	callq  0x7f6bc4a11130 <strlen@plt>
   0x00007f6bc4a1389c <+44>:	cmp    $0xfff,%rax
   0x00007f6bc4a138a2 <+50>:	ja     0x7f6bc4a139a8 <sasl_canonuser_add_plugin+312>
   0x00007f6bc4a138a8 <+56>:	mov    0x16689(%rip),%rax        # 0x7f6bc4a29f38
   0x00007f6bc4a138af <+63>:	lea    0x10(%rsp),%rcx
   0x00007f6bc4a138b4 <+68>:	lea    0xc(%rsp),%rdx
   0x00007f6bc4a138b9 <+73>:	mov    %rbx,%r8
   0x00007f6bc4a138bc <+76>:	mov    $0x5,%esi
   0x00007f6bc4a138c1 <+81>:	mov    (%rax),%rdi
   0x00007f6bc4a138c4 <+84>:	callq  *%rbp
   0x00007f6bc4a138c6 <+86>:	mov    %eax,%r12d
   0x00007f6bc4a138c9 <+89>:	test   %eax,%eax
   0x00007f6bc4a138cb <+91>:	jne    0x7f6bc4a13988 <sasl_canonuser_add_plugin+280>

The equivalent C code on GitHub to better understand what it means:

int sasl_canonuser_add_plugin(const char *plugname,
			      sasl_canonuser_init_t *canonuserfunc) 
{
    int result, out_version;
    canonuser_plug_list_t *new_item;
    sasl_canonuser_plug_t *plug;

    if(!plugname || strlen(plugname) > (PATH_MAX - 1)) {
	sasl_seterror(NULL, 0,
		      "bad plugname passed to sasl_canonuser_add_plugin\n");
	return SASL_BADPARAM;
    }
    
    result = canonuserfunc(sasl_global_utils, SASL_CANONUSER_PLUG_VERSION,
			   &out_version, &plug, plugname);

    if(result != SASL_OK) {
	_sasl_log(NULL, SASL_LOG_ERR, "%s_canonuser_plug_init() failed in sasl_canonuser_add_plugin(): %z\n",
		  plugname, result);
	return result;
    }
    ...
}

Using nexti to jump each time to the next instruction, I found that the issue was the canonuserfunc returned an error. I had jumped too far so I resumed a new debug session and this time, I jumped into the indirect call callq *%rbp using stepi a few times to get to the plugin function. Dynamic loading the plugin generates a few trampoline functions that I had to stop through.

I got into the ldapdb_canonuser_plug_init function:

(gdb) si
ldapdb_canonuser_plug_init (utils=0x55adbb27ef50, max_version=5, out_version=0x7ffd87fec8dc, plug=0x7ffd87fec8e0, plugname=0x7ffd87fed980 "ldapdb")
    at ../../plugins/ldapdb.c:565
565	../../plugins/ldapdb.c: No such file or directory.
(gdb) disas
Dump of assembler code for function ldapdb_canonuser_plug_init:
=> 0x00007f6bc16b73e0 <+0>:	test   %rdx,%rdx
   0x00007f6bc16b73e3 <+3>:	je     0x7f6bc16b7430 <ldapdb_canonuser_plug_init+80>
   0x00007f6bc16b73e5 <+5>:	test   %rcx,%rcx
   0x00007f6bc16b73e8 <+8>:	je     0x7f6bc16b7430 <ldapdb_canonuser_plug_init+80>
   0x00007f6bc16b73ea <+10>:	cmp    $0x4,%esi
   0x00007f6bc16b73ed <+13>:	jle    0x7f6bc16b7420 <ldapdb_canonuser_plug_init+64>
   0x00007f6bc16b73ef <+15>:	push   %rbp
   0x00007f6bc16b73f0 <+16>:	mov    %rcx,%rbp
   0x00007f6bc16b73f3 <+19>:	push   %rbx
   0x00007f6bc16b73f4 <+20>:	mov    %rdx,%rbx
   0x00007f6bc16b73f7 <+23>:	sub    $0x8,%rsp
   0x00007f6bc16b73fb <+27>:	callq  0x7f6bc16b6d00 <ldapdb_config>
   0x00007f6bc16b7400 <+32>:	movl   $0x5,(%rbx)
   0x00007f6bc16b7406 <+38>:	lea    0x3c13(%rip),%rbx        # 0x7f6bc16bb020 <ldapdb_canonuser_plugin>
   0x00007f6bc16b740d <+45>:	mov    %rbx,0x0(%rbp)
   0x00007f6bc16b7411 <+49>:	add    $0x8,%rsp
   0x00007f6bc16b7415 <+53>:	pop    %rbx
   0x00007f6bc16b7416 <+54>:	pop    %rbp
   0x00007f6bc16b7417 <+55>:	retq   
   0x00007f6bc16b7418 <+56>:	nopl   0x0(%rax,%rax,1)
   0x00007f6bc16b7420 <+64>:	mov    $0xffffffe9,%eax
   0x00007f6bc16b7425 <+69>:	retq   
   0x00007f6bc16b7426 <+70>:	nopw   %cs:0x0(%rax,%rax,1)
   0x00007f6bc16b7430 <+80>:	mov    $0xfffffff9,%eax
   0x00007f6bc16b7435 <+85>:	retq   
End of assembler dump.
(gdb) bt
#0  ldapdb_canonuser_plug_init (utils=0x55adbb27ef50, max_version=5, out_version=0x7ffd87fec8dc, plug=0x7ffd87fec8e0, plugname=0x7ffd87fed980 "ldapdb")
    at ../../plugins/ldapdb.c:565
#1  0x00007f6bc4a138c6 in sasl_canonuser_add_plugin (plugname=plugname@entry=0x7ffd87fed980 "ldapdb", canonuserfunc=0x7f6bc16b7450 <sasl_canonuser_init>)
    at ../../lib/canonusr.c:319
...

The equivalent C code:

int ldapdb_canonuser_plug_init(const sasl_utils_t *utils,
                             int max_version,
                             int *out_version,
                             sasl_canonuser_plug_t **plug,
                             const char *plugname __attribute__((unused))) 
{
    int rc;

    if(!out_version || !plug) return SASL_BADPARAM;

    if(max_version < SASL_CANONUSER_PLUG_VERSION) return SASL_BADVERS;
    
    rc = ldapdb_config(utils);

    *out_version = SASL_CANONUSER_PLUG_VERSION;

    *plug = &ldapdb_canonuser_plugin;

    return rc;
}

Here, the problem is with ldapdb_config(utils) that returns an error. Continuing into that function, I got the following code:

    utils->getopt(utils->getopt_context, ldapdb, "ldapdb_uri", &p->uri, NULL);
    if(!p->uri) return SASL_BADPARAM;

I had forgotten the ldapdb_uri configuration parameter (of course, I’m just testing and I had disabled the line in hope to get a meaningful error).

Step 4: Basic SASL configuration

Let’s configure it in imapd.conf using random values from the documentation:

allowplaintext: yes
sasl_pwcheck_method: auxprop
sasl_auto_transition: yes
sasl_auxprop_plugin: ldapdb

sasl_ldapdb_uri: ldap://glauth/dc=users,dc=local/
sasl_ldapdb_id: cyrus
sasl_ldapdb_pw: cyrus
sasl_ldapdb_mech: PLAIN
sasl_ldapdb_canon_attr: mail

It should work because I have the following request that is working:

$ ldapsearch -h glauth -p 3893 -D cn=cyrus,ou=machines,dc=users,dc=local -b dc=users,dc=local -w cyrus mail=jdoe@example.com

dn: cn=johndoe,ou=superheros,dc=users,dc=local
# ...
# numResponses: 2
# numEntries: 1

Note the use of the sasl_ldabdb_ option prefix instead of just ldap_ as appears in the imapd.conf man page. sasl_* options are all passed verbatim to Cyrus SASL.

I got a little further, but there is still work to do. No more errors from the plugin though:

cyrus/master[13555]: about to exec /usr/lib/cyrus/bin/imapd
cyrus/imap[13555]: executed
cyrus/imap[13555]: accepted connection
cyrus/imap[13555]: SASL DIGEST-MD5 server step 1
cyrus/imap[13555]: SASL DIGEST-MD5 server step 2
cyrus/imap[13555]: SASL unable to canonify user and get auxprops
cyrus/imap[13555]: SASL DIGEST-MD5 common mech dispose
cyrus/imap[13555]: badlogin: localhost [::1] DIGEST-MD5 [SASL(-1): generic failure: unable to canonify user and get auxprops]

Step 5: More debugging

Let’s enable back maxprocs=1 in cyrus.conf and restart the master process.

# gdb -p $(pidof master) -ex 'set follow-fork-mode child'
(gdb) break ldapdb_canonuser_plug_init
(gdb) c
...
(gdb) si
...
(gdb) ni
475	in ../../plugins/ldapdb.c
(gdb) print p->uri
$1 = 0x558a24684ce0 "ldap://glauth/dc=users,dc=local/"

No problem with the configuration this time… Let’s break somewhere else.

(gdb) break ldapdb_canon_client
Breakpoint 2 at 0x7ff322b66f40: ldapdb_canon_client. (2 locations)
(gdb) break ldapdb_canon_server
Breakpoint 3 at 0x7ff322b67010: ldapdb_canon_server. (2 locations)
(gdb) c

No match… let’s try:

(gdb) break ldapdb_connect
...
Thread 2.1 "imapd" hit Breakpoint 1, ldapdb_connect (ctx=ctx@entry=0x7f08784ba0e0 <ldapdb_ctx>, user=user@entry=0x565338fa59e1 "root", ulen=ulen@entry=4, 
    cp=cp@entry=0x7ffdb182ea80, sparams=<optimized out>) at ../../plugins/ldapdb.c:81
81	../../plugins/ldapdb.c: No such file or directory.
(gdb) print ctx->uri
$1 = 0x565338f9cce0 "ldap://glauth/dc=users,dc=local/"
(gdb) print cp->ld
$2 = (LDAP *) 0x0

Got it, the code:

static int ldapdb_connect(ldapctx *ctx, sasl_server_params_t *sparams,
	const char *user, unsigned ulen, connparm *cp)
{
    int i, rc;
    char *authzid;

    if((i=ldap_initialize(&cp->ld, ctx->uri))) {
	cp->ld = NULL;
	return i;
    }

And we have ldap_initialize that returns with -9, that’s LDAP_PARAM_ERROR as shown in ldap.h. Ok, the URI I specified is not correct…

After getting some more doc, I rewrite the imapd.conf options to:

sasl_ldapdb_uri: ldap://glauth/dc=users,dc=local
sasl_ldapdb_id: cn=cyrus,ou=machines
sasl_ldapdb_pw: cyrus
sasl_ldapdb_mech: PLAIN
sasl_ldapdb_canon_attr: mail

What prompted me to change the sasl_ldapdb_id is the documentation:

LDAP URI (Uniform Resource Identifier). It has the following form: ldap[is]://host[:port][/dn]. This parameter is identical to that of the underlying function ldap_initialize() of the library openLDAP except the optional dn. When this dn is provided, each parameter of a method of an instance of a LDAPObject which is a DN, is automatically completed by dn unless it already ends with dn. For example, in the code below:

>>> l = ldap_initialize('ldap:://host.test/dc=example,dc=test')
>>> l.simple_bind_s(user='cn=admin', password='secret')

parameter user is rewritten to ‘cn=admin,dc=example,dc=test’ before passed to the underlying OpenLDAP library C function

No more luck, I get the exact same LDAP_PARAM_ERROR.

Let’s dig into ldap itself…

(gdb) break ldap_initialize
    Thread 2.1 "imapd" hit Breakpoint 1, ldap_initialize (ldp=ldp@entry=0x7ffe3566d780, url=0x558ea5417c30 "ldap://glauth/dc=users,dc=local") at open.c:235
235	open.c: No such file or directory.

In ldap_initialize its’s ldap_set_option that returns the error. In this function there is this interesting piece of code:

			case LDAP_URL_ERR_PARAM:	/* parameter is bad */
			case LDAP_URL_ERR_BADSCHEME:	/* URL doesn't begin with "ldap[si]://" */
			case LDAP_URL_ERR_BADENCLOSURE:	/* URL is missing trailing ">" */
			case LDAP_URL_ERR_BADURL:	/* URL is bad */
			case LDAP_URL_ERR_BADHOST:	/* host port is bad */
			case LDAP_URL_ERR_BADATTRS:	/* bad (or missing) attributes */
			case LDAP_URL_ERR_BADSCOPE:	/* scope string is invalid (or missing) */
			case LDAP_URL_ERR_BADFILTER:	/* bad or missing filter */
			case LDAP_URL_ERR_BADEXTS:	/* bad or missing extensions */
				rc = LDAP_PARAM_ERROR;
				break;

But before that, in ldap_url_parselist_int, we can see that the single url is parsed into a list of two URLs:

Thread 2.1 "imapd" hit Breakpoint 3, ldap_url_parselist_int (ludlist=ludlist@entry=0x7ffe3566d660, url=url@entry=0x558ea5417c30 "ldap://glauth/dc=users,dc=local", 
    sep=sep@entry=0x0, flags=flags@entry=3) at url.c:1279

(gdb) print urls[0]
$5 = 0x558ea542e280 "ldap://glauth/dc=users"
(gdb) print urls[1]
$6 = 0x558ea542e1c0 "dc=local"

This time, let’s try and escape the comma in the URL in imapd.conf:

sasl_ldapdb_uri: ldap://glauth/dc=users%44dc=local
sasl_ldapdb_id: cn=cyrus,ou=machines
sasl_ldapdb_pw: cyrus
sasl_ldapdb_mech: PLAIN
sasl_ldapdb_canon_attr: mail

This time, ldap_url_parselist_int and ldapdb_connect succeeds… Back to Cyrus SASL.

In ldapdb_connect, the function still returns with -1 (where 0 is probably the success code) because of ldap_sasl_interactive_bind_s. I needed to get the debian version of cyrus.c to get the correct line numbers.

		if ( sd == AC_SOCKET_INVALID || !ld->ld_defconn ) {
			/* not connected yet */

			rc = ldap_open_defconn( ld );

			if ( rc == 0 ) {

ldap_open_defconn returned rc with -1.

To get more debug messages, I manually changed the log level with gdb:

(gdb) print (&ldap_int_global_options)->ldo_debug
$16 = 255
(gdb) set var (&ldap_int_global_options)->ldo_debug = 0xFFFFFFFF
(gdb) print (&ldap_int_global_options)->ldo_debug
$17 = -1

And I got a lot of log messages with the imtest program:

S: ldap_create
base64 decoding error
Authentication failed. generic failure
Security strength factor: 128
C: C01 CAPABILITY
S: ldap_url_parse_ext(ldap://glauth/dc=users%44dc=local)
S: ldap_sasl_interactive_bind: user selected: PLAIN
S: ldap_int_sasl_bind: PLAIN
S: ldap_new_connection 1 1 0
S: ldap_int_open_connection
S: ldap_connect_to_host: TCP glauth:389
S: ldap_new_socket: 15
S: ldap_prepare_socket: 15
S: ldap_connect_to_host: Trying fd5c:4791:2773:2b8:8000::2 389
S: ldap_pvt_connect: fd: 15 tm: -1 async: 0
S: attempting to connect: 
S: connect errno: 111
S: ldap_close_socket: 15
S: ldap_new_socket: 15
S: ldap_prepare_socket: 15
S: ldap_connect_to_host: Trying 127.0.0.2:389
S: ldap_pvt_connect: fd: 15 tm: -1 async: 0
S: attempting to connect: 
S: connect errno: 111
S: ldap_close_socket: 15
S: ldap_msgfree
S: A01 NO remote authentication server unavailable

glauth is not listening on port 389 but on port 3893. This time I found a way to get way more info. For the meantime, let’s change the LDAP port number on the configuration.

Step 6: Failing to make Cyrus authenticate to glauth LDAP server

This time, the error log on the server is:

Nov 05 14:51:19 vps-e7887199 cyrus/master[16395]: about to exec /usr/lib/cyrus/bin/imapd
Nov 05 14:51:19 vps-e7887199 cyrus/imap[16395]: executed
Nov 05 14:51:19 vps-e7887199 cyrus/imap[16395]: accepted connection
Nov 05 14:51:19 vps-e7887199 cyrus/imap[16395]: SASL DIGEST-MD5 server step 1
Nov 05 14:51:19 vps-e7887199 cyrus/imap[16395]: SASL DIGEST-MD5 server step 2
Nov 05 14:51:19 vps-e7887199 cyrus/imap[16395]: SASL No worthy mechs found
Nov 05 14:51:19 vps-e7887199 cyrus/imap[16395]: SASL unable to canonify user and get auxprops
Nov 05 14:51:19 vps-e7887199 cyrus/imap[16395]: SASL DIGEST-MD5 common mech dispose
Nov 05 14:51:19 vps-e7887199 cyrus/imap[16395]: badlogin: localhost [::1] DIGEST-MD5 [SASL(-1): generic failure: unable to canonify user and get auxprops]

After restarting cyrus, I start gdb, run a test (imtest), Ctrl-C to break gdb and update log level:

# gdb -p $(pidof master) -ex 'set follow-fork-mode child'
(gdb) c

...

Thread 2.1 "imapd" received signal SIGINT, Interrupt.
(gdb) set var (&ldap_int_global_options)->ldo_debug = -1
(gdb) print (&ldap_int_global_options)->ldo_debug
$1 = -1
(gdb) c

The logs from OpenLDAP:

S: ldap_url_parse_ext(ldap://glauth:3893/dc=users%44dc=local)
S: ldap_sasl_interactive_bind: user selected: PLAIN
S: ldap_int_sasl_bind: PLAIN
S: ldap_new_connection 1 1 0
S: ldap_int_open_connection
S: ldap_connect_to_host: TCP glauth:3893
S: ldap_new_socket: 15
S: ldap_prepare_socket: 15
S: ldap_connect_to_host: Trying fd5c:4791:2773:2b8:8000::2 3893
S: ldap_pvt_connect: fd: 15 tm: -1 async: 0
S: attempting to connect: 
S: connect errno: 111
S: ldap_close_socket: 15
S: ldap_new_socket: 15
S: ldap_prepare_socket: 15
S: ldap_connect_to_host: Trying 127.0.0.2:3893
S: ldap_pvt_connect: fd: 15 tm: -1 async: 0
S: attempting to connect: 
S: connect success
S: ldap_int_sasl_open: host=glauth
S: ldap_msgfree
S: ldap_free_connection 1 1
S: ldap_send_unbind
S: ldap_free_connection: actually freed
S: A01 NO generic failure

Trying to decode the LDAP protocol with tcpdump -nvvv -i lo 'port 3893' but protocol decode is not powerful enough to understand what’s going on. Nothing seems bad on the LDAP client side, the only error to continue debugging is:

SASL No worthy mechs found

Could it be Cyrus that fails to authenticate to glauth because there is no matching login mechanism ? let’s try removing the sasl_ldapdb_mech: PLAIN configuration line…

This time, glauth is outputting some more information (the force-bind lines below):

cyrus/master[17012]: about to exec /usr/lib/cyrus/bin/imapd
cyrus/imap[17012]: executed
cyrus/imap[17012]: accepted connection
cyrus/imap[17012]: SASL DIGEST-MD5 server step 1
cyrus/imap[17012]: SASL DIGEST-MD5 server step 2
force-bind[12757]: 15:09:13.295780 Search ▶ DEBU 014 Search request as  from 127.0.0.1:56398 for (objectclass=*)
force-bind[12757]: 2020/11/05 15:09:13 handleSearchRequest error LDAP Result Code 50 "Insufficient Access Rights": Search Error: Anonymous BindDN not allowed
cyrus/imap[17012]: SASL unable to canonify user and get auxprops
cyrus/imap[17012]: SASL DIGEST-MD5 common mech dispose
cyrus/imap[17012]: badlogin: localhost [::1] DIGEST-MD5 [SASL(-13): authentication failure: unable to canonify user and get auxprops]

This time, the request makes its way to glauth. The message Search Error: Anonymous BindDN not allowed signifies that the bind request was made with an empty bind dn (-D option in ldapsearch). In our case, the bind dn should be dc=users,dc=local and we have to configure Cyrus SASL to this.

Reading on the topic on ServerFault I could grab that the bind dn was sort of like your user id and it should not be empty.

I discovered that the bind dn was always empty on Cyrus SASL in ldabdb.c:

    i = ldap_sasl_interactive_bind_s(cp->ld, NULL, ctx->mech.bv_val, NULL,
    	NULL, LDAP_SASL_QUIET, ldapdb_interact, ctx);
    if (i != LDAP_SUCCESS) {
    	sparams->utils->free(authzid);
	return i;
    }

The second parameter set to NULL is the bind db as evidenced elsewhere in the code. The solution is to either change Cyrus SASL to allow the bind dn to be configurable or change glauth to allow empty bind dn.

Step 7: New LDAP server, LDAP client not behaving

After a custom build of the LDAP server, still, it does not work. There must be something on the SASL client library that I don’t understand. Both ldapsearch and Cyrus SASL via imtest are failing.

Here is what I get with ldapsearch using a base dn:

Bind ▶ DEBU 023  "level"=6 "msg"="Bind request"  "basedn"="dc=users,dc=local" "binddn"="cn=cyrus,ou=machines,dc=users,dc=local" "src"={"IP":"127.0.0.1","Port":57894,"Zone":""}
Bind ▶ DEBU 024  "level"=6 "msg"="Bind success"  "binddn"="cn=cyrus,ou=machines,dc=users,dc=local" "src"={"IP":"127.0.0.1","Port":57894,"Zone":""}
Search ▶ DEBU 025  "level"=6 "msg"="{BaseDN:dc=users,dc=local Scope:2 DerefAliases:0 SizeLimit:0 TimeLimit:0 TypesOnly:false Filter:(mail=jdoe@example.com) Attributes:[] Controls:[]}\n"
Search ▶ DEBU 026  "level"=6 "msg"="Search request"  "basedn"=",dc=users,dc=local" "binddn"="cn=cyrus,ou=machines,dc=users,dc=local" "filter"="(mail=jdoe@example.com)" "src"={"IP":"127.0.0.1","Port":57894,"Zone":""}
Search ▶ DEBU 027  "level"=6 "msg"="AP: Search OK"  "filter"="(mail=jdoe@example.com)"

Here is what I get without base dn and authentication on LDAP:

Search ▶ DEBU 020  "level"=6 "msg"="{BaseDN: Scope:0 DerefAliases:0 SizeLimit:0 TimeLimit:0 TypesOnly:false Filter:(objectclass=*) Attributes:[supportedSASLMechanisms] Controls:[]}\n"
Search ▶ DEBU 021  "level"=6 "msg"="Search request"  "basedn"=",dc=users,dc=local" "binddn"="" "filter"="(objectclass=*)" "src"={"IP":"127.0.0.1","Port":57874,"Zone":""}
Search ▶ DEBU 022  "level"=6 "msg"="AP: Search OK"  "filter"="(objectclass=*)"

The difference is in the search request, the filter is (objectclass=*) instead of the user email (mail=jdoe@example.com). The reason why the filter is not transmitted when there is no authentication escapes to my reason for the moment.

Step 8: Making the LDAP authentication work

We’re back to making authenticated LDAP work properly. After documenting myself, I found that there are two ways to authenticate on LDAP: simple bind and SASL. Probably Cyrus SASL is trying to use SASL to authenticate itself as Cyrus on LDAP, and my LDAP server is only understanding simple bind.

SASL Bind operation is described in RFC 4513 section 5.2

While looking up implementation, I found that RFC 4512 section 5.1 defines that a client is allowed to make a search on the root DSE (a search with an empty bind dn) to find out server properties such as allowed authentication mechanisms. Such requests are made with the (objectClass=*) filter, which is the same as found on the above logs.